diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 153978168be..d5aaf837a81 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.1 + +* Improves README example and updates it to use code excerpts. + ## 2.4.0 * Adds support for passing observers to the ShellRoute for the nested Navigator. diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index 73faf973937..7fec272e80f 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -8,12 +8,12 @@ To use `go_router_builder`, you need to have the following dependencies in ```yaml dependencies: # ...along with your other dependencies - go_router: ^9.0.3 + go_router: ^13.0.0 dev_dependencies: # ...along with your other dev-dependencies - build_runner: ^2.0.0 - go_router_builder: ^2.3.0 + build_runner: ^2.4.0 + go_router_builder: ^2.4.0 ``` ### Source code @@ -23,10 +23,11 @@ Along with importing the `go_router.dart` library, it's essential to also include a `part` directive that references the generated Dart file. The generated file will always have the name `[source_file].g.dart`. + ```dart import 'package:go_router/go_router.dart'; -part 'this_file.g.dart'; +part 'example_builder_output.g.dart'; ``` ### Running `build_runner` @@ -50,23 +51,28 @@ via the `pathParameters` and `queryParameters` properties of the `GoRouterState` often the page builder must first parse the parameters into types that aren't `String`s, e.g. + ```dart -GoRoute( - path: ':authorId', - builder: (context, state) { - // require the authorId to be present and be an integer - final authorId = int.parse(state.pathParameters['authorId']!); - return AuthorDetailsScreen(authorId: authorId); - }, -), +routes: [ + GoRoute( + path: '/author/:authorId', + builder: (BuildContext context, GoRouterState state) { + // require the authorId to be present and be an integer + final int authorId = int.parse(state.pathParameters['authorId']!); + return AuthorDetailsScreen(authorId: authorId); + }, + ), +], ``` In this example, the `authorId` parameter is a) required and b) must be an `int`. However, neither of these requirements are checked until run-time, making it easy to write code that is not type-safe, e.g. + ```dart -void _tap() => context.go('/author/a42'); // error: `a42` is not an `int` +onPressed: () => + context.go('/author/a42'), // error: `a42` is not an `int` ``` Dart's type system allows mistakes to be caught at compile-time instead of @@ -80,6 +86,7 @@ boilerplate code implementations ourselves. Define each route as a class extending `GoRouteData` and overriding the `build` method. + ```dart class HomeRoute extends GoRouteData { const HomeRoute(); @@ -93,24 +100,31 @@ class HomeRoute extends GoRouteData { The tree of routes is defined as an attribute on each of the top-level routes: + ```dart @TypedGoRoute( path: '/', routes: >[ TypedGoRoute( - path: 'family/:familyId', - ) + path: 'family/:fid', +// ··· + ), +// ··· ], ) class HomeRoute extends GoRouteData { const HomeRoute(); @override - Widget build(BuildContext context, GoRouterState state) => HomeScreen(families: familyData); + Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); } -@TypedGoRoute(path: '/login') -class LoginRoute extends GoRouteData {...} +@TypedGoRoute( + path: '/login' +) +class LoginRoute extends GoRouteData { +// ··· +} ``` ## `GoRouter` initialization @@ -118,46 +132,53 @@ class LoginRoute extends GoRouteData {...} The code generator aggregates all top-level routes into a single list called `$appRoutes` for use in initializing the `GoRouter` instance: + ```dart -final _router = GoRouter(routes: $appRoutes); +final GoRouter _router = GoRouter(routes: $appRoutes); ``` ## Error builder One can use typed routes to provide an error builder as well: + ```dart class ErrorRoute extends GoRouteData { ErrorRoute({required this.error}); final Exception error; @override - Widget build(BuildContext context, GoRouterState state) => ErrorScreen(error: error); + Widget build(BuildContext context, GoRouterState state) => + ErrorScreen(error: error); } ``` With this in place, you can provide the `errorBuilder` parameter like so: + ```dart -final _router = GoRouter( - routes: $appRoutes, - errorBuilder: (c, s) => ErrorRoute(s.error!).build(c), -); + final GoRouter _router = GoRouter( + errorBuilder: (BuildContext c, GoRouterState s) => + ErrorRoute(error: s.error!).build(c, s), +// ··· + ); ``` ## Navigation Navigate using the `go` or `push` methods provided by the code generator: + ```dart -void _tap() => PersonRoute(fid: 'f2', pid: 'p1').go(context); +onTap: () => PersonRoute(family.id, p.id).go(context), ``` If you get this wrong, the compiler will complain: + ```dart // error: missing required parameter 'fid' -void _tap() => PersonRoute(pid: 'p1').go(context); +onPressed: () => const PersonRoute(pid: 'p1').go(context), ``` This is the point of typed routing: the error is found statically. @@ -167,9 +188,10 @@ This is the point of typed routing: the error is found statically. Starting from `go_router` 6.5.0, pushing a route and subsequently popping it, can produce a return value. The generated routes also follow this functionality. + ```dart -void _tap() async { - final result = await PersonRoute(pid: 'p1').go(context); +Future _tap(BuildContext context) async { + final String result = await const PersonRoute(pid: 'p1').go(context); } ``` @@ -177,14 +199,19 @@ void _tap() async { Parameters (named or positional) not listed in the path of `TypedGoRoute` indicate query parameters: + ```dart -@TypedGoRoute(path: '/login') +@TypedGoRoute( + path: '/login' +) class LoginRoute extends GoRouteData { - LoginRoute({this.from}); - final String? from; + const LoginRoute({this.fromPage}); + + final String? fromPage; @override - Widget build(BuildContext context, GoRouterState state) => LoginScreen(from: from); + Widget build(BuildContext context, GoRouterState state) => + LoginScreen(from: fromPage); } ``` @@ -192,14 +219,16 @@ class LoginRoute extends GoRouteData { For query parameters with a **non-nullable** type, you can define a default value: + ```dart -@TypedGoRoute(path: '/my-route') +@TypedGoRoute(path: '/my-route') class MyRoute extends GoRouteData { MyRoute({this.queryParameter = 'defaultValue'}); final String queryParameter; @override - Widget build(BuildContext context, GoRouterState state) => MyScreen(queryParameter: queryParameter); + Widget build(BuildContext context, GoRouterState state) => + MyScreen(queryParameter: queryParameter); } ``` @@ -211,20 +240,26 @@ A query parameter that equals to its default value is not included in the locati A route can consume an extra parameter by taking it as a typed constructor parameter with the special name `$extra`: + ```dart -class PersonRouteWithExtra extends GoRouteData { - PersonRouteWithExtra({this.$extra}); - final int? $extra; +@TypedGoRoute(path: '/optionalExtra') +class OptionalExtraRoute extends GoRouteData { + const OptionalExtraRoute({this.$extra}); + + final Extra? $extra; @override - Widget build(BuildContext context, GoRouterState state) => PersonScreen(personId: $extra); + Widget build(BuildContext context, GoRouterState state) => + OptionalExtraScreen(extra: $extra); } ``` Pass the extra param as a typed object: + ```dart -void _tap() => PersonRouteWithExtra(Person(name: 'Marvin', age: 42)).go(context); +onPressed: () => + const OptionalExtraRoute($extra: Extra(2)).go(context), ``` The `$extra` parameter is still passed outside the location, still defeats @@ -235,6 +270,7 @@ recommended when targeting Flutter web. You can, of course, combine the use of path, query and $extra parameters: + ```dart @TypedGoRoute(path: '/:ketchup') class HotdogRouteWithEverything extends GoRouteData { @@ -244,7 +280,8 @@ class HotdogRouteWithEverything extends GoRouteData { final Sauce $extra; // special $extra parameter @override - Widget build(BuildContext context, GoRouterState state) => HotdogScreen(ketchup, mustard, $extra); + Widget build(BuildContext context, GoRouterState state) => + HotdogScreen(ketchup, mustard, $extra); } ``` @@ -255,25 +292,42 @@ This seems kinda silly, but it works. Redirect using the `location` property on a route provided by the code generator: + ```dart -redirect: (state) { - final loggedIn = loginInfo.loggedIn; - final loggingIn = state.matchedLocation == LoginRoute().location; - if( !loggedIn && !loggingIn ) return LoginRoute(from: state.matchedLocation).location; - if( loggedIn && loggingIn ) return HomeRoute().location; +// redirect to the login page if the user is not logged in +redirect: (BuildContext context, GoRouterState state) { + final bool loggedIn = loginInfo.loggedIn; + + // check just the matchedLocation in case there are query parameters + final String loginLoc = const LoginRoute().location; + final bool goingToLogin = state.matchedLocation == loginLoc; + + // the user is not logged in and not headed to /login, they need to login + if (!loggedIn && !goingToLogin) { + return LoginRoute(fromPage: state.matchedLocation).location; + } + + // the user is logged in and headed to /login, no need to login again + if (loggedIn && goingToLogin) { + return const HomeRoute().location; + } + + // no need to redirect at all return null; -} +}, ``` ## Route-level redirection Handle route-level redirects by implementing the `redirect` method on the route: + ```dart class HomeRoute extends GoRouteData { // no need to implement [build] when this [redirect] is unconditional @override - String? redirect(BuildContext context, GoRouterState state) => BooksRoute().location; + String? redirect(BuildContext context, GoRouterState state) => + BooksRoute().location; } ``` @@ -282,15 +336,19 @@ class HomeRoute extends GoRouteData { The code generator can convert simple types like `int` and `enum` to/from the `String` type of the underlying pathParameters: + ```dart enum BookKind { all, popular, recent } +@TypedGoRoute(path: '/books') class BooksRoute extends GoRouteData { BooksRoute({this.kind = BookKind.popular}); + final BookKind kind; @override - Widget build(BuildContext context, GoRouterState state) => BooksScreen(kind: kind); + Widget build(BuildContext context, GoRouterState state) => + BooksScreen(kind: kind); } ``` @@ -310,15 +368,15 @@ type, pass non-default parameters when creating the page (like a custom key) or access the `GoRouteState` object, you can override the `buildPage` method of the base class instead of the `build` method: + ```dart -class MyMaterialRouteWithKey extends GoRouteData { - static final _key = LocalKey('my-route-with-key'); +class MyMaterialRoute extends GoRouteData { @override MaterialPage buildPage(BuildContext context, GoRouterState state) => - MaterialPage( - key: _key, - child: MyPage(), - ); + MaterialPage( + key: state.pageKey, + child: const MyPage(), + ); } ``` @@ -326,16 +384,19 @@ class MyMaterialRouteWithKey extends GoRouteData { Overriding the `buildPage` method is also useful for custom transitions: + ```dart class FancyRoute extends GoRouteData { @override - MaterialPage buildPage(BuildContext context, GoRouterState state) => - CustomTransitionPage( - key: state.pageKey, - child: FancyPage(), - transitionsBuilder: (context, animation, animation2, child) => - RotationTransition(turns: animation, child: child), - ), + CustomTransitionPage buildPage( + BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const FancyPage(), + transitionsBuilder: (BuildContext context, Animation animation, + Animation animation2, Widget child) => + RotationTransition(turns: animation, child: child), + ); } ``` @@ -350,6 +411,7 @@ different navigator. This kind of scenarios can be achieved by declaring a Example: + ```dart // For ShellRoutes: final GlobalKey shellNavigatorKey = GlobalKey(); @@ -361,11 +423,13 @@ class MyShellRouteData extends ShellRouteData { @override Widget builder(BuildContext context, GoRouterState state, Widget navigator) { - // ... +// ··· } } // For GoRoutes: +final GlobalKey rootNavigatorKey = GlobalKey(); + class MyGoRouteData extends GoRouteData { const MyGoRouteData(); @@ -373,7 +437,7 @@ class MyGoRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - // ... + // ··· } } ``` @@ -384,4 +448,4 @@ An example is available [here](https://github.com/flutter/packages/blob/main/pac To run unit tests, run command `dart tool/run_tests.dart` from `packages/go_router_builder/`. -To run tests in examples, run `flutter test` from `packages/go_router_builder/example`. \ No newline at end of file +To run tests in examples, run `flutter test` from `packages/go_router_builder/example`. diff --git a/packages/go_router_builder/example/lib/example_builder_output.g.dart b/packages/go_router_builder/example/lib/example_builder_output.g.dart new file mode 100644 index 00000000000..d0fa8276d45 --- /dev/null +++ b/packages/go_router_builder/example/lib/example_builder_output.g.dart @@ -0,0 +1,136 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: always_specify_types, public_member_api_docs + +part of 'readme_excerpts.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $myRoute, + $hotdogRouteWithEverything, + $booksRoute, + ]; + +RouteBase get $myRoute => GoRouteData.$route( + path: '/my-route', + factory: $MyRouteExtension._fromState, + ); + +extension $MyRouteExtension on MyRoute { + static MyRoute _fromState(GoRouterState state) => MyRoute( + queryParameter: + state.uri.queryParameters['query-parameter'] ?? 'defaultValue', + ); + + String get location => GoRouteData.$location( + '/my-route', + queryParams: { + if (queryParameter != 'defaultValue') + 'query-parameter': queryParameter, + }, + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $hotdogRouteWithEverything => GoRouteData.$route( + path: '/:ketchup', + factory: $HotdogRouteWithEverythingExtension._fromState, + ); + +extension $HotdogRouteWithEverythingExtension on HotdogRouteWithEverything { + static HotdogRouteWithEverything _fromState(GoRouterState state) => + HotdogRouteWithEverything( + _$boolConverter(state.pathParameters['ketchup']!), + state.uri.queryParameters['mustard'], + state.extra as Sauce, + ); + + String get location => GoRouteData.$location( + '/${Uri.encodeComponent(ketchup.toString())}', + queryParams: { + if (mustard != null) 'mustard': mustard, + }, + ); + + void go(BuildContext context) => context.go(location, extra: $extra); + + Future push(BuildContext context) => + context.push(location, extra: $extra); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: $extra); + + void replace(BuildContext context) => + context.replace(location, extra: $extra); +} + +bool _$boolConverter(String value) { + switch (value) { + case 'true': + return true; + case 'false': + return false; + default: + throw UnsupportedError('Cannot convert "$value" into a bool.'); + } +} + +RouteBase get $booksRoute => GoRouteData.$route( + path: '/books', + factory: $BooksRouteExtension._fromState, + ); + +extension $BooksRouteExtension on BooksRoute { + static BooksRoute _fromState(GoRouterState state) => BooksRoute( + kind: _$convertMapValue('kind', state.uri.queryParameters, + _$BookKindEnumMap._$fromName) ?? + BookKind.popular, + ); + + String get location => GoRouteData.$location( + '/books', + queryParams: { + if (kind != BookKind.popular) 'kind': _$BookKindEnumMap[kind], + }, + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +const _$BookKindEnumMap = { + BookKind.all: 'all', + BookKind.popular: 'popular', + BookKind.recent: 'recent', +}; + +T? _$convertMapValue( + String key, + Map map, + T Function(String) converter, +) { + final value = map[key]; + return value == null ? null : converter(value); +} + +extension on Map { + T _$fromName(String value) => + entries.singleWhere((element) => element.value == value).key; +} diff --git a/packages/go_router_builder/example/lib/extra_example.dart b/packages/go_router_builder/example/lib/extra_example.dart index b16d6ff030c..d1aaaddcf6c 100644 --- a/packages/go_router_builder/example/lib/extra_example.dart +++ b/packages/go_router_builder/example/lib/extra_example.dart @@ -58,6 +58,7 @@ class RequiredExtraScreen extends StatelessWidget { } } +// #docregion ExtraParameter @TypedGoRoute(path: '/optionalExtra') class OptionalExtraRoute extends GoRouteData { const OptionalExtraRoute({this.$extra}); @@ -68,6 +69,7 @@ class OptionalExtraRoute extends GoRouteData { Widget build(BuildContext context, GoRouterState state) => OptionalExtraScreen(extra: $extra); } +// #enddocregion ExtraParameter class OptionalExtraScreen extends StatelessWidget { const OptionalExtraScreen({super.key, this.extra}); @@ -108,8 +110,10 @@ class Splash extends StatelessWidget { child: const Text('Required Extra'), ), ElevatedButton( + // #docregion PassExtraParameter onPressed: () => const OptionalExtraRoute($extra: Extra(2)).go(context), + // #enddocregion PassExtraParameter child: const Text('Optional Extra'), ), ElevatedButton( diff --git a/packages/go_router_builder/example/lib/main.dart b/packages/go_router_builder/example/lib/main.dart index 773b6687ed9..b422ecb6554 100644 --- a/packages/go_router_builder/example/lib/main.dart +++ b/packages/go_router_builder/example/lib/main.dart @@ -36,6 +36,7 @@ class App extends StatelessWidget { debugLogDiagnostics: true, routes: $appRoutes, +// #docregion Redirection // redirect to the login page if the user is not logged in redirect: (BuildContext context, GoRouterState state) { final bool loggedIn = loginInfo.loggedIn; @@ -57,17 +58,20 @@ class App extends StatelessWidget { // no need to redirect at all return null; }, +// #enddocregion Redirection // changes on the listenable will cause the router to refresh it's route refreshListenable: loginInfo, ); } +// #docregion RouteTree @TypedGoRoute( path: '/', routes: >[ TypedGoRoute( path: 'family/:fid', +// #enddocregion RouteTree routes: >[ TypedGoRoute( path: 'person/:pid', @@ -76,21 +80,26 @@ class App extends StatelessWidget { ], ), ], +// #docregion RouteTree ), +// #enddocregion RouteTree TypedGoRoute(path: 'family-count/:count'), +// #docregion RouteTree ], ) +// #docregion DefineRoute class HomeRoute extends GoRouteData { const HomeRoute(); @override Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); } +// #enddocregion DefineRoute -@TypedGoRoute( - path: '/login', -) +// #docregion QueryParameters +@TypedGoRoute(path: '/login') class LoginRoute extends GoRouteData { +// #enddocregion RouteTree const LoginRoute({this.fromPage}); final String? fromPage; @@ -98,7 +107,10 @@ class LoginRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) => LoginScreen(from: fromPage); +// #docregion RouteTree } +// #enddocregion RouteTree +// #enddocregion QueryParameters class FamilyRoute extends GoRouteData { const FamilyRoute(this.fid); @@ -233,7 +245,9 @@ class FamilyScreen extends StatelessWidget { for (final Person p in family.people) ListTile( title: Text(p.name), + // #docregion Navigation onTap: () => PersonRoute(family.id, p.id).go(context), + // #enddocregion Navigation ), ], ), diff --git a/packages/go_router_builder/example/lib/readme_excerpts.dart b/packages/go_router_builder/example/lib/readme_excerpts.dart new file mode 100644 index 00000000000..4cb3d98a87a --- /dev/null +++ b/packages/go_router_builder/example/lib/readme_excerpts.dart @@ -0,0 +1,327 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: unused_local_variable, unused_field, public_member_api_docs + +// This file contains compiled code snippets that are extracted and written into the README, to help explain how to use the go_router_builder package. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +// #docregion Import +import 'package:go_router/go_router.dart'; + +part 'example_builder_output.g.dart'; +// #enddocregion Import + +void main() { + runApp(App()); +} + +class App extends StatelessWidget { + App({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Text('In App Purchase Examples'), + ), + ), + ); + } + + // #docregion ErrorBuilderParameter + final GoRouter _router = GoRouter( + errorBuilder: (BuildContext c, GoRouterState s) => + ErrorRoute(error: s.error!).build(c, s), +// #enddocregion ErrorBuilderParameter +// #docregion ParsedParameter + routes: [ + GoRoute( + path: '/author/:authorId', + builder: (BuildContext context, GoRouterState state) { + // require the authorId to be present and be an integer + final int authorId = int.parse(state.pathParameters['authorId']!); + return AuthorDetailsScreen(authorId: authorId); + }, + ), + ], +// #enddocregion ParsedParameter + // #docregion ErrorBuilderParameter + ); + // #enddocregion ErrorBuilderParameter +} + +class PersonRoute extends GoRouteData { + const PersonRoute({required this.pid}); + + final String pid; + + Future go(BuildContext context) async { + return 'Result from PersonRoute'; + } +} + +// #docregion ErrorBuilder +class ErrorRoute extends GoRouteData { + ErrorRoute({required this.error}); + final Exception error; + + @override + Widget build(BuildContext context, GoRouterState state) => + ErrorScreen(error: error); +} +// #enddocregion ErrorBuilder + +// #docregion DefaultValues +@TypedGoRoute(path: '/my-route') +class MyRoute extends GoRouteData { + MyRoute({this.queryParameter = 'defaultValue'}); + final String queryParameter; + + @override + Widget build(BuildContext context, GoRouterState state) => + MyScreen(queryParameter: queryParameter); +} +// #enddocregion DefaultValues + +// #docregion MixedParameters +@TypedGoRoute(path: '/:ketchup') +class HotdogRouteWithEverything extends GoRouteData { + HotdogRouteWithEverything(this.ketchup, this.mustard, this.$extra); + final bool ketchup; // required path parameter + final String? mustard; // optional query parameter + final Sauce $extra; // special $extra parameter + + @override + Widget build(BuildContext context, GoRouterState state) => + HotdogScreen(ketchup, mustard, $extra); +} +// #enddocregion MixedParameters + +class ReturnValueExample extends StatelessWidget { + const ReturnValueExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Tap Example')), + body: Center( + child: ElevatedButton( + onPressed: () => _tap(context), + child: const Text('Tap Me'), + ), + ), + ); + } + + // #docregion ReturnValue + Future _tap(BuildContext context) async { + final String result = await const PersonRoute(pid: 'p1').go(context); + } +// #enddocregion ReturnValue +} + +class RoutePathTypeErrorExample extends StatelessWidget { + const RoutePathTypeErrorExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Route Path Type Error Example')), + body: Center( + child: ElevatedButton( + // #docregion RoutePathTypeError + onPressed: () => + context.go('/author/a42'), // error: `a42` is not an `int` +// #enddocregion RoutePathTypeError + child: const Text('Tap Me'), + ), + ), + ); + } +} + +class NavigationErrorExample extends StatelessWidget { + const NavigationErrorExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Navigation Error Example')), + body: Center( + child: ElevatedButton( + // #docregion NavigationError + // error: missing required parameter 'fid' + onPressed: () => const PersonRoute(pid: 'p1').go(context), + // #enddocregion NavigationError + child: const Text('Tap Me'), + ), + ), + ); + } +} + +// #docregion RouteLevelRedirection +class HomeRoute extends GoRouteData { + // no need to implement [build] when this [redirect] is unconditional + @override + String? redirect(BuildContext context, GoRouterState state) => + BooksRoute().location; +} +// #enddocregion RouteLevelRedirection + +// #docregion TypeConversions +enum BookKind { all, popular, recent } + +@TypedGoRoute(path: '/books') +class BooksRoute extends GoRouteData { + BooksRoute({this.kind = BookKind.popular}); + + final BookKind kind; + + @override + Widget build(BuildContext context, GoRouterState state) => + BooksScreen(kind: kind); +} +// #enddocregion TypeConversions + +// #docregion CustomTransitions +class FancyRoute extends GoRouteData { + @override + CustomTransitionPage buildPage( + BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const FancyPage(), + transitionsBuilder: (BuildContext context, Animation animation, + Animation animation2, Widget child) => + RotationTransition(turns: animation, child: child), + ); +} +// #enddocregion CustomTransitions + +// #docregion TransitionOverride +class MyMaterialRoute extends GoRouteData { + @override + MaterialPage buildPage(BuildContext context, GoRouterState state) => + MaterialPage( + key: state.pageKey, + child: const MyPage(), + ); +} +// #enddocregion TransitionOverride + +class MyPage extends StatelessWidget { + const MyPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold(body: Center(child: Text('My Page'))); + } +} + +// #docregion NavigatorKey +// For ShellRoutes: +final GlobalKey shellNavigatorKey = GlobalKey(); + +class MyShellRouteData extends ShellRouteData { + const MyShellRouteData(); + + static final GlobalKey $navigatorKey = shellNavigatorKey; + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { +// #enddocregion NavigatorKey + return Container(); +// #docregion NavigatorKey + } +} + +// For GoRoutes: +final GlobalKey rootNavigatorKey = GlobalKey(); + +class MyGoRouteData extends GoRouteData { + const MyGoRouteData(); + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Widget build(BuildContext context, GoRouterState state) { + // #enddocregion NavigatorKey + return Container(); +// #docregion NavigatorKey + } +} +// #enddocregion NavigatorKey + +class ErrorScreen extends StatelessWidget { + const ErrorScreen({required this.error, super.key}); + + final Exception error; + + @override + Widget build(BuildContext context) { + return Scaffold(body: Center(child: Text('Error: $error'))); + } +} + +class AuthorDetailsScreen extends StatelessWidget { + const AuthorDetailsScreen({required this.authorId, super.key}); + + final int authorId; + + @override + Widget build(BuildContext context) { + return Scaffold(body: Center(child: Text('Author ID: $authorId'))); + } +} + +class HotdogScreen extends StatelessWidget { + const HotdogScreen(this.ketchup, this.mustard, this.$extra, {super.key}); + + final bool ketchup; + final String? mustard; + final Sauce $extra; + + @override + Widget build(BuildContext context) { + return const Scaffold(body: Center(child: Text('Hot dog screen'))); + } +} + +class Sauce {} + +class MyScreen extends StatelessWidget { + const MyScreen({required this.queryParameter, super.key}); + + final String queryParameter; + + @override + Widget build(BuildContext context) { + return Scaffold(body: Center(child: Text('Query: $queryParameter'))); + } +} + +class BooksScreen extends StatelessWidget { + const BooksScreen({required this.kind, super.key}); + + final BookKind kind; + + @override + Widget build(BuildContext context) { + return Scaffold(body: Center(child: Text('Book Kind: $kind'))); + } +} + +class FancyPage extends StatelessWidget { + const FancyPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold(body: Center(child: Text('Fancy Page'))); + } +} diff --git a/packages/go_router_builder/example/lib/simple_example.dart b/packages/go_router_builder/example/lib/simple_example.dart index dbb0a15a69c..4cd4c33ac90 100644 --- a/packages/go_router_builder/example/lib/simple_example.dart +++ b/packages/go_router_builder/example/lib/simple_example.dart @@ -22,7 +22,9 @@ class App extends StatelessWidget { title: _appTitle, ); +// #docregion Initialization final GoRouter _router = GoRouter(routes: $appRoutes); +// #enddocregion Initialization } @TypedGoRoute( diff --git a/packages/go_router_builder/example/test/readme_excerpts_test.dart b/packages/go_router_builder/example/test/readme_excerpts_test.dart new file mode 100644 index 00000000000..99a71b6963d --- /dev/null +++ b/packages/go_router_builder/example/test/readme_excerpts_test.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_builder_example/readme_excerpts.dart'; + +void main() { + testWidgets('App starts on HomeScreen', (WidgetTester tester) async { + await tester.pumpWidget(App()); + expect(find.text('In App Purchase Examples'), findsOneWidget); + }); + + testWidgets('AuthorDetailsScreen renders correctly', + (WidgetTester tester) async { + const int testAuthorId = 42; + await tester.pumpWidget(const MaterialApp( + home: AuthorDetailsScreen(authorId: testAuthorId), + )); + expect(find.text('Author ID: $testAuthorId'), findsOneWidget); + }); +} diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 0c4e2f158e3..83197ba3c69 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 2.4.0 +version: 2.4.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml index b2d445f30f8..fbca5c6e12b 100644 --- a/script/configs/temp_exclude_excerpt.yaml +++ b/script/configs/temp_exclude_excerpt.yaml @@ -8,7 +8,6 @@ - css_colors - espresso - extension_google_sign_in_as_googleapis_auth -- go_router_builder - google_sign_in/google_sign_in - image_picker_for_web - in_app_purchase/in_app_purchase