Build a notes app
import { Steps } from ‘@astrojs/starlight/components’;
We’ll build a small notes app and add one raku_router concept at a time: a route tree, a typed detail screen, tabs with their own back stacks, an unsaved-changes guard, and deep linking. By the end you’ll have the app you see in the live demo.
-
Install and define your routes as data.
Terminal window flutter pub add raku_routerRoutes are a
sealedhierarchy, so theswitchthat builds screens is checked for exhaustiveness — no code generation.sealed class AppRoute extends RakuRoute { const AppRoute(); }class Feed extends AppRoute { const Feed(); }class Note extends AppRoute {const Note(this.id);final String id;@overrideList<Object?> get props => [id];}class Settings extends AppRoute { const Settings(); } -
Declare the route tree and run it.
Each
route(path, parse, screen)ties a URL to a typed route and its screen. A child’s path extends its parent’s, so it stacks on top.final router = raku(initial: const Feed(),routes: [route('/feed', (_) => const Feed(), (_) => const FeedScreen(), children: [route('notes/:id', (p) => Note(p('id')), (n) => NoteScreen(id: n.id)),]),],);void main() => runApp(MaterialApp.router(routerConfig: router)); -
Navigate by typed object.
From any screen below the router, push a route — no constructors to thread. The address bar follows automatically.
// inside FeedScreen, on a note tap:onTap: () => context.push(Note(note.id)),Opening
/feed/notes/42now rebuilds[Feed, Note(42)], so back returns to the feed — that’s deep linking from the tree’s structure. -
Add tabs with their own back stacks.
Wrap your top-level routes in a
tabs(...)node. Each branch keeps its own history;context.pushof a tab route stays inside the active tab.raku(initial: const Feed(),routes: [tabs(shell: (context, tabs, child) => Scaffold(body: child,bottomNavigationBar: NavigationBar(selectedIndex: tabs.index,onDestinationSelected: tabs.go,destinations: const [/* Feed, Settings */],),),branches: [[route('/feed', (_) => const Feed(), (_) => const FeedScreen(), children: [route('notes/:id', (p) => Note(p('id')), (n) => NoteScreen(id: n.id)),])],[route('/settings', (_) => const Settings(), (_) => const SettingsScreen())],],),],); -
Guard a screen with unsaved changes.
Mix
RouteGuardinto a route.canPopis read synchronously (predictive back needs a sync answer);onPopBlockedlets you confirm.class EditNote extends AppRoute with RouteGuard {const EditNote(this.id);final String id;@overrideList<Object?> get props => [id];@overridebool get canPop => !hasUnsavedChanges;@overridevoid onPopBlocked(BuildContext context) {// show a "discard changes?" dialog}}The guard is honoured by every back path: the predictive-back gesture, the system back button, and
context.pop(). -
Ship it to the web.
The router already drives the browser’s address bar and back/forward. For clean paths instead of the
#hash, opt in once inmain():import 'package:flutter_web_plugins/url_strategy.dart';void main() {usePathUrlStrategy();runApp(MaterialApp.router(routerConfig: router));}