unrouter 0.9.0
unrouter: ^0.9.0 copied to clipboard
A URL-first typed router for Flutter with shell navigation and diagnostics.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:unrouter/machine.dart';
import 'package:unrouter/unrouter.dart';
void main() {
runApp(UnrouterExampleApp());
}
final DemoSession _session = DemoSession();
final ValueNotifier<int?> _lastUserResult = ValueNotifier<int?>(null);
final ValueNotifier<String> _machineBackStatus = ValueNotifier<String>('idle');
Unrouter<AppRoute> _createRouter() {
return Unrouter<AppRoute>(
routes: <RouteRecord<AppRoute>>[
route<HomeRoute>(
path: '/',
parse: (_) => const HomeRoute(),
builder: (_, _) => const HomeScreen(),
),
route<UserRoute>(
path: '/users/:id',
parse: (state) => UserRoute(id: state.pathInt('id')),
builder: (_, route) => UserScreen(route: route),
),
route<SettingsRoute>(
path: '/settings',
parse: (_) => const SettingsRoute(),
builder: (_, _) => const SettingsScreen(),
),
route<LoginRoute>(
path: '/login',
parse: (state) => LoginRoute(from: state.queryOrNull('from')),
builder: (_, route) => LoginScreen(route: route),
),
route<SecureRoute>(
path: '/secure',
parse: (_) => const SecureRoute(),
guards: <RouteGuard<SecureRoute>>[
(context) {
if (_session.isSignedIn) {
return RouteGuardResult.allow();
}
return RouteGuardResult.redirect(
LoginRoute(from: context.uri.toString()).toUri(),
);
},
],
builder: (_, _) => const SecureScreen(),
),
],
unknown: (_, uri) => UnknownRouteScreen(uri: uri),
);
}
class UnrouterExampleApp extends StatelessWidget {
UnrouterExampleApp({super.key}) : _router = _createRouter() {
_session.signOut();
_lastUserResult.value = null;
_machineBackStatus.value = 'idle';
}
final Unrouter<AppRoute> _router;
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'unrouter example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1363DF)),
useMaterial3: true,
),
routerConfig: _router,
);
}
}
sealed class AppRoute implements RouteData {
const AppRoute();
}
final class HomeRoute extends AppRoute {
const HomeRoute();
@override
Uri toUri() => Uri(path: '/');
}
final class UserRoute extends AppRoute {
const UserRoute({required this.id});
final int id;
@override
Uri toUri() => Uri(path: '/users/$id');
}
final class SettingsRoute extends AppRoute {
const SettingsRoute();
@override
Uri toUri() => Uri(path: '/settings');
}
final class LoginRoute extends AppRoute {
const LoginRoute({this.from});
final String? from;
@override
Uri toUri() {
return Uri(
path: '/login',
queryParameters: from == null ? null : <String, String>{'from': from!},
);
}
}
final class SecureRoute extends AppRoute {
const SecureRoute();
@override
Uri toUri() => Uri(path: '/secure');
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final controller = context.unrouterAs<AppRoute>();
final machine = context.unrouterMachineAs<AppRoute>();
final state = controller.state;
return AnimatedBuilder(
animation: _session,
builder: (context, _) {
return Scaffold(
appBar: AppBar(
title: const Text('unrouter example'),
actions: <Widget>[
TextButton(
key: const Key('app-auth-toggle'),
onPressed: _session.toggle,
child: Text(_session.isSignedIn ? 'Sign out' : 'Sign in'),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'state: ${state.resolution.name} ${state.routePath ?? '-'}',
key: const Key('home-state-line'),
),
const SizedBox(height: 8),
ValueListenableBuilder<int?>(
valueListenable: _lastUserResult,
builder: (context, value, _) {
return Text('lastUserResult: ${value ?? '-'}');
},
),
const SizedBox(height: 8),
ValueListenableBuilder<String>(
valueListenable: _machineBackStatus,
builder: (context, value, _) {
return Text('machineBack: $value');
},
),
const SizedBox(height: 20),
FilledButton(
key: const Key('home-go-user'),
onPressed: () async {
final value = await controller.push<int>(
const UserRoute(id: 42),
);
_lastUserResult.value = value;
},
child: const Text('Push /users/42'),
),
const SizedBox(height: 8),
FilledButton.tonal(
key: const Key('home-go-settings'),
onPressed: () {
controller.go(const SettingsRoute());
},
child: const Text('Go /settings'),
),
const SizedBox(height: 8),
FilledButton.tonal(
key: const Key('home-go-secure'),
onPressed: () {
controller.go(const SecureRoute());
},
child: const Text('Go /secure (guarded)'),
),
const SizedBox(height: 8),
OutlinedButton(
key: const Key('home-machine-back'),
onPressed: () {
final accepted = machine.dispatch<bool>(
UnrouterMachineCommand.back(),
);
_machineBackStatus.value = accepted
? 'accepted'
: 'rejected';
},
child: const Text('machine.dispatch(back)'),
),
],
),
),
);
},
);
}
}
class UserScreen extends StatelessWidget {
const UserScreen({super.key, required this.route});
final UserRoute route;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('User')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('User ${route.id}'),
const SizedBox(height: 12),
FilledButton(
key: const Key('user-pop-result'),
onPressed: () {
context.unrouter.pop(route.id * 10);
},
child: const Text('Pop with typed result'),
),
],
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: Center(
child: FilledButton(
key: const Key('settings-back'),
onPressed: () {
context.unrouter.back();
},
child: const Text('Back'),
),
),
);
}
}
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key, required this.route});
final LoginRoute route;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Sign in required'),
const SizedBox(height: 8),
Text('from: ${route.from ?? '/'}'),
const SizedBox(height: 16),
FilledButton(
key: const Key('login-sign-in-continue'),
onPressed: () {
_session.signIn();
final target = Uri.tryParse(route.from ?? '');
if (target == null) {
context.unrouter.go(const HomeRoute());
return;
}
context.unrouter.goUri(target);
},
child: const Text('Sign in and continue'),
),
],
),
),
);
}
}
class SecureScreen extends StatelessWidget {
const SecureScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Secure')),
body: const Center(child: Text('Secure area', key: Key('secure-title'))),
);
}
}
class UnknownRouteScreen extends StatelessWidget {
const UnknownRouteScreen({super.key, required this.uri});
final Uri uri;
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('Unknown route: ${uri.path}')));
}
}
class DemoSession extends ChangeNotifier {
bool _isSignedIn = false;
bool get isSignedIn => _isSignedIn;
void signIn() {
_isSignedIn = true;
notifyListeners();
}
void signOut() {
_isSignedIn = false;
notifyListeners();
}
void toggle() {
if (_isSignedIn) {
signOut();
return;
}
signIn();
}
}