From 5e3f578800caba375e813e8c588c12c7d2036b6f Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 16 Dec 2023 10:41:00 -0800 Subject: [PATCH 1/4] [go_router_builder] Adopt code excerpts in README Remove 'Import' section --- packages/go_router_builder/CHANGELOG.md | 4 + packages/go_router_builder/README.md | 208 ++++++----- .../example/lib/extra_example.dart | 4 + .../go_router_builder/example/lib/main.dart | 14 + .../example/lib/readme_excerpts.dart | 323 ++++++++++++++++++ .../example/lib/readme_excerpts.g.dart | 136 ++++++++ .../example/lib/simple_example.dart | 2 + .../example/test/readme_excerpts_test.dart | 23 ++ packages/go_router_builder/pubspec.yaml | 2 +- script/configs/temp_exclude_excerpt.yaml | 1 - 10 files changed, 627 insertions(+), 90 deletions(-) create mode 100644 packages/go_router_builder/example/lib/readme_excerpts.dart create mode 100644 packages/go_router_builder/example/lib/readme_excerpts.g.dart create mode 100644 packages/go_router_builder/example/test/readme_excerpts_test.dart diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 153978168be4..d5aaf837a816 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 73faf973937c..54246260ccb9 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -1,34 +1,5 @@ ## Usage -### Dependencies - -To use `go_router_builder`, you need to have the following dependencies in -`pubspec.yaml`. - -```yaml -dependencies: - # ...along with your other dependencies - go_router: ^9.0.3 - -dev_dependencies: - # ...along with your other dev-dependencies - build_runner: ^2.0.0 - go_router_builder: ^2.3.0 -``` - -### Source code - -Instructions below explain how to create and annotate types to use this builder. -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'; -``` - ### Running `build_runner` To do a one-time build: @@ -50,23 +21,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: ':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 +56,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 +70,29 @@ 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 +100,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 +156,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 +167,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 +187,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 +208,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 +238,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 +248,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 +260,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 +304,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 +336,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 +352,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 +379,7 @@ different navigator. This kind of scenarios can be achieved by declaring a Example: + ```dart // For ShellRoutes: final GlobalKey shellNavigatorKey = GlobalKey(); @@ -361,11 +391,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 +405,7 @@ class MyGoRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - // ... + // ··· } } ``` diff --git a/packages/go_router_builder/example/lib/extra_example.dart b/packages/go_router_builder/example/lib/extra_example.dart index b16d6ff030c0..d1aaaddcf6c4 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 773b6687ed93..8116e298cc83 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', @@ -78,19 +82,24 @@ class App extends StatelessWidget { ], ), 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 +// #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 000000000000..76b3fc5e4c2d --- /dev/null +++ b/packages/go_router_builder/example/lib/readme_excerpts.dart @@ -0,0 +1,323 @@ +// 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 + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +part 'readme_excerpts.g.dart'; + +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/readme_excerpts.g.dart b/packages/go_router_builder/example/lib/readme_excerpts.g.dart new file mode 100644 index 000000000000..d0fa8276d45b --- /dev/null +++ b/packages/go_router_builder/example/lib/readme_excerpts.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/simple_example.dart b/packages/go_router_builder/example/lib/simple_example.dart index dbb0a15a69c9..4cd4c33ac90c 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 000000000000..99a71b6963d2 --- /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 0c4e2f158e39..83197ba3c691 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 b2d445f30f8c..fbca5c6e12b8 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 From a815c1283e98901b00b648028a88ebc380734f47 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 18 Dec 2023 14:12:40 -0800 Subject: [PATCH 2/4] Run update excerpts --- packages/go_router_builder/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index 54246260ccb9..a0354854752a 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -25,7 +25,7 @@ often the page builder must first parse the parameters into types that aren't ```dart routes: [ GoRoute( - path: ':authorId', + 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']!); @@ -416,4 +416,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`. From 5efc2092f983f2e5c5e44025be527c864be505e3 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 21 Dec 2023 16:05:40 -0800 Subject: [PATCH 3/4] Re-add 'Dependencies' and 'Import' sections to README Add comment explaining purpose of readme_excerpts.dart and readme_excerpts.yaml --- packages/go_router_builder/README.md | 31 +++++++++++++++++++ .../example/lib/readme_excerpts.dart | 4 +++ .../example/readme_excerpts.yaml | 12 +++++++ 3 files changed, 47 insertions(+) create mode 100644 packages/go_router_builder/example/readme_excerpts.yaml diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index a0354854752a..b732141d1860 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -1,5 +1,36 @@ ## Usage +### Dependencies + +To use `go_router_builder`, you need to have the following dependencies in +`pubspec.yaml`. + + +```yaml +dependencies: + # ...along with your other dependencies + go_router: ^13.0.0 + +dev_dependencies: + # ...along with your other dev-dependencies + build_runner: ^2.4.0 + go_router_builder: ^2.4.0 +``` + +### Source code + +Instructions below explain how to create and annotate types to use this builder. +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 'readme_excerpts.g.dart'; +``` + ### Running `build_runner` To do a one-time build: diff --git a/packages/go_router_builder/example/lib/readme_excerpts.dart b/packages/go_router_builder/example/lib/readme_excerpts.dart index 76b3fc5e4c2d..fd5387ead9c8 100644 --- a/packages/go_router_builder/example/lib/readme_excerpts.dart +++ b/packages/go_router_builder/example/lib/readme_excerpts.dart @@ -4,12 +4,16 @@ // 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 'readme_excerpts.g.dart'; +// #enddocregion Import void main() { runApp(App()); diff --git a/packages/go_router_builder/example/readme_excerpts.yaml b/packages/go_router_builder/example/readme_excerpts.yaml new file mode 100644 index 000000000000..f0301f0327bb --- /dev/null +++ b/packages/go_router_builder/example/readme_excerpts.yaml @@ -0,0 +1,12 @@ +# 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. + +# #docregion Dependencies +dependencies: + # ...along with your other dependencies + go_router: ^13.0.0 + +dev_dependencies: + # ...along with your other dev-dependencies + build_runner: ^2.4.0 + go_router_builder: ^2.4.0 +# #enddocregion Dependencies From b3c3e6a186a5d4e8c6a3cc0c74fec3db0faeb055 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 25 Jan 2024 14:10:26 -0800 Subject: [PATCH 4/4] Remove .yaml file and fix errors Rename 'readme_excerpts' generated file Fix formatting errors --- packages/go_router_builder/README.md | 9 +++++---- ...excerpts.g.dart => example_builder_output.g.dart} | 0 packages/go_router_builder/example/lib/main.dart | 6 +++--- .../example/lib/readme_excerpts.dart | 2 +- .../go_router_builder/example/readme_excerpts.yaml | 12 ------------ 5 files changed, 9 insertions(+), 20 deletions(-) rename packages/go_router_builder/example/lib/{readme_excerpts.g.dart => example_builder_output.g.dart} (100%) delete mode 100644 packages/go_router_builder/example/readme_excerpts.yaml diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index b732141d1860..7fec272e80ff 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -5,7 +5,6 @@ To use `go_router_builder`, you need to have the following dependencies in `pubspec.yaml`. - ```yaml dependencies: # ...along with your other dependencies @@ -28,7 +27,7 @@ generated file will always have the name `[source_file].g.dart`. ```dart import 'package:go_router/go_router.dart'; -part 'readme_excerpts.g.dart'; +part 'example_builder_output.g.dart'; ``` ### Running `build_runner` @@ -108,6 +107,8 @@ The tree of routes is defined as an attribute on each of the top-level routes: routes: >[ TypedGoRoute( path: 'family/:fid', +// ··· + ), // ··· ], ) @@ -119,7 +120,7 @@ class HomeRoute extends GoRouteData { } @TypedGoRoute( - path: '/login', + path: '/login' ) class LoginRoute extends GoRouteData { // ··· @@ -201,7 +202,7 @@ Parameters (named or positional) not listed in the path of `TypedGoRoute` indica ```dart @TypedGoRoute( - path: '/login', + path: '/login' ) class LoginRoute extends GoRouteData { const LoginRoute({this.fromPage}); diff --git a/packages/go_router_builder/example/lib/readme_excerpts.g.dart b/packages/go_router_builder/example/lib/example_builder_output.g.dart similarity index 100% rename from packages/go_router_builder/example/lib/readme_excerpts.g.dart rename to packages/go_router_builder/example/lib/example_builder_output.g.dart diff --git a/packages/go_router_builder/example/lib/main.dart b/packages/go_router_builder/example/lib/main.dart index 8116e298cc83..b422ecb6554e 100644 --- a/packages/go_router_builder/example/lib/main.dart +++ b/packages/go_router_builder/example/lib/main.dart @@ -80,7 +80,9 @@ class App extends StatelessWidget { ], ), ], +// #docregion RouteTree ), +// #enddocregion RouteTree TypedGoRoute(path: 'family-count/:count'), // #docregion RouteTree ], @@ -95,9 +97,7 @@ class HomeRoute extends GoRouteData { // #enddocregion DefineRoute // #docregion QueryParameters -@TypedGoRoute( - path: '/login', -) +@TypedGoRoute(path: '/login') class LoginRoute extends GoRouteData { // #enddocregion RouteTree const LoginRoute({this.fromPage}); diff --git a/packages/go_router_builder/example/lib/readme_excerpts.dart b/packages/go_router_builder/example/lib/readme_excerpts.dart index fd5387ead9c8..4cb3d98a87a5 100644 --- a/packages/go_router_builder/example/lib/readme_excerpts.dart +++ b/packages/go_router_builder/example/lib/readme_excerpts.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; // #docregion Import import 'package:go_router/go_router.dart'; -part 'readme_excerpts.g.dart'; +part 'example_builder_output.g.dart'; // #enddocregion Import void main() { diff --git a/packages/go_router_builder/example/readme_excerpts.yaml b/packages/go_router_builder/example/readme_excerpts.yaml deleted file mode 100644 index f0301f0327bb..000000000000 --- a/packages/go_router_builder/example/readme_excerpts.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# 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. - -# #docregion Dependencies -dependencies: - # ...along with your other dependencies - go_router: ^13.0.0 - -dev_dependencies: - # ...along with your other dev-dependencies - build_runner: ^2.4.0 - go_router_builder: ^2.4.0 -# #enddocregion Dependencies