KasyKasy

Navigation

GoRouter, the bottom menu, where the app opens in debug vs production, and transitions.

Where does the app open? (debug vs production)

In debug (kasy run) the app may go back to the last screen you were on (e.g., /admin, /components). This is intentional: it helps with day-to-day work and hot restart (R), which restarts the app from scratch without losing context.

In production (a release build published to the store or the web) that behavior doesn't exist. The app doesn't "remember" the last screen between sessions.

SituationWhat happens
Native in production (closes and reopens)Always starts at /. If logged in, goes to Home. If not, login or onboarding
Web in production (yourdomain.com/)Opens at the URL the user typed. Root / → Home (after auth). /settings → Settings
Debug (kasy run, hot restart R)May resume the last visited route, including internal screens like admin

Summary: debug and production don't behave the same on the initial screen. If you ran kasy run --web, opened localhost:5555, and landed on admin, that doesn't mean the end user will see that when they open the app.

Web: the URL is the screen's address

On the published web, the address bar rules. Bookmarks, F5, and shared links work because every route has a URL (/premium, /admin/kanban, etc.). Protected routes (e.g., admin) still go through the auth redirect: whoever isn't an admin gets sent to Home.

To open a specific screen in production:

  • Web: use the direct URL (https://yourapp.com/settings)
  • Push: send the route field in the notification payload (see Push)
  • Native (external link): configure universal links / app links on iOS and Android so the OS hands the URL to the app

How to test as production on your Mac

flutter run --release          # native
flutter run --release -d chrome   # web in release

In release, the app always cold-starts at / (native) or at the opened URL (web), just like the end user will see it.


Three navigation layers

LayerFileUsage
Global applib/router.dartcontext.go('/premium'), context.push('/feedback')
Bottom tabslib/core/bottom_menu/Home, Wishlist, Notifications, Settings
Onboardingonboarding_page.dartInternal Navigator (feature_1 → paywall)

Without BuildContext (notifiers)

ref.read(goRouterProvider).go('/signin');
ref.read(goRouterProvider).push('/premium');

Changing animations for the whole app

One file controls the defaults:

lib/core/navigation/kasy_navigation_config.dart

class KasyNavigationConfig {
  static KasyTransitionKind push = KasyTransitionKind.fade;                 // most screens
  static KasyTransitionKind replace = KasyTransitionKind.fade;
  static KasyTransitionKind authPeer = KasyTransitionKind.sharedAxisScaled; // sign in and sign up
  static KasyTransitionKind bottomTab = KasyTransitionKind.fade;            // bottom menu tabs
  static KasyTransitionKind onboardingStep = KasyTransitionKind.fade;       // onboarding steps

  static const Duration duration = Duration(milliseconds: 250);
  static const Curve curve = Curves.easeOutCubic;
}

Available types: fade, fadeThrough, sharedAxisScaled, sharedAxisHorizontal, none.

What you want to changeWhere
The app's default animationpush
Sign in to sign upauthPeer
Bottom menu tab switchingbottomTab
Onboarding stepsonboardingStep
Speed or curve of everythingduration and curve

A new screen with the kit's transition

On a new route always use pageBuilder with kasyTransitionPage, never builder alone:

GoRoute(
  path: '/my_screen',
  pageBuilder: (context, state) => kasyTransitionPage(
    key: state.pageKey,
    child: const MyScreenPage(),
  ),
),

To break the default on one specific route, pass transition::

pageBuilder: (context, state) => kasyTransitionPage(
  key: state.pageKey,
  transition: KasyTransitionKind.none,
  child: const MyTechnicalRoute(),
),

What stays out of the standard (on purpose)

TypeExamplesWhy
DialogsshowAppDialog, showGeneralDialogNative system animation
Bottom sheetsshowModalBottomSheetSystem behavior
In-screen animationAnimatedSwitcher, flutter_animateLocal UX, not a route change

Standardizing pays off on full screens. Don't force kasyTransitionPage into a dialog or sheet.

Sub-screens inside a tab

Screens that open inside a bottom menu tab (Settings and Reminders, for example) are not standalone routes: they use the tab's inner path, with bartInnerPath('settings', 'reminder') or the settingsInnerPath('reminder') shortcut. The first segment must be the tab id (home, wishlist, notifications, settings), otherwise the tab bar loses its reference and breaks. Global screens (Premium, Feedback, Assistant) still use context.push('/route') on GoRouter.

What decides where you go

Redirects and guards don't define a visual effect, only a destination:

FileRole
lib/core/navigation/auth_redirect.dartGoRouter's global redirect: sends you to sign-in, onboarding or Home depending on the session
lib/core/navigation/auth_route_policy.dartThe rules that redirect consults (onboarding done, guest session, and so on)
lib/core/guards/guard.dartWidget guard: waits on an async check and falls back to a route
lib/core/security/biometric_guard.dartRequires Face ID / biometrics before showing the screen
lib/environments.dart and lib/core/config/features.dartRules per platform and per enabled feature

Bars that hide on scroll

On phones, scrolling down hides the app bar (top) and the bottom menu together; scrolling back up, or reaching the top, brings them back. It's the Instagram and Facebook behavior, and it gives more room for content. On tablet and desktop navigation is the sidebar, which never hides.

By default the app shows a toggle to the end user under Settings → Preferences → "Hide bars when scrolling". Whoever builds the project controls it with two constants in lib/core/chrome/chrome_visibility.dart:

/// Shows (true) or hides (false) the toggle on the Settings screen.
const bool kShowHideChromeOnScrollSetting = true;

/// Default behavior: true = hide on scroll, false = fixed bars.
const bool kHideChromeOnScrollDefault = true;
GoalWhat to do
Let the end user choose (default)kShowHideChromeOnScrollSetting = true
Remove the option from SettingskShowHideChromeOnScrollSetting = false
Default to "hide on scroll"kHideChromeOnScrollDefault = true
Default to "bars always fixed"kHideChromeOnScrollDefault = false

With the toggle hidden, the behavior is locked to kHideChromeOnScrollDefault and the end user cannot change it.

Android, iOS, and web alike

Transitions are centralized. You won't end up with Zoom on Android and Cupertino on iOS by accident.

Last updated on 08/23/2026