agentleFS
alfredobs97alfredobs97/flutter_agentic_template/.agents/skills/routing/SKILL.md

routing

Skill0 starsChanged 50 days agoMITView on GitHub

Explains how to add a route in app_router.dart — the AppRoute enum, choosing between the StatefulShellRoute and a top-level GoRoute, why buildAppRouter() must stay a function, typed arguments via state.extra with a redirect type guard, path parameters, and adding a second bottom-nav branch; triggers whenever a new screen needs a route or the shell gains a tab.

13 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use parallelization and prompt-chaining.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • hajekimSkill

    parallelization

    This skill should be used when the user wants to "run tasks in parallel", "concurrent LLM calls", "fan-out and fan-in", "parallel agents", "simultaneous execution", "reduce latency with concurrency", "parallel sub-tasks", "parallel processing", "concurrent agent execution", "run agents simultaneously", "speed up with parallelism", "async agent execution", "batch LLM calls", "parallel workflow", "parallelize agent tasks", or needs to execute multiple independent tasks simultaneously. Also responds to Korean: "병렬 작업 실행", "동시 LLM 호출", "팬아웃 팬인 패턴", "병렬 에이전트", "동시 실행", "동시성으로 지연 시간 감소", "병렬로 실행", "동시에 여러 작업", "병렬 처리", "동시에 돌려줘", "빠르게 병렬로", "여러 작업 동시 실행해줘". Also responds to Japanese: "並列処理", "同時実行", "並列エージェント", "並行LLM呼び出し", "ファンアウト・ファンイン", "複数タスクを同時に実行したい", "処理速度を上げたい", "非同期エージェント", "バッチ処理", "並列で動かしたい" Also responds to Chinese: "并行处理", "并发执行", "并行智能体", "同时调用多个LLM", "扇出扇入模式", "多任务并行运行", "加快处理速度", "异步智能体", "批量LLM调用", "同时运行多个任务".. Apply this skill to design or implement the Parallelization agentic design pattern.

    Also in 3 projects that use this

  • hajekimSkill

    prompt-chaining

    This skill should be used when the user wants to "chain prompts", "build a pipeline", "break down complex tasks into steps", "sequential LLM calls", "multi-step reasoning", "pipeline pattern", "sequential agent pipeline", "multi-step prompt pipeline", "LLM chain", "step-by-step agent", "prompt pipeline", "decompose task into prompts", "stage-by-stage processing", "chained LLM calls", or when a task is too complex for a single prompt and needs to be decomposed into sequential sub-tasks. Also responds to Korean: "프롬프트 체이닝", "체인 방식 에이전트", "단계별 LLM 호출", "복잡한 작업 분해", "순차적 프롬프트 파이프라인", "단계적 작업 처리", "단계별 처리", "순차 처리 에이전트", "파이프라인 만들어줘", "단계적으로 처리해줘", "연속 프롬프트", "체이닝 해줘". Also responds to Japanese: "プロンプトチェーニング", "ステップごとに処理", "順次LLM呼び出し", "パイプライン構築", "複雑なタスクを分解", "段階的エージェント", "連続プロンプト", "LLMパイプライン", "順番に処理してほしい", "チェーン方式エージェント" Also responds to Chinese: "提示词链", "顺序调用LLM", "构建流水线", "分解复杂任务", "逐步处理", "链式提示", "多步骤流水线", "按顺序处理任务", "提示词管道", "帮我一步一步处理".. Apply this skill to design, implement, or explain the Prompt Chaining agentic design pattern.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • 0

    Common

    Most of the 12 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 2

    Very specific

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

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

    Every navigation call references AppRoute.taskDetail.path (or context.go/context.push built from it), never a hand-typed '/task/:id' string …

    line 25
  2. Distinctive:

    Ask: does this screen keep the app's persistent navigation chrome (bottom nav bar, drawer) visible, or does it need full immersion?

    line 31
  3. Very specific:

    If buildAppRouter() were memoized, one test navigating away from /home would leak into the next test's initial location, and tests would …

    line 95

“Common” is measured: kept by at least 6 in 10 of the 12 edited versions of this file we could read. “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

Adding or changing a route

Routing is go_router via packages/ui/lib/src/router/app_router.dart (root AGENTS.md section 10). Every destination is a typed AppRoute enum value, never a string literal at the call site.

1. Add an AppRoute value

enum AppRoute {
  home('/home'),
  taskDetail('/task/:id');

  const AppRoute(this.path);

  final String path;
}

Every navigation call references AppRoute.taskDetail.path (or context.go/context.push built from it), never a hand-typed '/task/:id' string scattered across the codebase.

2. Shell branch vs top-level sibling GoRoute

Ask: does this screen keep the app's persistent navigation chrome (bottom nav bar, drawer) visible, or does it need full immersion?

  • Persistent navigation chrome → nest the GoRoute inside the existing StatefulShellRoute.indexedStack's branch:

    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) => navigationShell,
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(path: AppRoute.home.path, builder: (context, state) => const HomeScreen()),
            GoRoute(
              path: AppRoute.taskDetail.path,
              builder: (context, state) => TaskDetailScreen(taskId: state.pathParameters['id']!),
            ),
          ],
        ),
      ],
    );

    A StatefulShellRoute.indexedStack keeps every branch's navigation stack alive when switching tabs (unlike a plain GoRoute, which rebuilds from scratch) — see the TickerMode implication for animated widgets in .agents/skills/ui-screen/SKILL.md.

  • Full immersion (no shell chrome) — a checkout flow, a media viewer, an active session screen — → register it as a top-level GoRoute, a sibling of StatefulShellRoute, not nested inside a branch:

    GoRouter buildAppRouter() => GoRouter(
      initialLocation: AppRoute.home.path,
      errorBuilder: (context, state) => const RouteErrorScreen(),
      routes: [
        StatefulShellRoute.indexedStack(/* ... */),
        GoRoute(
          path: AppRoute.taskEditor.path,
          builder: (context, state) => const TaskEditorScreen(),
        ),
      ],
    );

