agentleFS
NarekManukyanNarekManukyan/flutter-mobx-kit/skills/add-route/SKILL.md

add-route

Skill0 starsChanged 56 days agoMITView on GitHub

Register a screen with AutoRoute — the @RoutePage annotation, router entry, path and naming conventions, route parameters, nested routes, guards, modals and dialogs, and the AppNavigator methods states call. Use when adding a screen to the router, adding a guard, or navigating somewhere new.

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Run melos run build — this generates the {PageName}Route class in app_router.gr.dart.

    line 13
  2. Very specific:

    Add an AutoRoute entry to AppRouter.routes.

    line 14
  3. Very specific:

    Every path must end in exactly one of resolver.next() or resolver.redirectUntil(...).

    line 137

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Add a route

Governed by ADR-0008. The router lives in lib/core/navigation/; guards in lib/core/guards/.

The loop

  1. Annotate the page with @RoutePage() (create-page).
  2. Run melos run build — this generates the {PageName}Route class in app_router.gr.dart.
  3. Add an AutoRoute entry to AppRouter.routes.
  4. Navigate to it from a state class via AppNavigator. Never from a widget.

Router entry

AppRouter is not registered in DI. AppNavigator (a @singleton) builds it and hands it its guards, which get their dependencies through the constructor (ADR-0007):

@AutoRouterConfig(replaceInRouteName: 'Page,Route')
class AppRouter extends RootStackRouter {
  AppRouter({required AuthGuard authGuard}) : _authGuard = authGuard;

  final AuthGuard _authGuard;

  @override
  late final List<AutoRoute> routes = [
    AutoRoute(
      initial: true,
      path: '/',
      page: SplashRoute.page,
      // No guard: SplashPageState decides between home and login.
    ),
    AutoRoute(path: '/login', page: LoginRoute.page),
    AutoRoute(path: '/home', page: HomeRoute.page, guards: [_authGuard]),
    // Illustrative: nested routes. The template has none.
    AutoRoute(
      path: '/dashboard',
      page: DashboardRoute.page,
      guards: [_authGuard],
      meta: const {'description': 'Signed-in dashboard with its tabs'},
      children: [
        AutoRoute(path: 'agenda', page: AgendaRoute.page),
        AutoRoute(path: 'meetings', page: MeetingsListRoute.page),
      ],
    ),
  ];
}

The real router is lib/core/navigation/app_router.dart; it also has /todos/:id and /challenges/joined, both guarded.

  • replaceInRouteName: 'Page,Route' is why LoginPage generates LoginRoute. Always use the generated class, never a string.
  • Paths are kebab-case and describe the page: /login, /sign-up-with-email. Children use relative paths.
  • Give a new route meta: {'description': …}; it is what makes the router readable a year later. The template's own routes do not carry one yet.
  • The initial route is not guarded. A guard on / let a signed-in user through to the splash page and left them there. SplashPageState.init reads the token and replaces the stack with Home or Login.

Parameters

Passed through the generated route's constructor, const wherever possible:

await _appNavigator.push(TodoDetailsRoute(todoId: id));
await _appNavigator.push(const SettingsRoute());

Optional values are named parameters. Do not pass objects a deep link could not reconstruct — pass an id and let the target load it.

Navigating — from the state class only

// ❌ FORBIDDEN, anywhere in a widget
context.router.push(const SettingsRoute());

// ✅ inject AppNavigator into the state
abstract class _MyPageStateBase with Store {
  final AppNavigator _appNavigator;
  _MyPageStateBase(this._appNavigator);

  @action
  Future<void> onSettingsPressed() =>
      _appNavigator.push(const SettingsRoute());
}

Pages call the state method. If a navigation decision has real logic in it, extract a Navigation use case (create-use-case).

AppNavigator methods

MethodDoes
push(route)push a new route
pop<T>({result})pop the current route
popUntilRoot()pop to the root
popUntilRouteWithName(name)pop back to a named route
replace(route)replace the current route
pushAndPopAll(route)replace the whole stack — use after login and logout
navigatePath(path)navigate by path string (deep-link style)
canPop()whether the router can pop
showModal({builder, …})modal bottom sheet
showScaffoldMessage(widget)bottom overlay notification, used by IssueTrackingService
showFullScreenLoading() / hideFullScreenLoading()blocking spinner dialog, and pop it
openPhoneSettings()the app's page in system settings

showAppDialog, showErrorMessage and showSuccessMessage also exist, but their bodies are empty: calling them shows nothing. Implement one before a state relies on it.

Guards

Guards extend AutoRouteGuard, live in lib/core/guards/, and are listed on the route they protect. They are not registered in DI: AppNavigator constructs each one with its dependencies and passes it to AppRouter (AppRouter(authGuard: AuthGuard(authStore))), so a new guard is a new AppRouter constructor parameter too.

class AuthGuard extends AutoRouteGuard {
  AuthGuard(this._authStore);

  final AuthStore _authStore;

  @override
  Future<void> onNavigation(
    NavigationResolver resolver,
    StackRouter router,
  ) async {
    // Reads the stored token, which sets isAuthed on a cold start.
    await _authStore.getAccessToken();

    if (!_authStore.isAuthed) {
      resolver.redirectUntil(const LoginRoute());
      return;
    }

    resolver.next();
  }
}

Every path must end in exactly one of resolver.next() or resolver.redirectUntil(...). A guard that returns without calling either hangs the navigation with no error.

Modals and dialogs

Reached through AppNavigator, not showModalBottomSheet directly, so the state layer stays the only thing that knows about presentation:

await _appNavigator.showModal(builder: (_) => const AddTodoModal());

A modal with its own state is not a feature (ADR-0005): it has no route. It lives inside the feature that opens it, at lib/features/{feature}/modals/{modal}/, with view/ for the widget, mobx/ for its state and use_cases/ when it has one, as home/modals/add_todo_modal/ does (create-feature). Its widget is a page root: Provider(create: (_) => getIt<AddTodoModalState>()).

E2E

clearState: true in a Maestro flow clears the auth token, so splash sends the app to login and a guarded route redirects there. Flows start from the shared common/start_home.yaml subflow rather than assuming a signed-in state (write-maestro-flow).

Checklist

  • @RoutePage() on the page, melos run build run
  • AutoRoute entry added with a kebab-case path and a meta description; no guard on the initial route
  • Generated {Page}Route class used, never a path string
  • Parameters are ids, not objects a deep link could not rebuild
  • Navigation only from a state class via AppNavigator; no context.router anywhere
  • Guards live in lib/core/guards/, are constructed by AppNavigator with their dependencies, and every path calls next() or redirectUntil()
  • Deep link into the route works from a cold start

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.