3. Why buildAppRouter() is a function, never a singleton

buildAppRouter() must stay a plain function that returns a fresh GoRouter on every call — never memoized into a top-level final/late variable. lib/bootstrap.dart calls it once for the running app; a widget test calls it again to get its own isolated router instance instead of racing shared navigation state with every other test in the suite. This is exactly what packages/ui/test/router/app_router_test.dart relies on:

Future<void> pumpRouter(WidgetTester tester) {
  // A fresh router per test — this is exactly why buildAppRouter() must
  // stay a function and not a memoized singleton.
  final router = buildAppRouter();
  return tester.pumpWidget(/* ... MaterialApp.router(routerConfig: router) ... */);
}

If buildAppRouter() were memoized, one test navigating away from /home would leak into the next test's initial location, and tests would start failing depending on run order.

4. Typed arguments via state.extra + a redirect type guard

state.pathParameters is for plain id strings only (:id → a String). For anything richer — a typed object the destination screen needs — pass it via state.extra and add a redirect that checks the runtime type before the screen ever builds, bouncing back to a safe route if a caller navigated here without the expected argument (e.g. a deep link, or a programming mistake):

// A UI-only navigation argument — lives in the feature's models/ folder,
// not domain (it's not a business entity, just what this route needs).
class TaskEditorArgs {
  const TaskEditorArgs({required this.taskId, required this.initialTitle});

  final String taskId;
  final String initialTitle;
}

GoRoute(
  path: AppRoute.taskEditor.path,
  redirect: (context, state) {
    if (state.extra is! TaskEditorArgs) {
      return AppRoute.home.path;
    }
    return null; // null keeps the match, only a non-null String redirects.
  },
  builder: (context, state) => TaskEditorScreen(args: state.extra! as TaskEditorArgs),
);

Navigate to it with:

context.push(AppRoute.taskEditor.path, extra: TaskEditorArgs(taskId: task.id, initialTitle: task.title));

The redirect guard matters because state.extra is untyped (Object?) at the go_router level — without the guard, a bad/missing extra would only fail at the as TaskEditorArgs cast inside builder, deep in the widget tree, instead of being caught and redirected before any UI is built.

5. Path parameters for plain ids

A route segment that's just an identifier uses state.pathParameters directly — no extra, no redirect guard needed, since a String can't fail a type check:

GoRoute(
  path: AppRoute.taskDetail.path, // '/task/:id'
  builder: (context, state) => TaskDetailScreen(taskId: state.pathParameters['id']!),
);

6. Adding a second StatefulShellBranch (a bottom-nav tab)

The router currently ships with a single branch, so StatefulShellRoute.indexedStack's builder renders navigationShell directly with no visible bottom-nav bar — there's nothing to switch between yet. Adding a second branch means the shell now needs chrome to switch branches, so wrap navigationShell in a Scaffold with a NavigationBar (or BottomNavigationBar) driven by navigationShell.currentIndex and navigationShell.goBranch(index):

GoRouter buildAppRouter() => GoRouter(
  initialLocation: AppRoute.home.path,
  errorBuilder: (context, state) => const RouteErrorScreen(),
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) => Scaffold(
        body: navigationShell,
        bottomNavigationBar: NavigationBar(
          selectedIndex: navigationShell.currentIndex,
          // goBranch preserves each branch's own navigation stack; passing
          // initialLocation: true when re-tapping the already-selected tab
          // resets that branch back to its own start, matching platform
          // bottom-nav convention.
          onDestinationSelected: (index) => navigationShell.goBranch(
            index,
            initialLocation: index == navigationShell.currentIndex,
          ),
          destinations: [
            NavigationDestination(icon: const Icon(Icons.home_outlined), label: context.l10n.homeTabLabel),
            NavigationDestination(icon: const Icon(Icons.list_alt_outlined), label: context.l10n.tasksTabLabel),
          ],
        ),
      ),
      branches: [
        StatefulShellBranch(
          routes: [GoRoute(path: AppRoute.home.path, builder: (context, state) => const HomeScreen())],
        ),
        StatefulShellBranch(
          routes: [GoRoute(path: AppRoute.tasks.path, builder: (context, state) => const TaskListScreen())],
        ),
      ],
    ),
  ],
);

Each NavigationDestination label is a localized string via context.l10n, per the zero-hardcoded-strings rule — see .agents/skills/localization/SKILL.md.

Common mistakes

  • Hand-typing a path string (context.go('/task/1')) instead of building it from AppRoute.
  • Memoizing buildAppRouter()'s result into a top-level variable — breaks test isolation immediately.
  • Using state.extra for a plain id (just use state.pathParameters instead) or state.pathParameters for a typed object it can't express.
  • Skipping the redirect type guard on a state.extra route — the failure then surfaces as an unhandled cast exception deep in builder instead of a clean redirect.
  • Nesting a full-immersion screen inside a shell branch (it will render with the bottom-nav bar still visible) or putting a persistent-nav screen as a top-level sibling (it loses the shell chrome and its branch's preserved navigation stack).

Checklist

  • New AppRoute enum value added, referenced everywhere instead of a string literal.
  • Route placed inside the shell branch (persistent chrome) or as a top-level sibling GoRoute (full immersion) — deliberately, not by default.
  • buildAppRouter() was not memoized into a singleton.
  • A typed argument uses state.extra with a redirect type guard; a plain id uses state.pathParameters.
  • A route test exists, building a fresh buildAppRouter() instance (see .agents/skills/testing/SKILL.md).

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.