diff --git a/.cirrus.yml b/.cirrus.yml index 3906021bf1e2..216abf49281d 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -48,7 +48,8 @@ task: always: format_script: ./script/tool_runner.sh format --fail-on-change license_script: dart pub global run flutter_plugin_tools license-check - analyze_script: ./script/tool_runner.sh analyze --custom-analysis=web_benchmarks/testing/test_app,flutter_lints/example,rfw/example + # TODO(chunhtai): Remove go_router custom-analysis https://github.com/flutter/flutter/issues/98711 + analyze_script: ./script/tool_runner.sh analyze --custom-analysis=go_router,web_benchmarks/testing/test_app,flutter_lints/example,rfw/example pubspec_script: ./script/tool_runner.sh pubspec-check - name: publishable env: diff --git a/README.md b/README.md index 53defec27cf7..8193c8924f96 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ These are the available packages in this repository. | [flutter\_image](./packages/flutter_image/) | [![pub package](https://img.shields.io/pub/v/flutter_image.svg)](https://pub.dev/packages/flutter_image) | | [flutter\_lints](./packages/flutter_lints/) | [![pub package](https://img.shields.io/pub/v/flutter_lints.svg)](https://pub.dev/packages/flutter_lints) | | [flutter\_markdown](./packages/flutter_markdown/) | [![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dev/packages/flutter_markdown) | +| [go\_router](./packages/go_router/) | [![pub package](https://img.shields.io/pub/v/go_router.svg)](https://pub.dev/packages/go_router) | | [multicast\_dns](./packages/multicast_dns/) | [![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](https://pub.dev/packages/multicast_dns) | | [palette\_generator](./packages/palette_generator/) | [![pub package](https://img.shields.io/pub/v/palette_generator.svg)](https://pub.dartlang.org/packages/palette_generator) | | [pigeon](./packages/pigeon/) | [![pub package](https://img.shields.io/pub/v/pigeon.svg)](https://pub.dev/packages/pigeon) | diff --git a/packages/go_router/.metadata b/packages/go_router/.metadata new file mode 100644 index 000000000000..af897ac430d9 --- /dev/null +++ b/packages/go_router/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: b22742018b3edf16c6cadd7b76d9db5e7f9064b5 + channel: stable + +project_type: package diff --git a/packages/go_router/AUTHORS b/packages/go_router/AUTHORS new file mode 100644 index 000000000000..fa2a9bee3bad --- /dev/null +++ b/packages/go_router/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +csells@sellsbrothers.com diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md new file mode 100644 index 000000000000..f015d5f58412 --- /dev/null +++ b/packages/go_router/CHANGELOG.md @@ -0,0 +1,454 @@ +## 3.0.2 + +- Moves source to flutter/packages. +- Removes all_lint_rules_community and path_to_regexp dependencies. + +## 3.0.1 + +- pass along the error to the `navigatorBuilder` to allow for different + implementations based on the presence of an error + +## 3.0.0 + +- breaking change: added `GoRouterState` to `navigatorBuilder` function +- breaking change: removed `BuildContext` from `GoRouter.pop()` to remove the + need to use `context` parameter when calling the `GoRouter` API; this changes + the behavior of `GoRouter.pop()` to only pop what's on the `GoRouter` page + stack and no longer calls `Navigator.pop()` +- new [Migrating to 3.0 section](https://gorouter.dev/migrating-to-30) in the + docs to describe the details of the breaking changes and how to update your + code +- added a new [shared + scaffold](https://github.com/csells/go_router/blob/main/go_router/example/lib/shared_scaffold.dart) + sample to show how to use the `navigatorBuilder` function to build a custom + shared scaffold outside of the animations provided by go_router + +## 2.5.7 + +- [PR 262](https://github.com/csells/go_router/pull/262): add support for + `Router.neglect`; thanks to [nullrocket](https://github.com/nullrocket)! +- [PR 265](https://github.com/csells/go_router/pull/265): add Japanese + translation of the docs; thanks to + [toshi-kuji](https://github.com/toshi-kuji)! Unfortunately I don't yet know + how to properly display them via docs.page, but [I'm working on + it](https://github.com/csells/go_router/issues/266) +- updated the examples using the `from` query parameter to be completely + self-contained in the `redirect` function, simplifying usage +- updated the async data example to be simpler +- added a new example to show how to implement a loading page +- renamed the navigator_integration example to user_input and added an example + of `WillPopScope` for go_router apps + +## 2.5.6 + +- [PR 259](https://github.com/csells/go_router/pull/259): remove a hack for + notifying the router of a route change that was no longer needed; thanks to + [nullrocket](https://github.com/nullrocket)! +- improved async example to handle the case that the data has been returned but + the page is no longer there by checking the `mounted` property of the screen + +## 2.5.5 + +- updated implementation to use logging package for debug diagnostics; thanks + to [johnpryan](https://github.com/johnpryan) + +## 2.5.4 + +- fixed up the `GoRouterRefreshStream` implementation with an export, an example + and some docs + +## 2.5.3 + +- added `GoRouterRefreshStream` from + [jopmiddelkamp](https://github.com/jopmiddelkamp) to easily map from a + `Stream` to a `Listenable` for use with `refreshListenable`; very useful when + combined with stream-based state management like + [flutter_bloc](https://pub.dev/packages/flutter_bloc) +- dartdocs fixups from [mehade369](https://github.com/mehade369) +- example link fixes from [ben-milanko](https://github.com/ben-milanko) + +## 2.5.2 + +- pass additional information to the `NavigatorObserver` via default args to + `MaterialPage`, etc. + +## 2.5.1 + +- [fix 205](https://github.com/csells/go_router/issues/205): hack around a + failed assertion in Flutter when using `Duration.zero` in the + `NoTransitionPage` + +## 2.5.0 + +- provide default implementation of `GoRoute.pageBuilder` to provide a simpler + way to build pages via the `GoRouter.build` method +- provide default implementation of `GoRouter.errorPageBuilder` to provide a + simpler way to build error pages via the `GoRouter.errorBuilder` method +- provide default implementation of `GoRouter.errorBuilder` to provide an error + page without the need to implement a custom error page builder +- new [Migrating to 2.5 section](https://gorouter.dev/migrating-to-25) in + the docs to show how to take advantage of the new `builder` and default error + page builder +- removed `launch.json` as VSCode-centric and unnecessary for discovery or easy + launching +- added a [new custom error screen + sample](https://github.com/csells/go_router/blob/master/example/lib/error_screen.dart) +- added a [new WidgetsApp + sample](https://github.com/csells/go_router/blob/master/example/lib/widgets_app.dart) +- added a new `NoTransitionPage` class +- updated docs to explain why the browser's Back button doesn't work + with the `extra` param +- updated README to point to new docs site: [gorouter.dev](https://gorouter.dev) + +## 2.3.1 + +- [fix 191](https://github.com/csells/go_router/issues/191): handle several + kinds of trailing / in the location, e.g. `/foo/` should be the same as `/foo` + +## 2.3.0 + +- fix a misleading error message when using redirect functions with sub-routes + +## 2.2.9 + +- [fix 182](https://github.com/csells/go_router/issues/182): fixes a regression + in the nested navigation caused by the fix for + [#163](https://github.com/csells/go_router/issues/163); thanks to + [lulupointu](https://github.com/lulupointu) for the fix! + +## 2.2.8 + +- reformatted CHANGELOG file; lets see if pub.dev is still ok with it... +- staged an in-progress doc site at https://docs.page/csells/go_router +- tightened up a test that was silently failing +- fixed a bug that dropped parent params in sub-route redirects + +## 2.2.7 + +- [fix 163](https://github.com/csells/go_router/issues/163): avoids unnecessary + page rebuilds +- [fix 139](https://github.com/csells/go_router/issues/139): avoids unnecessary + page flashes on deep linking +- [fix 158](https://github.com/csells/go_router/issues/158): shows exception + info in the debug output even during a top-level redirect coded w/ an + anonymous function, i.e. what the samples all use +- [fix 151](https://github.com/csells/go_router/issues/151): exposes + `Navigator.pop()` via `GoRouter.pop()` to make it easy to find + +## 2.2.6 + +- [fix 127](https://github.com/csells/go_router/issues/127): updated the docs + to add a video overview of the project for people that prefer that media style + over long-form text when approaching a new topic +- [fix 108](https://github.com/csells/go_router/issues/108): updated the + description of the `state` parameter to clarfy that not all properties will be + set at every usage + +## 2.2.5 + +- [fix 120 again](https://github.com/csells/go_router/issues/120): found the bug + in my tests that was masking the real bug; changed two characters to implement + the actual fix (sigh) + +## 2.2.4 + +- [fix 116](https://github.com/csells/go_router/issues/116): work-around for + auto-import of the `context.go` family of extension methods + +## 2.2.3 + +- [fix 132](https://github.com/csells/go_router/issues/132): route names are + stored as case insensitive and are now matched in a case insensitive manner + +## 2.2.2 + +- [fix 120](https://github.com/csells/go_router/issues/120): encoding and + decoding of params and query params + +## 2.2.1 + +- [fix 114](https://github.com/csells/go_router/issues/114): give a better error + message when the `GoRouter` isn't found in the widget tree via + `GoRouter.of(context)`; thanks [aoatmon](https://github.com/aoatmon) for the + [excellent bug report](https://github.com/csells/go_router/issues/114)! + +## 2.2.0 + +- added a new [`navigatorBuilder`](https://gorouter.dev/navigator-builder) argument to the + `GoRouter` constructor; thanks to [andyduke](https://github.com/andyduke)! +- also from [andyduke](https://github.com/andyduke) is an update to + improve state restoration +- refactor from [kevmoo](https://github.com/kevmoo) for easier maintenance +- added a new [Navigator Integration section of the + docs](https://gorouter.dev/navigator-integration) + +## 2.1.2 + +- [fix 61 again](https://github.com/csells/go_router/issues/61): enable images + and file links to work on pub.dev/documentation +- [fix 62](https://github.com/csells/go_router/issues/62) re-tested; fixed w/ + earlier Android system Back button fix (using navigation key) +- [fix 91](https://github.com/csells/go_router/issues/91): fix a regression w/ + the `errorPageBuilder` +- [fix 92](https://github.com/csells/go_router/issues/92): fix an edge case w/ + named sub-routes +- [fix 89](https://github.com/csells/go_router/issues/89): enable queryParams + and extra object param w/ `push` +- refactored tests for greater coverage and fewer methods `@visibleForTesting` + +## 2.1.1 + +- [fix 86](https://github.com/csells/go_router/issues/86): add `name` to + `GoRouterState` to complete support for URI-free navigation knowledge in your + code +- [fix 83](https://github.com/csells/go_router/issues/83): fix for `null` + `extra` object + +## 2.1.0 + +- [fix 80](https://github.com/csells/go_router/issues/80): adding a redirect + limit to catch too many redirects error +- [fix 81](https://github.com/csells/go_router/issues/81): allow an `extra` + object to pass through for navigation + +## 2.0.1 + +- add badges to the README and codecov to the GitHub commit action; thanks to + [rydmike](https://github.com/rydmike) for both + +## 2.0.0 + +- BREAKING CHANGE and [fix #50](https://github.com/csells/go_router/issues/50): + split `params` into `params` and `queryParams`; see the [Migrating to 2.0 + section of the docs](https://gorouter.dev/migrating-to-20) + for instructions on how to migrate your code from 1.x to 2.0 +- [fix 69](https://github.com/csells/go_router/issues/69): exposed named + location lookup for redirection +- [fix 57](https://github.com/csells/go_router/issues/57): enable the Android + system Back button to behave exactly like the `AppBar` Back button; thanks to + [SunlightBro](https://github.com/SunlightBro) for the one-line fix that I had + no idea about until he pointed it out +- [fix 59](https://github.com/csells/go_router/issues/59): add query params to + top-level redirect +- [fix 44](https://github.com/csells/go_router/issues/44): show how to use the + `AutomaticKeepAliveClientMixin` with nested navigation to keep widget state + between navigations; thanks to [rydmike](https://github.com/rydmike) for this + update + +## 1.1.3 + +- enable case-insensitive path matching while still preserving path and query + parameter cases +- change a lifetime of habit to sort constructors first as per + [sort_constructors_first](https://dart-lang.github.io/linter/lints/sort_constructors_first.html). + Thanks for the PR, [Abhishek01039](https://github.com/Abhishek01039)! +- set the initial transition example route to `/none` to make pushing the 'fade + transition' button on the first run through more fun +- fixed an error in the async data example + +## 1.1.2 + +- Thanks, Mikes! + - updated dartdocs from [rydmike](https://github.com/rydmike) + - also shoutout to [https://github.com/Salakar](https://github.com/Salakar) + for the CI action on GitHub + - this is turning into a real community effort... + +## 1.1.1 + +- now showing routing exceptions in the debug log +- updated the docs to make it clear that it will be called until it returns + `null` + +## 1.1.0 + +- added support `NavigatorObserver` objects to receive change notifications + +## 1.0.1 + +- docs updates based on user feedback for clarity +- fix for setting URL path strategy in `main()` +- fix for `push()` disables `AppBar` Back button + +## 1.0.0 + +- updated version for initial release +- some renaming for clarify and consistency with transitions + - `GoRoute.builder` => `GoRoute.pageBuilder` + - `GoRoute.error` => `GoRoute.errorPageBuilder` +- added diagnostic logging for `push` and `pushNamed` + +## 0.9.6 + +- added support for `push` as well as `go` +- added 'none' to transitions example app +- updated animation example to use no transition and added an animated gif to + the docs + +## 0.9.5 + +- added support for custom transitions between routes + +## 0.9.4 + +- updated API docs +- updated docs for `GoRouterState` + +## 0.9.3 + +- updated API docs + +## 0.9.2 + +- updated named route lookup to O(1) +- updated diagnostics output to show known named routes + +## 0.9.1 + +- updated diagnostics output to show named route lookup +- docs updates + +## 0.9.0 + +- added support for named routes + +## 0.8.8 + +- fix to make `GoRouter` notify on pop + +## 0.8.7 + +- made `GoRouter` a `ChangeNotifier` so you can listen for `location` changes + +## 0.8.6 + +- books sample bug fix + +## 0.8.5 + +- added Cupertino sample +- added example of async data lookup + +## 0.8.4 + +- added state restoration sample + +## 0.8.3 + +- changed `debugOutputFullPaths` to `debugLogDiagnostics` and added add'l + debugging logging +- parameterized redirect + +## 0.8.2 + +- updated docs for `Link` widget support + +## 0.8.1 + +- added Books sample; fixed some issues it revealed + +## 0.8.0 + +- breaking build to refactor the API for simplicity and capability +- move to fixed routing from conditional routing; simplies API, allows for + redirection at the route level and there scenario was sketchy anyway +- add redirection at the route level +- replace guard objects w/ redirect functions +- add `refresh` method and `refreshListener` +- removed `.builder` ctor from `GoRouter` (not reasonable to implement) +- add Dynamic linking section to the docs +- replaced Books sample with Nested Navigation sample +- add ability to dump the known full paths to your routes to debug output + +## 0.7.1 + +- update to pageKey to take sub-routes into account + +## 0.7.0 + +- BREAK: rename `pattern` to `path` for consistency w/ other routers in the + world +- added the `GoRouterLoginGuard` for the common redirect-to-login-page pattern + +## 0.6.2 + +- fixed issue showing home page for a second before redirecting (if needed) + +## 0.6.1 + +- added `GoRouterState.pageKey` +- removed `cupertino_icons` from main `pubspec.yaml` + +## 0.6.0 + +- refactor to support sub-routes to build a stack of pages instead of matching + multiple routes +- added unit tests for building the stack of pages +- some renaming of the types, e.g. `Four04Page` and `FamiliesPage` to + `ErrorPage` and `HomePage` respectively +- fix a redirection error shown in the debug output + +## 0.5.2 + +- add `urlPathStrategy` argument to `GoRouter` ctor + +## 0.5.1 + +- docs and description updates + +## 0.5.0 + +- moved redirect to top-level instead of per route for simplicity + +## 0.4.1 + +- fixed CHANGELOG formatting + +## 0.4.0 + +- bundled various useful route handling variables into the `GoRouterState` for + use when building pages and error pages +- updated URL Strategy section of docs to reference `flutter run` + +## 0.3.2 + +- formatting update to appease the pub.dev gods... + +## 0.3.1 + +- updated the CHANGELOG + +## 0.3.0 + +- moved redirection into a `GoRoute` ctor arg +- forgot to update the CHANGELOG + +## 0.2.3 + +- move outstanding issues to [issue + tracker](https://github.com/csells/go_router/issues) +- added explanation of Deep Linking to docs +- reformatting to meet pub.dev scoring guidelines + +## 0.2.2 + +- docs updates + +## 0.2.1 + +- messing with the CHANGELOG formatting + +## 0.2.0 + +- initial useful release +- added support for declarative routes via `GoRoute` instances +- added support for imperative routing via `GoRoute.builder` +- added support for setting the URL path strategy +- added support for conditional routing +- added support for redirection +- added support for optional query parameters as well as positional parameters + in route names + +## 0.1.0 + +- squatting on the package name (I'm not too proud to admit it) diff --git a/packages/go_router/LICENSE b/packages/go_router/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/go_router/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/go_router/README.md b/packages/go_router/README.md new file mode 100644 index 000000000000..4c66a6ea7282 --- /dev/null +++ b/packages/go_router/README.md @@ -0,0 +1,44 @@ +# Welcome to go_router! + +The purpose of [the go_router package](https://pub.dev/packages/go_router) is to +use declarative routes to reduce complexity, regardless of the platform you're +targeting (mobile, web, desktop), handle deep and dynamic linking from +Android, iOS and the web, along with a number of other navigation-related +scenarios, while still (hopefully) providing an easy-to-use developer +experience. + +You can get started with go_router with code as simple as this: + +```dart +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: 'GoRouter Example', + ); + + final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + ); +} + +class Page1Screen extends StatelessWidget {...} + +class Page2Screen extends StatelessWidget {...} +``` + +But go_router can do oh so much more! + +# See [gorouter.dev](https://gorouter.dev) for go_router docs & samples diff --git a/packages/go_router/analysis_options.yaml b/packages/go_router/analysis_options.yaml new file mode 100644 index 000000000000..53897be883cf --- /dev/null +++ b/packages/go_router/analysis_options.yaml @@ -0,0 +1,33 @@ +# This file is temporary and should be removed as a part of +# https://github.com/flutter/flutter/issues/98711. +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "test/.test_coverage.dart" + - "bin/cache/**" + - "lib/generated_plugin_registrant.dart" + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + included_file_warning: ignore + missing_required_param: error + missing_return: error + parameter_assignments: error + +linter: + rules: + prefer_double_quotes: false + unnecessary_final: false + always_specify_types: false + prefer_final_parameters: false + prefer_asserts_with_message: false + require_trailing_commas: false + avoid_classes_with_only_static_members: false + always_put_control_body_on_new_line: false + always_use_package_imports: false + avoid_annotating_with_dynamic: false + avoid_redundant_argument_values: false + one_member_abstracts: false + flutter_style_todos: false diff --git a/packages/go_router/example/.gitignore b/packages/go_router/example/.gitignore new file mode 100644 index 000000000000..0d920e614a4c --- /dev/null +++ b/packages/go_router/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/go_router/example/.metadata b/packages/go_router/example/.metadata new file mode 100644 index 000000000000..be0f63d80321 --- /dev/null +++ b/packages/go_router/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 4cc385b4b84ac2f816d939a49ea1f328c4e0b48e + channel: stable + +project_type: app diff --git a/packages/go_router/example/analysis_options.yaml b/packages/go_router/example/analysis_options.yaml new file mode 100644 index 000000000000..6cb1fba39e9e --- /dev/null +++ b/packages/go_router/example/analysis_options.yaml @@ -0,0 +1,39 @@ +# This file is temporary and should be removed as a part of +# https://github.com/flutter/flutter/issues/98711. + +include: package:all_lint_rules_community/all.yaml + +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "test/.test_coverage.dart" + - "bin/cache/**" + - "lib/generated_plugin_registrant.dart" + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + included_file_warning: ignore + missing_required_param: error + missing_return: error + parameter_assignments: error + +linter: + rules: + prefer_double_quotes: false + unnecessary_final: false + always_specify_types: false + prefer_final_parameters: false + prefer_asserts_with_message: false + require_trailing_commas: false + public_member_api_docs: false + avoid_classes_with_only_static_members: false + always_put_control_body_on_new_line: false + always_use_package_imports: false + avoid_annotating_with_dynamic: false + avoid_redundant_argument_values: false + one_member_abstracts: false + flutter_style_todos: false + diagnostic_describe_all_properties: false + library_private_types_in_public_api: false diff --git a/packages/go_router/example/android/.gitignore b/packages/go_router/example/android/.gitignore new file mode 100644 index 000000000000..6f568019d3c6 --- /dev/null +++ b/packages/go_router/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/go_router/example/android/app/build.gradle b/packages/go_router/example/android/app/build.gradle new file mode 100644 index 000000000000..b120248e50fc --- /dev/null +++ b/packages/go_router/example/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + minSdkVersion 16 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/go_router/example/android/app/src/debug/AndroidManifest.xml b/packages/go_router/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..c208884f3014 --- /dev/null +++ b/packages/go_router/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/go_router/example/android/app/src/main/AndroidManifest.xml b/packages/go_router/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..6f325b42a72d --- /dev/null +++ b/packages/go_router/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/packages/go_router/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/go_router/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 000000000000..56d56eee85f8 --- /dev/null +++ b/packages/go_router/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,10 @@ +// 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. + +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/packages/go_router/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/go_router/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/go_router/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/go_router/example/android/app/src/main/res/drawable/launch_background.xml b/packages/go_router/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/go_router/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/go_router/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/go_router/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/go_router/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/go_router/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/go_router/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/go_router/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/go_router/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/go_router/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/go_router/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/go_router/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/go_router/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/go_router/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/go_router/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/go_router/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/go_router/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/go_router/example/android/app/src/main/res/values-night/styles.xml b/packages/go_router/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..449a9f930826 --- /dev/null +++ b/packages/go_router/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/go_router/example/android/app/src/main/res/values/styles.xml b/packages/go_router/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d74aa35c2826 --- /dev/null +++ b/packages/go_router/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/go_router/example/android/app/src/profile/AndroidManifest.xml b/packages/go_router/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..c208884f3014 --- /dev/null +++ b/packages/go_router/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/go_router/example/android/build.gradle b/packages/go_router/example/android/build.gradle new file mode 100644 index 000000000000..27ef0fc4b028 --- /dev/null +++ b/packages/go_router/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + ext.kotlin_version = '1.6.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/go_router/example/android/gradle.properties b/packages/go_router/example/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/go_router/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/go_router/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/go_router/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..bc6a58afdda2 --- /dev/null +++ b/packages/go_router/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/go_router/example/android/settings.gradle b/packages/go_router/example/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/packages/go_router/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/go_router/example/ios/.gitignore b/packages/go_router/example/ios/.gitignore new file mode 100644 index 000000000000..151026b91bc9 --- /dev/null +++ b/packages/go_router/example/ios/.gitignore @@ -0,0 +1,33 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/go_router/example/ios/Flutter/AppFrameworkInfo.plist b/packages/go_router/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..8d4492f977ad --- /dev/null +++ b/packages/go_router/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/packages/go_router/example/ios/Flutter/Debug.xcconfig b/packages/go_router/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/go_router/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/go_router/example/ios/Flutter/Release.xcconfig b/packages/go_router/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/go_router/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/go_router/example/ios/Podfile b/packages/go_router/example/ios/Podfile new file mode 100644 index 000000000000..1e8c3c90a55e --- /dev/null +++ b/packages/go_router/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj b/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..bb6278d14477 --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,539 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + AA177319549D428929ABDB4C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0686CB2BA1F156C1D2447565 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0686CB2BA1F156C1D2447565 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5A1A98A53CAE3552462AEDA6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 90212C6E1492C81AAE2C3375 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DB438FC03DF109A82002E877 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AA177319549D428929ABDB4C /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + E80AFA277EC2D973F131FACC /* Pods */, + FFB63AA64ECEC712FABE7A82 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + E80AFA277EC2D973F131FACC /* Pods */ = { + isa = PBXGroup; + children = ( + DB438FC03DF109A82002E877 /* Pods-Runner.debug.xcconfig */, + 5A1A98A53CAE3552462AEDA6 /* Pods-Runner.release.xcconfig */, + 90212C6E1492C81AAE2C3375 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + FFB63AA64ECEC712FABE7A82 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0686CB2BA1F156C1D2447565 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 562FBE9BACC960D39E748F43 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E0381E59351462E7DB47E74E /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 562FBE9BACC960D39E748F43 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + E0381E59351462E7DB47E74E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..a28140cfdb3f --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/go_router/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/go_router/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/go_router/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/go_router/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/go_router/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/go_router/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/go_router/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/go_router/example/ios/Runner/AppDelegate.swift b/packages/go_router/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..caf998393333 --- /dev/null +++ b/packages/go_router/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// 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 UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..dc9ada4725e9 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/go_router/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/go_router/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/go_router/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/go_router/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/go_router/example/ios/Runner/Info.plist b/packages/go_router/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a060db61e461 --- /dev/null +++ b/packages/go_router/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/go_router/example/ios/Runner/Runner-Bridging-Header.h b/packages/go_router/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..eb7e8ba8052f --- /dev/null +++ b/packages/go_router/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,5 @@ +// 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 "GeneratedPluginRegistrant.h" diff --git a/packages/go_router/example/lib/async_data.dart b/packages/go_router/example/lib/async_data.dart new file mode 100644 index 000000000000..1ab14c278362 --- /dev/null +++ b/packages/go_router/example/lib/async_data.dart @@ -0,0 +1,265 @@ +// 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: use_late_for_private_fields_and_variables + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Async Data'; + static final repo = Repository(); + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (context, state) => FamilyScreen( + fid: state.params['fid']!, + ), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) => PersonScreen( + fid: state.params['fid']!, + pid: state.params['pid']!, + ), + ), + ], + ), + ], + ), + ], + ); +} + +class HomeScreen extends StatefulWidget { + const HomeScreen({Key? key}) : super(key: key); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + Future>? _future; + + @override + void initState() { + super.initState(); + _fetch(); + } + + @override + void didUpdateWidget(covariant HomeScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + // refresh cached data + _fetch(); + } + + void _fetch() => _future = App.repo.getFamilies(); + + @override + Widget build(BuildContext context) => FutureBuilder>( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Scaffold( + appBar: AppBar(title: const Text('${App.title}: Loading...')), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (snapshot.hasError) { + return Scaffold( + appBar: AppBar(title: const Text('${App.title}: Error')), + body: SnapshotError(snapshot.error!), + ); + } + + assert(snapshot.hasData); + final families = snapshot.data!; + return Scaffold( + appBar: AppBar( + title: Text('${App.title}: ${families.length} families'), + ), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.go('/family/${f.id}'), + ) + ], + ), + ); + }, + ); +} + +class FamilyScreen extends StatefulWidget { + const FamilyScreen({required this.fid, Key? key}) : super(key: key); + final String fid; + + @override + State createState() => _FamilyScreenState(); +} + +class _FamilyScreenState extends State { + Future? _future; + + @override + void initState() { + super.initState(); + _fetch(); + } + + @override + void didUpdateWidget(covariant FamilyScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + // refresh cached data + if (oldWidget.fid != widget.fid) _fetch(); + } + + void _fetch() => _future = App.repo.getFamily(widget.fid); + + @override + Widget build(BuildContext context) => FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Scaffold( + appBar: AppBar(title: const Text('Loading...')), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (snapshot.hasError) { + return Scaffold( + appBar: AppBar(title: const Text('Error')), + body: SnapshotError(snapshot.error!), + ); + } + + assert(snapshot.hasData); + final family = snapshot.data!; + return Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go( + '/family/${family.id}/person/${p.id}', + ), + ), + ], + ), + ); + }, + ); +} + +class PersonScreen extends StatefulWidget { + const PersonScreen({required this.fid, required this.pid, Key? key}) + : super(key: key); + + final String fid; + final String pid; + + @override + State createState() => _PersonScreenState(); +} + +class _PersonScreenState extends State { + Future? _future; + + @override + void initState() { + super.initState(); + _fetch(); + } + + @override + void didUpdateWidget(covariant PersonScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + // refresh cached data + if (oldWidget.fid != widget.fid || oldWidget.pid != widget.pid) _fetch(); + } + + void _fetch() => _future = App.repo.getPerson(widget.fid, widget.pid); + + @override + Widget build(BuildContext context) => FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Scaffold( + appBar: AppBar(title: const Text('Loading...')), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (snapshot.hasError) { + return Scaffold( + appBar: AppBar(title: const Text('Error')), + body: SnapshotError(snapshot.error!), + ); + } + + assert(snapshot.hasData); + final famper = snapshot.data!; + return Scaffold( + appBar: AppBar(title: Text(famper.person.name)), + body: Text( + '${famper.person.name} ${famper.family.name} is ' + '${famper.person.age} years old', + ), + ); + }, + ); +} + +class SnapshotError extends StatelessWidget { + SnapshotError(Object error, {Key? key}) + : error = error is Exception ? error : Exception(error), + super(key: key); + final Exception error; + + @override + Widget build(BuildContext context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText(error.toString()), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ); +} diff --git a/packages/go_router/example/lib/books/main.dart b/packages/go_router/example/lib/books/main.dart new file mode 100644 index 000000000000..f5af9095727e --- /dev/null +++ b/packages/go_router/example/lib/books/main.dart @@ -0,0 +1,158 @@ +// 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:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'src/auth.dart'; +import 'src/data/library.dart'; +import 'src/screens/author_details.dart'; +import 'src/screens/authors.dart'; +import 'src/screens/book_details.dart'; +import 'src/screens/books.dart'; +import 'src/screens/scaffold.dart'; +import 'src/screens/settings.dart'; +import 'src/screens/sign_in.dart'; + +void main() => runApp(Bookstore()); + +class Bookstore extends StatelessWidget { + Bookstore({Key? key}) : super(key: key); + + final _scaffoldKey = const ValueKey('App scaffold'); + + @override + Widget build(BuildContext context) => BookstoreAuthScope( + notifier: _auth, + child: MaterialApp.router( + routerDelegate: _router.routerDelegate, + routeInformationParser: _router.routeInformationParser, + ), + ); + + final _auth = BookstoreAuth(); + + late final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_) => '/books', + ), + GoRoute( + path: '/signin', + pageBuilder: (context, state) => FadeTransitionPage( + key: state.pageKey, + child: SignInScreen( + onSignIn: (credentials) { + BookstoreAuthScope.of(context) + .signIn(credentials.username, credentials.password); + }, + ), + ), + ), + GoRoute( + path: '/books', + redirect: (_) => '/books/popular', + ), + GoRoute( + path: '/book/:bookId', + redirect: (state) => '/books/all/${state.params['bookId']}', + ), + GoRoute( + path: '/books/:kind(new|all|popular)', + pageBuilder: (context, state) => FadeTransitionPage( + key: _scaffoldKey, + child: BookstoreScaffold( + selectedTab: ScaffoldTab.books, + child: BooksScreen(state.params['kind']!), + ), + ), + routes: [ + GoRoute( + path: ':bookId', + builder: (context, state) { + final bookId = state.params['bookId']!; + final selectedBook = libraryInstance.allBooks + .firstWhereOrNull((b) => b.id.toString() == bookId); + + return BookDetailsScreen(book: selectedBook); + }, + ), + ], + ), + GoRoute( + path: '/author/:authorId', + redirect: (state) => '/authors/${state.params['authorId']}', + ), + GoRoute( + path: '/authors', + pageBuilder: (context, state) => FadeTransitionPage( + key: _scaffoldKey, + child: const BookstoreScaffold( + selectedTab: ScaffoldTab.authors, + child: AuthorsScreen(), + ), + ), + routes: [ + GoRoute( + path: ':authorId', + builder: (context, state) { + final authorId = int.parse(state.params['authorId']!); + final selectedAuthor = libraryInstance.allAuthors + .firstWhereOrNull((a) => a.id == authorId); + + return AuthorDetailsScreen(author: selectedAuthor); + }, + ), + ], + ), + GoRoute( + path: '/settings', + pageBuilder: (context, state) => FadeTransitionPage( + key: _scaffoldKey, + child: const BookstoreScaffold( + selectedTab: ScaffoldTab.settings, + child: SettingsScreen(), + ), + ), + ), + ], + redirect: _guard, + refreshListenable: _auth, + debugLogDiagnostics: true, + ); + + String? _guard(GoRouterState state) { + final signedIn = _auth.signedIn; + final signingIn = state.subloc == '/signin'; + + // Go to /signin if the user is not signed in + if (!signedIn && !signingIn) { + return '/signin'; + } + // Go to /books if the user is signed in and tries to go to /signin. + else if (signedIn && signingIn) { + return '/books'; + } + + // no redirect + return null; + } +} + +class FadeTransitionPage extends CustomTransitionPage { + FadeTransitionPage({ + required LocalKey key, + required Widget child, + }) : super( + key: key, + transitionsBuilder: (c, animation, a2, child) => FadeTransition( + opacity: animation.drive(_curveTween), + child: child, + ), + child: child); + + static final _curveTween = CurveTween(curve: Curves.easeIn); +} diff --git a/packages/go_router/example/lib/books/src/auth.dart b/packages/go_router/example/lib/books/src/auth.dart new file mode 100644 index 000000000000..f8481499c573 --- /dev/null +++ b/packages/go_router/example/lib/books/src/auth.dart @@ -0,0 +1,40 @@ +// 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/widgets.dart'; + +/// A mock authentication service +class BookstoreAuth extends ChangeNotifier { + bool _signedIn = false; + + bool get signedIn => _signedIn; + + Future signOut() async { + await Future.delayed(const Duration(milliseconds: 200)); + // Sign out. + _signedIn = false; + notifyListeners(); + } + + Future signIn(String username, String password) async { + await Future.delayed(const Duration(milliseconds: 200)); + + // Sign in. Allow any password. + _signedIn = true; + notifyListeners(); + return _signedIn; + } +} + +class BookstoreAuthScope extends InheritedNotifier { + const BookstoreAuthScope({ + required BookstoreAuth notifier, + required Widget child, + Key? key, + }) : super(key: key, notifier: notifier, child: child); + + static BookstoreAuth of(BuildContext context) => context + .dependOnInheritedWidgetOfExactType()! + .notifier!; +} diff --git a/packages/go_router/example/lib/books/src/data.dart b/packages/go_router/example/lib/books/src/data.dart new file mode 100644 index 000000000000..109082e3d5a6 --- /dev/null +++ b/packages/go_router/example/lib/books/src/data.dart @@ -0,0 +1,7 @@ +// 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. + +export 'data/author.dart'; +export 'data/book.dart'; +export 'data/library.dart'; diff --git a/packages/go_router/example/lib/books/src/data/author.dart b/packages/go_router/example/lib/books/src/data/author.dart new file mode 100644 index 000000000000..51138eafa792 --- /dev/null +++ b/packages/go_router/example/lib/books/src/data/author.dart @@ -0,0 +1,16 @@ +// 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 'book.dart'; + +class Author { + Author({ + required this.id, + required this.name, + }); + + final int id; + final String name; + final List books = []; +} diff --git a/packages/go_router/example/lib/books/src/data/book.dart b/packages/go_router/example/lib/books/src/data/book.dart new file mode 100644 index 000000000000..036bbaaf8c39 --- /dev/null +++ b/packages/go_router/example/lib/books/src/data/book.dart @@ -0,0 +1,21 @@ +// 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 'author.dart'; + +class Book { + Book({ + required this.id, + required this.title, + required this.isPopular, + required this.isNew, + required this.author, + }); + + final int id; + final String title; + final Author author; + final bool isPopular; + final bool isNew; +} diff --git a/packages/go_router/example/lib/books/src/data/library.dart b/packages/go_router/example/lib/books/src/data/library.dart new file mode 100644 index 000000000000..d58e5e2b491d --- /dev/null +++ b/packages/go_router/example/lib/books/src/data/library.dart @@ -0,0 +1,68 @@ +// 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 'author.dart'; +import 'book.dart'; + +final libraryInstance = Library() + ..addBook( + title: 'Left Hand of Darkness', + authorName: 'Ursula K. Le Guin', + isPopular: true, + isNew: true) + ..addBook( + title: 'Too Like the Lightning', + authorName: 'Ada Palmer', + isPopular: false, + isNew: true) + ..addBook( + title: 'Kindred', + authorName: 'Octavia E. Butler', + isPopular: true, + isNew: false) + ..addBook( + title: 'The Lathe of Heaven', + authorName: 'Ursula K. Le Guin', + isPopular: false, + isNew: false); + +class Library { + final List allBooks = []; + final List allAuthors = []; + + void addBook({ + required String title, + required String authorName, + required bool isPopular, + required bool isNew, + }) { + final author = allAuthors.firstWhere( + (author) => author.name == authorName, + orElse: () { + final value = Author(id: allAuthors.length, name: authorName); + allAuthors.add(value); + return value; + }, + ); + + final book = Book( + id: allBooks.length, + title: title, + isPopular: isPopular, + isNew: isNew, + author: author, + ); + + author.books.add(book); + allBooks.add(book); + } + + List get popularBooks => [ + ...allBooks.where((book) => book.isPopular), + ]; + + List get newBooks => [ + ...allBooks.where((book) => book.isNew), + ]; +} diff --git a/packages/go_router/example/lib/books/src/screens/author_details.dart b/packages/go_router/example/lib/books/src/screens/author_details.dart new file mode 100644 index 000000000000..0cde5a20cc5f --- /dev/null +++ b/packages/go_router/example/lib/books/src/screens/author_details.dart @@ -0,0 +1,46 @@ +// 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:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/book_list.dart'; + +class AuthorDetailsScreen extends StatelessWidget { + const AuthorDetailsScreen({ + required this.author, + Key? key, + }) : super(key: key); + + final Author? author; + + @override + Widget build(BuildContext context) { + if (author == null) { + return const Scaffold( + body: Center( + child: Text('No author found.'), + ), + ); + } + return Scaffold( + appBar: AppBar( + title: Text(author!.name), + ), + body: Center( + child: Column( + children: [ + Expanded( + child: BookList( + books: author!.books, + onTap: (book) => context.go('/book/${book.id}'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/go_router/example/lib/books/src/screens/authors.dart b/packages/go_router/example/lib/books/src/screens/authors.dart new file mode 100644 index 000000000000..97269962a1f3 --- /dev/null +++ b/packages/go_router/example/lib/books/src/screens/authors.dart @@ -0,0 +1,28 @@ +// 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:go_router/go_router.dart'; + +import '../data/library.dart'; +import '../widgets/author_list.dart'; + +class AuthorsScreen extends StatelessWidget { + const AuthorsScreen({Key? key}) : super(key: key); + + static const title = 'Authors'; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text(title), + ), + body: AuthorList( + authors: libraryInstance.allAuthors, + onTap: (author) { + context.go('/author/${author.id}'); + }, + ), + ); +} diff --git a/packages/go_router/example/lib/books/src/screens/book_details.dart b/packages/go_router/example/lib/books/src/screens/book_details.dart new file mode 100644 index 000000000000..0bdce993f7fd --- /dev/null +++ b/packages/go_router/example/lib/books/src/screens/book_details.dart @@ -0,0 +1,73 @@ +// 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:go_router/go_router.dart'; +import 'package:url_launcher/link.dart'; + +import '../data.dart'; +import 'author_details.dart'; + +class BookDetailsScreen extends StatelessWidget { + const BookDetailsScreen({ + Key? key, + this.book, + }) : super(key: key); + + final Book? book; + + @override + Widget build(BuildContext context) { + if (book == null) { + return const Scaffold( + body: Center( + child: Text('No book found.'), + ), + ); + } + return Scaffold( + appBar: AppBar( + title: Text(book!.title), + ), + body: Center( + child: Column( + children: [ + Text( + book!.title, + style: Theme.of(context).textTheme.headline4, + ), + Text( + book!.author.name, + style: Theme.of(context).textTheme.subtitle1, + ), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + AuthorDetailsScreen(author: book!.author), + ), + ); + }, + child: const Text('View author (navigator.push)'), + ), + Link( + uri: Uri.parse('/author/${book!.author.id}'), + builder: (context, followLink) => TextButton( + onPressed: followLink, + child: const Text('View author (Link)'), + ), + ), + TextButton( + onPressed: () { + context.push('/author/${book!.author.id}'); + }, + child: const Text('View author (GoRouter.push)'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/go_router/example/lib/books/src/screens/books.dart b/packages/go_router/example/lib/books/src/screens/books.dart new file mode 100644 index 000000000000..156d1403b698 --- /dev/null +++ b/packages/go_router/example/lib/books/src/screens/books.dart @@ -0,0 +1,115 @@ +// 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:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/book_list.dart'; + +class BooksScreen extends StatefulWidget { + const BooksScreen(this.kind, {Key? key}) : super(key: key); + + final String kind; + + @override + _BooksScreenState createState() => _BooksScreenState(); +} + +class _BooksScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + void didUpdateWidget(BooksScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + switch (widget.kind) { + case 'popular': + _tabController.index = 0; + break; + + case 'new': + _tabController.index = 1; + break; + + case 'all': + _tabController.index = 2; + break; + } + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Books'), + bottom: TabBar( + controller: _tabController, + onTap: _handleTabTapped, + tabs: const [ + Tab( + text: 'Popular', + icon: Icon(Icons.people), + ), + Tab( + text: 'New', + icon: Icon(Icons.new_releases), + ), + Tab( + text: 'All', + icon: Icon(Icons.list), + ), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + BookList( + books: libraryInstance.popularBooks, + onTap: _handleBookTapped, + ), + BookList( + books: libraryInstance.newBooks, + onTap: _handleBookTapped, + ), + BookList( + books: libraryInstance.allBooks, + onTap: _handleBookTapped, + ), + ], + ), + ); + + void _handleBookTapped(Book book) { + context.go('/book/${book.id}'); + } + + void _handleTabTapped(int index) { + switch (index) { + case 1: + context.go('/books/new'); + break; + case 2: + context.go('/books/all'); + break; + case 0: + default: + context.go('/books/popular'); + break; + } + } +} diff --git a/packages/go_router/example/lib/books/src/screens/scaffold.dart b/packages/go_router/example/lib/books/src/screens/scaffold.dart new file mode 100644 index 000000000000..ce67e377efef --- /dev/null +++ b/packages/go_router/example/lib/books/src/screens/scaffold.dart @@ -0,0 +1,56 @@ +// 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:adaptive_navigation/adaptive_navigation.dart'; +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +enum ScaffoldTab { books, authors, settings } + +class BookstoreScaffold extends StatelessWidget { + const BookstoreScaffold({ + required this.selectedTab, + required this.child, + Key? key, + }) : super(key: key); + + final ScaffoldTab selectedTab; + final Widget child; + + @override + Widget build(BuildContext context) => Scaffold( + body: AdaptiveNavigationScaffold( + selectedIndex: selectedTab.index, + body: child, + onDestinationSelected: (idx) { + switch (ScaffoldTab.values[idx]) { + case ScaffoldTab.books: + context.go('/books'); + break; + case ScaffoldTab.authors: + context.go('/authors'); + break; + case ScaffoldTab.settings: + context.go('/settings'); + break; + } + }, + destinations: const [ + AdaptiveScaffoldDestination( + title: 'Books', + icon: Icons.book, + ), + AdaptiveScaffoldDestination( + title: 'Authors', + icon: Icons.person, + ), + AdaptiveScaffoldDestination( + title: 'Settings', + icon: Icons.settings, + ), + ], + ), + ); +} diff --git a/packages/go_router/example/lib/books/src/screens/settings.dart b/packages/go_router/example/lib/books/src/screens/settings.dart new file mode 100644 index 000000000000..d6dcced93b2d --- /dev/null +++ b/packages/go_router/example/lib/books/src/screens/settings.dart @@ -0,0 +1,95 @@ +// 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:go_router/go_router.dart'; +import 'package:url_launcher/link.dart'; + +import '../auth.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({Key? key}) : super(key: key); + + @override + _SettingsScreenState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + @override + Widget build(BuildContext context) => Scaffold( + body: SafeArea( + child: SingleChildScrollView( + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: const Card( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12), + child: SettingsContent(), + ), + ), + ), + ), + ), + ), + ); +} + +class SettingsContent extends StatelessWidget { + const SettingsContent({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Column( + children: [ + ...[ + Text( + 'Settings', + style: Theme.of(context).textTheme.headline4, + ), + ElevatedButton( + onPressed: () { + BookstoreAuthScope.of(context).signOut(); + }, + child: const Text('Sign out'), + ), + Link( + uri: Uri.parse('/book/0'), + builder: (context, followLink) => TextButton( + onPressed: followLink, + child: const Text('Go directly to /book/0 (Link)'), + ), + ), + TextButton( + onPressed: () { + context.go('/book/0'); + }, + child: const Text('Go directly to /book/0 (GoRouter)'), + ), + ].map((w) => Padding(padding: const EdgeInsets.all(8), child: w)), + TextButton( + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Alert!'), + content: const Text('The alert description goes here.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ), + child: const Text('Show Dialog'), + ) + ], + ); +} diff --git a/packages/go_router/example/lib/books/src/screens/sign_in.dart b/packages/go_router/example/lib/books/src/screens/sign_in.dart new file mode 100644 index 000000000000..56b478b76e36 --- /dev/null +++ b/packages/go_router/example/lib/books/src/screens/sign_in.dart @@ -0,0 +1,68 @@ +// 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'; + +class Credentials { + Credentials(this.username, this.password); + + final String username; + final String password; +} + +class SignInScreen extends StatefulWidget { + const SignInScreen({ + required this.onSignIn, + Key? key, + }) : super(key: key); + + final ValueChanged onSignIn; + + @override + _SignInScreenState createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + + @override + Widget build(BuildContext context) => Scaffold( + body: Center( + child: Card( + child: Container( + constraints: BoxConstraints.loose(const Size(600, 600)), + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text('Sign in', style: Theme.of(context).textTheme.headline4), + TextField( + decoration: const InputDecoration(labelText: 'Username'), + controller: _usernameController, + ), + TextField( + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + controller: _passwordController, + ), + Padding( + padding: const EdgeInsets.all(16), + child: TextButton( + onPressed: () async { + widget.onSignIn(Credentials( + _usernameController.value.text, + _passwordController.value.text)); + }, + child: const Text('Sign in'), + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/packages/go_router/example/lib/books/src/widgets/author_list.dart b/packages/go_router/example/lib/books/src/widgets/author_list.dart new file mode 100644 index 000000000000..91dd16374ecf --- /dev/null +++ b/packages/go_router/example/lib/books/src/widgets/author_list.dart @@ -0,0 +1,32 @@ +// 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 '../data.dart'; + +class AuthorList extends StatelessWidget { + const AuthorList({ + required this.authors, + this.onTap, + Key? key, + }) : super(key: key); + + final List authors; + final ValueChanged? onTap; + + @override + Widget build(BuildContext context) => ListView.builder( + itemCount: authors.length, + itemBuilder: (context, index) => ListTile( + title: Text( + authors[index].name, + ), + subtitle: Text( + '${authors[index].books.length} books', + ), + onTap: onTap != null ? () => onTap!(authors[index]) : null, + ), + ); +} diff --git a/packages/go_router/example/lib/books/src/widgets/book_list.dart b/packages/go_router/example/lib/books/src/widgets/book_list.dart new file mode 100644 index 000000000000..7f0720689197 --- /dev/null +++ b/packages/go_router/example/lib/books/src/widgets/book_list.dart @@ -0,0 +1,32 @@ +// 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 '../data.dart'; + +class BookList extends StatelessWidget { + const BookList({ + required this.books, + this.onTap, + Key? key, + }) : super(key: key); + + final List books; + final ValueChanged? onTap; + + @override + Widget build(BuildContext context) => ListView.builder( + itemCount: books.length, + itemBuilder: (context, index) => ListTile( + title: Text( + books[index].title, + ), + subtitle: Text( + books[index].author.name, + ), + onTap: onTap != null ? () => onTap!(books[index]) : null, + ), + ); +} diff --git a/packages/go_router/example/lib/cupertino.dart b/packages/go_router/example/lib/cupertino.dart new file mode 100644 index 000000000000..77fd28d33884 --- /dev/null +++ b/packages/go_router/example/lib/cupertino.dart @@ -0,0 +1,74 @@ +// 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/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Cupertino App'; + + @override + Widget build(BuildContext context) => CupertinoApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(middle: Text(App.title)), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(middle: Text(App.title)), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/error_screen.dart b/packages/go_router/example/lib/error_screen.dart new file mode 100644 index 000000000000..b872fedc5c50 --- /dev/null +++ b/packages/go_router/example/lib/error_screen.dart @@ -0,0 +1,97 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Custom Error Screen'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + errorBuilder: (context, state) => ErrorScreen(state.error!), + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} + +class ErrorScreen extends StatelessWidget { + const ErrorScreen(this.error, {Key? key}) : super(key: key); + final Exception error; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('My "Page Not Found" Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText(error.toString()), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/extra_param.dart b/packages/go_router/example/lib/extra_param.dart new file mode 100644 index 000000000000..660a49bf1549 --- /dev/null +++ b/packages/go_router/example/lib/extra_param.dart @@ -0,0 +1,137 @@ +// 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Extra Parameter'; + static const alertOnWeb = true; + + @override + Widget build(BuildContext context) => alertOnWeb && kIsWeb + ? const MaterialApp( + title: title, + home: NoExtraParamOnWebScreen(), + ) + : MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (context, state) => HomeScreen(families: Families.data), + routes: [ + GoRoute( + name: 'family', + path: 'family', + builder: (context, state) { + final params = state.extra! as Map; + final family = params['family']! as Family; + return FamilyScreen(family: family); + }, + routes: [ + GoRoute( + name: 'person', + path: 'person', + builder: (context, state) { + final params = state.extra! as Map; + final family = params['family']! as Family; + final person = params['person']! as Person; + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + ), + ], + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({required this.families, Key? key}) : super(key: key); + final List families; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.goNamed('family', extra: {'family': f}), + ) + ], + ), + ); +} + +class FamilyScreen extends StatelessWidget { + const FamilyScreen({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go( + context.namedLocation('person'), + extra: {'family': family, 'person': p}, + ), + ), + ], + ), + ); +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} + +class NoExtraParamOnWebScreen extends StatelessWidget { + const NoExtraParamOnWebScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text("The `extra` param doesn't mix with the web:"), + Text("There's no support for the brower's Back button or" + ' deep linking'), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/init_loc.dart b/packages/go_router/example/lib/init_loc.dart new file mode 100644 index 000000000000..07442ff384f3 --- /dev/null +++ b/packages/go_router/example/lib/init_loc.dart @@ -0,0 +1,99 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Initial Location'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + initialLocation: '/page3', + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + GoRoute( + path: '/page3', + builder: (context, state) => const Page3Screen(), + ), + ], + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} + +class Page3Screen extends StatelessWidget { + const Page3Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/loading_page.dart b/packages/go_router/example/lib/loading_page.dart new file mode 100644 index 000000000000..d6d4bb708795 --- /dev/null +++ b/packages/go_router/example/lib/loading_page.dart @@ -0,0 +1,381 @@ +// 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:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class AppState extends ChangeNotifier { + AppState() { + loginInfo.addListener(loginChange); + repo.addListener(notifyListeners); + } + + final loginInfo = LoginInfo2(); + final repo = ValueNotifier(null); + + Future loginChange() async { + notifyListeners(); + + // this will call notifyListeners(), too + repo.value = + loginInfo.loggedIn ? await Repository2.get(loginInfo.userName) : null; + } + + @override + void dispose() { + loginInfo.removeListener(loginChange); + repo.removeListener(notifyListeners); + super.dispose(); + } +} + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Loading Page'; + final appState = AppState(); + + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: appState, + child: MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + debugShowCheckedModeBanner: false, + ), + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + GoRoute( + path: '/loading', + builder: (context, state) => const LoadingScreen(), + ), + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (context, state) => FamilyScreen( + fid: state.params['fid']!, + ), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) => PersonScreen( + fid: state.params['fid']!, + pid: state.params['pid']!, + ), + ), + ], + ), + ], + ), + ], + redirect: (state) { + // if the user is not logged in, they need to login + final loggedIn = appState.loginInfo.loggedIn; + final loggingIn = state.subloc == '/login'; + final subloc = state.subloc; + final fromp1 = subloc == '/' ? '' : '?from=$subloc'; + if (!loggedIn) return loggingIn ? null : '/login$fromp1'; + + // if the user is logged in but the repository is not loaded, they need to + // wait while it's loaded + final loaded = appState.repo.value != null; + final loading = state.subloc == '/loading'; + final from = state.queryParams['from']; + final fromp2 = from == null ? '' : '?from=$from'; + if (!loaded) return loading ? null : '/loading$fromp2'; + + // if the user is logged in and the repository is loaded, send them where + // they were going before (or home if they weren't going anywhere) + if (loggingIn || loading) return from ?? '/'; + + // no need to redirect at all + return null; + }, + refreshListenable: appState, + navigatorBuilder: (context, state, child) => + appState.loginInfo.loggedIn ? AuthOverlay(child: child) : child, + ); +} + +class AuthOverlay extends StatelessWidget { + const AuthOverlay({ + required this.child, + Key? key, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) => Stack( + children: [ + child, + Positioned( + top: 90, + right: 4, + child: ElevatedButton( + onPressed: () async { + // ignore: unawaited_futures + context.read().loginInfo.logout(); + // ignore: use_build_context_synchronously + context.go('/'); // clear query parameters + }, + child: const Icon(Icons.logout), + ), + ), + ], + ); +} + +class LoginScreen extends StatefulWidget { + const LoginScreen({Key? key}) : super(key: key); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + // ignore: unawaited_futures + context.read().loginInfo.login('test-user'); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} + +class LoadingScreen extends StatelessWidget { + const LoadingScreen({this.from, Key? key}) : super(key: key); + final String? from; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + CircularProgressIndicator(), + Text('loading repository...'), + ], + ), + ), + ); +} + +class HomeScreen extends StatefulWidget { + const HomeScreen({Key? key}) : super(key: key); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + Future>? _future; + + @override + void initState() { + super.initState(); + _fetch(); + } + + @override + void didUpdateWidget(covariant HomeScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + // refresh cached data + _fetch(); + } + + void _fetch() => _future = _repo.getFamilies(); + Repository2 get _repo => context.read().repo.value!; + + @override + Widget build(BuildContext context) => MyFutureBuilder>( + future: _future, + builder: (context, families) => Scaffold( + appBar: AppBar( + title: Text('${App.title}: ${families.length} families'), + ), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.go('/family/${f.id}'), + ) + ], + ), + ), + ); +} + +class FamilyScreen extends StatefulWidget { + const FamilyScreen({required this.fid, Key? key}) : super(key: key); + final String fid; + + @override + State createState() => _FamilyScreenState(); +} + +class _FamilyScreenState extends State { + Future? _future; + + @override + void initState() { + super.initState(); + _fetch(); + } + + @override + void didUpdateWidget(covariant FamilyScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + // refresh cached data + if (oldWidget.fid != widget.fid) _fetch(); + } + + void _fetch() => _future = _repo.getFamily(widget.fid); + Repository2 get _repo => context.read().repo.value!; + + @override + Widget build(BuildContext context) => MyFutureBuilder( + future: _future, + builder: (context, family) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go( + '/family/${family.id}/person/${p.id}', + ), + ), + ], + ), + ), + ); +} + +class PersonScreen extends StatefulWidget { + const PersonScreen({required this.fid, required this.pid, Key? key}) + : super(key: key); + + final String fid; + final String pid; + + @override + State createState() => _PersonScreenState(); +} + +class _PersonScreenState extends State { + Future? _future; + + @override + void initState() { + super.initState(); + _fetch(); + } + + @override + void didUpdateWidget(covariant PersonScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + // refresh cached data + if (oldWidget.fid != widget.fid || oldWidget.pid != widget.pid) _fetch(); + } + + void _fetch() => _future = _repo.getPerson(widget.fid, widget.pid); + Repository2 get _repo => context.read().repo.value!; + + @override + Widget build(BuildContext context) => MyFutureBuilder( + future: _future, + builder: (context, famper) => Scaffold( + appBar: AppBar(title: Text(famper.person.name)), + body: Text( + '${famper.person.name} ${famper.family.name} is ' + '${famper.person.age} years old', + ), + ), + ); +} + +class MyFutureBuilder extends StatelessWidget { + const MyFutureBuilder({required this.future, required this.builder, Key? key}) + : super(key: key); + + final Future? future; + final Widget Function(BuildContext context, T data) builder; + + @override + Widget build(BuildContext context) => FutureBuilder( + future: future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Scaffold( + appBar: AppBar(title: const Text('Loading...')), + body: const Center(child: CircularProgressIndicator()), + ); + } + + if (snapshot.hasError) { + return Scaffold( + appBar: AppBar(title: const Text('Error')), + body: SnapshotError(snapshot.error!), + ); + } + + assert(snapshot.hasData); + return builder(context, snapshot.data!); + }, + ); +} + +class SnapshotError extends StatelessWidget { + SnapshotError(Object error, {Key? key}) + : error = error is Exception ? error : Exception(error), + super(key: key); + final Exception error; + + @override + Widget build(BuildContext context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText(error.toString()), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ); +} diff --git a/packages/go_router/example/lib/main.dart b/packages/go_router/example/lib/main.dart new file mode 100644 index 000000000000..bf2c5d44f7a8 --- /dev/null +++ b/packages/go_router/example/lib/main.dart @@ -0,0 +1,74 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Declarative Routes'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/named_routes.dart b/packages/go_router/example/lib/named_routes.dart new file mode 100644 index 000000000000..34bc16ac5287 --- /dev/null +++ b/packages/go_router/example/lib/named_routes.dart @@ -0,0 +1,187 @@ +// 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:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + final loginInfo = LoginInfo(); + static const title = 'GoRouter Example: Named Routes'; + + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: loginInfo, + child: MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + debugShowCheckedModeBanner: false, + ), + ); + + late final _router = GoRouter( + debugLogDiagnostics: true, + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (context, state) => HomeScreen(families: Families.data), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (context, state) => FamilyScreen( + family: Families.family(state.params['fid']!), + ), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + ), + GoRoute( + name: 'login', + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (state) { + // if the user is not logged in, they need to login + final loggedIn = loginInfo.loggedIn; + final loginloc = state.namedLocation('login'); + final loggingIn = state.subloc == loginloc; + + // bundle the location the user is coming from into a query parameter + final homeloc = state.namedLocation('home'); + final fromloc = state.subloc == homeloc ? '' : state.subloc; + if (!loggedIn) { + return loggingIn + ? null + : state.namedLocation( + 'login', + queryParams: {if (fromloc.isNotEmpty) 'from': fromloc}, + ); + } + + // if the user is logged in, send them where they were going before (or + // home if they weren't going anywhere) + if (loggingIn) return state.queryParams['from'] ?? homeloc; + + // no need to redirect at all + return null; + }, + + // changes on the listenable will cause the router to refresh it's route + refreshListenable: loginInfo, + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({required this.families, Key? key}) : super(key: key); + final List families; + + @override + Widget build(BuildContext context) { + final info = context.read(); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: info.logout, + tooltip: 'Logout: ${info.userName}', + icon: const Icon(Icons.logout), + ) + ], + ), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.goNamed('family', params: {'fid': f.id}), + ) + ], + ), + ); + } +} + +class FamilyScreen extends StatelessWidget { + const FamilyScreen({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go(context.namedLocation( + 'person', + params: {'fid': family.id, 'pid': p.id}, + queryParams: {'qid': 'quid'}, + )), + ), + ], + ), + ); +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + // log a user in, letting all the listeners know + context.read().login('test-user'); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/nav_builder.dart b/packages/go_router/example/lib/nav_builder.dart new file mode 100644 index 000000000000..2d532b30ce6d --- /dev/null +++ b/packages/go_router/example/lib/nav_builder.dart @@ -0,0 +1,206 @@ +// 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:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + final loginInfo = LoginInfo(); + static const title = 'GoRouter Example: Navigator Builder'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + late final _router = GoRouter( + debugLogDiagnostics: true, + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (context, state) => + HomeScreenNoLogout(families: Families.data), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + return FamilyScreen(family: family); + }, + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + ), + GoRoute( + name: 'login', + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (state) { + // if the user is not logged in, they need to login + final loggedIn = loginInfo.loggedIn; + final loginloc = state.namedLocation('login'); + final loggingIn = state.subloc == loginloc; + + // bundle the location the user is coming from into a query parameter + final homeloc = state.namedLocation('home'); + final fromloc = state.subloc == homeloc ? '' : state.subloc; + if (!loggedIn) { + return loggingIn + ? null + : state.namedLocation( + 'login', + queryParams: {if (fromloc.isNotEmpty) 'from': fromloc}, + ); + } + + // if the user is logged in, send them where they were going before (or + // home if they weren't going anywhere) + if (loggingIn) return state.queryParams['from'] ?? homeloc; + + // no need to redirect at all + return null; + }, + + // changes on the listenable will cause the router to refresh it's route + refreshListenable: loginInfo, + + // add a wrapper around the navigator to: + // - put loginInfo into the widget tree, and to + // - add an overlay to show a logout option + navigatorBuilder: (context, state, child) => + ChangeNotifierProvider.value( + value: loginInfo, + builder: (context, _) { + debugPrint('navigatorBuilder: ${state.subloc}'); + return loginInfo.loggedIn ? AuthOverlay(child: child) : child; + }, + ), + ); +} + +// A simple class for placing an exit button on top of all screens +class AuthOverlay extends StatelessWidget { + const AuthOverlay({required this.child, Key? key}) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) => Stack( + children: [ + child, + Positioned( + top: 90, + right: 4, + child: ElevatedButton( + onPressed: () { + context.read().logout(); + context.goNamed('home'); // clear out the `from` query param + }, + child: const Icon(Icons.logout), + ), + ), + ], + ); +} + +class HomeScreenNoLogout extends StatelessWidget { + const HomeScreenNoLogout({required this.families, Key? key}) + : super(key: key); + final List families; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.goNamed('family', params: {'fid': f.id}), + ) + ], + ), + ); +} + +class FamilyScreen extends StatelessWidget { + const FamilyScreen({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go('/family/${family.id}/person/${p.id}'), + ), + ], + ), + ); +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + // log a user in, letting all the listeners know + context.read().login('test-user'); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/nav_observer.dart b/packages/go_router/example/lib/nav_observer.dart new file mode 100644 index 000000000000..c3dc7a7aaa0f --- /dev/null +++ b/packages/go_router/example/lib/nav_observer.dart @@ -0,0 +1,153 @@ +// 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:go_router/go_router.dart'; +import 'package:logging/logging.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Navigator Observer'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + observers: [MyNavObserver()], + routes: [ + GoRoute( + // if there's no name, path will be used as name for observers + path: '/', + builder: (context, state) => const Page1Screen(), + routes: [ + GoRoute( + name: 'page2', + path: 'page2/:p1', + builder: (context, state) => const Page2Screen(), + routes: [ + GoRoute( + name: 'page3', + path: 'page3', + builder: (context, state) => const Page3Screen(), + ), + ], + ), + ], + ), + ], + ); +} + +class MyNavObserver extends NavigatorObserver { + MyNavObserver() { + log.onRecord.listen((e) => debugPrint('$e')); + } + + final log = Logger('MyNavObserver'); + + @override + void didPush(Route route, Route? previousRoute) => + log.info('didPush: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didPop(Route route, Route? previousRoute) => + log.info('didPop: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didRemove(Route route, Route? previousRoute) => + log.info('didRemove: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didReplace({Route? newRoute, Route? oldRoute}) => + log.info('didReplace: new= ${newRoute?.str}, old= ${oldRoute?.str}'); + + @override + void didStartUserGesture( + Route route, + Route? previousRoute, + ) => + log.info('didStartUserGesture: ${route.str}, ' + 'previousRoute= ${previousRoute?.str}'); + + @override + void didStopUserGesture() => log.info('didStopUserGesture'); +} + +extension on Route { + String get str => 'route(${settings.name}: ${settings.arguments})'; +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.goNamed( + 'page2', + params: {'p1': 'pv1'}, + queryParams: {'q1': 'qv1'}, + ), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.goNamed( + 'page3', + params: {'p1': 'pv2'}, + ), + child: const Text('Go to page 3'), + ), + ], + ), + ), + ); +} + +class Page3Screen extends StatelessWidget { + const Page3Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/nested_nav.dart b/packages/go_router/example/lib/nested_nav.dart new file mode 100644 index 000000000000..45846b0e6e6c --- /dev/null +++ b/packages/go_router/example/lib/nested_nav.dart @@ -0,0 +1,178 @@ +// 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:go_router/go_router.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Nested Navigation'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_) => '/family/${Families.data[0].id}', + ), + GoRoute( + path: '/family/:fid', + builder: (context, state) => FamilyTabsScreen( + key: state.pageKey, + selectedFamily: Families.family(state.params['fid']!), + ), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + + // show the current router location as the user navigates page to page; note + // that this is not required for nested navigation but it is useful to show + // the location as it changes + navigatorBuilder: (context, state, child) => Material( + child: Column( + children: [ + Expanded(child: child), + Padding( + padding: const EdgeInsets.all(8), + child: Text(state.location), + ), + ], + ), + ), + ); +} + +class FamilyTabsScreen extends StatefulWidget { + FamilyTabsScreen({required Family selectedFamily, Key? key}) + : index = Families.data.indexWhere((f) => f.id == selectedFamily.id), + super(key: key) { + assert(index != -1); + } + + final int index; + + @override + _FamilyTabsScreenState createState() => _FamilyTabsScreenState(); +} + +class _FamilyTabsScreenState extends State + with TickerProviderStateMixin { + late final TabController _controller; + + @override + void initState() { + super.initState(); + _controller = TabController( + length: Families.data.length, + vsync: this, + initialIndex: widget.index, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(FamilyTabsScreen oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.index = widget.index; + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text(App.title), + bottom: TabBar( + controller: _controller, + tabs: [for (final f in Families.data) Tab(text: f.name)], + onTap: (index) => _tap(context, index), + ), + ), + body: TabBarView( + controller: _controller, + children: [for (final f in Families.data) FamilyView(family: f)], + ), + ); + + void _tap(BuildContext context, int index) => + context.go('/family/${Families.data[index].id}'); +} + +class FamilyView extends StatefulWidget { + const FamilyView({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + State createState() => _FamilyViewState(); +} + +/// Use the [AutomaticKeepAliveClientMixin] to keep the state, like scroll +/// position and text fields when switching tabs, as well as when popping back +/// from sub screens. To use the mixin override [wantKeepAlive] and call +/// `super.build(context)` in build. +/// +/// In this example if you make a web build and make the browser window so low +/// that you have to scroll to see the last person on each family tab, you will +/// see that state is kept when you switch tabs and when you open a person +/// screen and pop back to the family. +class _FamilyViewState extends State + with AutomaticKeepAliveClientMixin { + // Override `wantKeepAlive` when using `AutomaticKeepAliveClientMixin`. + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + // Call `super.build` when using `AutomaticKeepAliveClientMixin`. + super.build(context); + return ListView( + children: [ + for (final p in widget.family.people) + ListTile( + title: Text(p.name), + onTap: () => + context.go('/family/${widget.family.id}/person/${p.id}'), + ), + ], + ); + } +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} diff --git a/packages/go_router/example/lib/push.dart b/packages/go_router/example/lib/push.dart new file mode 100644 index 000000000000..97cd03cffbe9 --- /dev/null +++ b/packages/go_router/example/lib/push.dart @@ -0,0 +1,91 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Push'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1ScreenWithPush(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => Page2ScreenWithPush( + int.parse(state.queryParams['push-count']!), + ), + ), + ], + ); +} + +class Page1ScreenWithPush extends StatelessWidget { + const Page1ScreenWithPush({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('${App.title}: page 1')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.push('/page2?push-count=1'), + child: const Text('Push page 2'), + ), + ], + ), + ), + ); +} + +class Page2ScreenWithPush extends StatelessWidget { + const Page2ScreenWithPush(this.pushCount, {Key? key}) : super(key: key); + final int pushCount; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text('${App.title}: page 2 w/ push count $pushCount'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.push( + '/page2?push-count=${pushCount + 1}', + ), + child: const Text('Push page 2 (again)'), + ), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/query_params.dart b/packages/go_router/example/lib/query_params.dart new file mode 100644 index 000000000000..f3fc7487dcc9 --- /dev/null +++ b/packages/go_router/example/lib/query_params.dart @@ -0,0 +1,173 @@ +// 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:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + final loginInfo = LoginInfo(); + static const title = 'GoRouter Example: Query Parameters'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: loginInfo, + child: MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + debugShowCheckedModeBanner: false, + ), + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => HomeScreen(families: Families.data), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (context, state) => FamilyScreen( + family: Families.family(state.params['fid']!), + ), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + ), + GoRoute( + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (state) { + // if the user is not logged in, they need to login + final loggedIn = loginInfo.loggedIn; + final loggingIn = state.subloc == '/login'; + + // bundle the location the user is coming from into a query parameter + final fromp = state.subloc == '/' ? '' : '?from=${state.subloc}'; + if (!loggedIn) return loggingIn ? null : '/login$fromp'; + + // if the user is logged in, send them where they were going before (or + // home if they weren't going anywhere) + if (loggingIn) return state.queryParams['from'] ?? '/'; + + // no need to redirect at all + return null; + }, + + // changes on the listenable will cause the router to refresh it's route + refreshListenable: loginInfo, + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({required this.families, Key? key}) : super(key: key); + final List families; + + @override + Widget build(BuildContext context) { + final info = context.read(); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: () { + info.logout(); + context.go('/'); + }, + tooltip: 'Logout: ${info.userName}', + icon: const Icon(Icons.logout), + ) + ], + ), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.go('/family/${f.id}'), + ) + ], + ), + ); + } +} + +class FamilyScreen extends StatelessWidget { + const FamilyScreen({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go('/family/${family.id}/person/${p.id}'), + ), + ], + ), + ); +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + // log a user in, letting all the listeners know + context.read().login('test-user'); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/redirection.dart b/packages/go_router/example/lib/redirection.dart new file mode 100644 index 000000000000..c358554c0105 --- /dev/null +++ b/packages/go_router/example/lib/redirection.dart @@ -0,0 +1,171 @@ +// 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:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + final loginInfo = LoginInfo(); + static const title = 'GoRouter Example: Redirection'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: loginInfo, + child: MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + debugShowCheckedModeBanner: false, + ), + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => HomeScreen(families: Families.data), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (context, state) => FamilyScreen( + family: Families.family(state.params['fid']!), + ), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + ), + GoRoute( + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (state) { + // if the user is not logged in, they need to login + final loggedIn = loginInfo.loggedIn; + final loggingIn = state.subloc == '/login'; + if (!loggedIn) return loggingIn ? null : '/login'; + + // if the user is logged in but still on the login page, send them to + // the home page + if (loggingIn) return '/'; + + // no need to redirect at all + return null; + }, + + // changes on the listenable will cause the router to refresh it's route + refreshListenable: loginInfo, + ); +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + // log a user in, letting all the listeners know + context.read().login('test-user'); + + // router will automatically redirect from /login to / using + // refreshListenable + //context.go('/'); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({required this.families, Key? key}) : super(key: key); + final List families; + + @override + Widget build(BuildContext context) { + final info = context.read(); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: info.logout, + tooltip: 'Logout: ${info.userName}', + icon: const Icon(Icons.logout), + ) + ], + ), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.go('/family/${f.id}'), + ) + ], + ), + ); + } +} + +class FamilyScreen extends StatelessWidget { + const FamilyScreen({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go('/family/${family.id}/person/${p.id}'), + ), + ], + ), + ); +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} diff --git a/packages/go_router/example/lib/router_neglect.dart b/packages/go_router/example/lib/router_neglect.dart new file mode 100644 index 000000000000..119d92266028 --- /dev/null +++ b/packages/go_router/example/lib/router_neglect.dart @@ -0,0 +1,87 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Router neglect'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + // turn off history tracking in the browser for this navigation + routerNeglect: true, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + const SizedBox(height: 8), + ElevatedButton( + // turn off history tracking in the browser for this navigation; + // note that this isn't necessary when you've set routerNeglect + // but it does illustrate the technique + onPressed: () => Router.neglect( + context, + () => context.push('/page2'), + ), + child: const Text('Push page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/router_stream_refresh.dart b/packages/go_router/example/lib/router_stream_refresh.dart new file mode 100644 index 000000000000..8271a369499c --- /dev/null +++ b/packages/go_router/example/lib/router_stream_refresh.dart @@ -0,0 +1,196 @@ +// 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:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'shared/data.dart'; + +void main() => runApp(const App()); + +class App extends StatefulWidget { + const App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Stream Refresh'; + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + late LoggedInState loggedInState; + late GoRouter router; + + @override + void initState() { + loggedInState = LoggedInState.seeded(false); + router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => HomeScreen(families: Families.data), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (context, state) => FamilyScreen( + family: Families.family(state.params['fid']!), + ), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + ), + GoRoute( + path: '/login', + builder: (context, state) => const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (state) { + // if the user is not logged in, they need to login + final loggedIn = loggedInState.state; + final loggingIn = state.subloc == '/login'; + + // bundle the location the user is coming from into a query parameter + final fromp = state.subloc == '/' ? '' : '?from=${state.subloc}'; + if (!loggedIn) return loggingIn ? null : '/login$fromp'; + + // if the user is logged in, send them where they were going before (or + // home if they weren't going anywhere) + if (loggingIn) return state.queryParams['from'] ?? '/'; + + // no need to redirect at all + return null; + }, + // changes on the listenable will cause the router to refresh it's route + refreshListenable: GoRouterRefreshStream(loggedInState.stream), + ); + super.initState(); + } + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => Provider.value( + value: loggedInState, + child: MaterialApp.router( + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate, + title: App.title, + debugShowCheckedModeBanner: false, + ), + ); + + @override + void dispose() { + loggedInState.dispose(); + super.dispose(); + } +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({ + required this.families, + Key? key, + }) : super(key: key); + + final List families; + + @override + Widget build(BuildContext context) { + final info = context.read(); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: () => info.emit(false), + icon: const Icon(Icons.logout), + ) + ], + ), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.go('/family/${f.id}'), + ) + ], + ), + ); + } +} + +class FamilyScreen extends StatelessWidget { + const FamilyScreen({ + required this.family, + Key? key, + }) : super(key: key); + final Family family; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go('/family/${family.id}/person/${p.id}'), + ), + ], + ), + ); +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({ + required this.family, + required this.person, + Key? key, + }) : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} + +class LoginScreen extends StatelessWidget { + const LoginScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + // log a user in, letting all the listeners know + context.read().emit(true); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/shared/data.dart b/packages/go_router/example/lib/shared/data.dart new file mode 100644 index 000000000000..db7dff4fa813 --- /dev/null +++ b/packages/go_router/example/lib/shared/data.dart @@ -0,0 +1,208 @@ +// 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 'dart:async'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; + +class Person { + Person({required this.id, required this.name, required this.age}); + + final String id; + final String name; + final int age; +} + +class Family { + Family({required this.id, required this.name, required this.people}); + + final String id; + final String name; + final List people; + + Person person(String pid) => people.singleWhere( + (p) => p.id == pid, + orElse: () => throw Exception('unknown person $pid for family $id'), + ); +} + +class Families { + static final data = [ + Family( + id: 'f1', + name: 'Sells', + people: [ + Person(id: 'p1', name: 'Chris', age: 52), + Person(id: 'p2', name: 'John', age: 27), + Person(id: 'p3', name: 'Tom', age: 26), + ], + ), + Family( + id: 'f2', + name: 'Addams', + people: [ + Person(id: 'p1', name: 'Gomez', age: 55), + Person(id: 'p2', name: 'Morticia', age: 50), + Person(id: 'p3', name: 'Pugsley', age: 10), + Person(id: 'p4', name: 'Wednesday', age: 17), + ], + ), + Family( + id: 'f3', + name: 'Hunting', + people: [ + Person(id: 'p1', name: 'Mom', age: 54), + Person(id: 'p2', name: 'Dad', age: 55), + Person(id: 'p3', name: 'Will', age: 20), + Person(id: 'p4', name: 'Marky', age: 21), + Person(id: 'p5', name: 'Ricky', age: 22), + Person(id: 'p6', name: 'Danny', age: 23), + Person(id: 'p7', name: 'Terry', age: 24), + Person(id: 'p8', name: 'Mikey', age: 25), + Person(id: 'p9', name: 'Davey', age: 26), + Person(id: 'p10', name: 'Timmy', age: 27), + Person(id: 'p11', name: 'Tommy', age: 28), + Person(id: 'p12', name: 'Joey', age: 29), + Person(id: 'p13', name: 'Robby', age: 30), + Person(id: 'p14', name: 'Johnny', age: 31), + Person(id: 'p15', name: 'Brian', age: 32), + ], + ), + ]; + + static Family family(String fid) => data.family(fid); +} + +extension on List { + Family family(String fid) => singleWhere( + (f) => f.id == fid, + orElse: () => throw Exception('unknown family $fid'), + ); +} + +class LoginInfo extends ChangeNotifier { + var _userName = ''; + String get userName => _userName; + bool get loggedIn => _userName.isNotEmpty; + + void login(String userName) { + _userName = userName; + notifyListeners(); + } + + void logout() { + _userName = ''; + notifyListeners(); + } +} + +class LoginInfo2 extends ChangeNotifier { + var _userName = ''; + String get userName => _userName; + bool get loggedIn => _userName.isNotEmpty; + + Future login(String userName) async { + _userName = userName; + notifyListeners(); + await Future.delayed(const Duration(microseconds: 2500)); + } + + Future logout() async { + _userName = ''; + notifyListeners(); + await Future.delayed(const Duration(microseconds: 2500)); + } +} + +class FamilyPerson { + FamilyPerson({required this.family, required this.person}); + + final Family family; + final Person person; +} + +class Repository { + static final rnd = Random(); + + Future> getFamilies() async { + // simulate network delay + await Future.delayed(const Duration(seconds: 1)); + + // simulate error + // if (rnd.nextBool()) throw Exception('error fetching families'); + + // return data "fetched over the network" + return Families.data; + } + + Future getFamily(String fid) async => + (await getFamilies()).family(fid); + + Future getPerson(String fid, String pid) async { + final family = await getFamily(fid); + return FamilyPerson(family: family, person: family.person(pid)); + } +} + +class Repository2 { + Repository2._(this.userName); + final String userName; + + static Future get(String userName) async { + // simulate network delay + await Future.delayed(const Duration(seconds: 1)); + return Repository2._(userName); + } + + static final rnd = Random(); + + Future> getFamilies() async { + // simulate network delay + await Future.delayed(const Duration(seconds: 1)); + + // simulate error + // if (rnd.nextBool()) throw Exception('error fetching families'); + + // return data "fetched over the network" + return Families.data; + } + + Future getFamily(String fid) async => + (await getFamilies()).family(fid); + + Future getPerson(String fid, String pid) async { + final family = await getFamily(fid); + return FamilyPerson(family: family, person: family.person(pid)); + } +} + +abstract class StateStream { + StateStream(); + + StateStream.seeded(T value) : state = value { + _controller.add(value); + } + + final StreamController _controller = StreamController(); + late T state; + + Stream get stream => _controller.stream; + + void emit(T state) { + this.state = state; + _controller.add(state); + } + + void dispose() { + _controller.close(); + } +} + +class LoggedInState extends StateStream { + LoggedInState(); + + // ignore: avoid_positional_boolean_parameters + LoggedInState.seeded(bool value) : super.seeded(value); +} diff --git a/packages/go_router/example/lib/shared_scaffold.dart b/packages/go_router/example/lib/shared_scaffold.dart new file mode 100644 index 000000000000..2675ff6637d6 --- /dev/null +++ b/packages/go_router/example/lib/shared_scaffold.dart @@ -0,0 +1,186 @@ +// 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:adaptive_navigation/adaptive_navigation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Shared Scaffold'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + late final _router = GoRouter( + debugLogDiagnostics: true, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => _build(const Page1View()), + ), + GoRoute( + path: '/page2', + builder: (context, state) => _build(const Page2View()), + ), + ], + errorBuilder: (context, state) => _build(ErrorView(state.error!)), + + // use the navigatorBuilder to keep the SharedScaffold from being animated + // as new pages as shown; wrappiong that in single-page Navigator at the + // root provides an Overlay needed for the adaptive navigation scaffold and + // a root Navigator to show the About box + navigatorBuilder: (context, state, child) => Navigator( + onPopPage: (route, dynamic result) { + route.didPop(result); + return false; // don't pop the single page on the root navigator + }, + pages: [ + MaterialPage( + child: state.error != null + ? ErrorScaffold(body: child) + : SharedScaffold( + selectedIndex: state.subloc == '/' ? 0 : 1, + body: child, + ), + ), + ], + ), + ); + + // wrap the view widgets in a Scaffold to get the exit animation just right on + // the page being replaced + Widget _build(Widget child) => Scaffold(body: child); +} + +class SharedScaffold extends StatefulWidget { + const SharedScaffold({ + required this.selectedIndex, + required this.body, + Key? key, + }) : super(key: key); + + final int selectedIndex; + final Widget body; + + @override + State createState() => _SharedScaffoldState(); +} + +class _SharedScaffoldState extends State { + @override + Widget build(BuildContext context) => AdaptiveNavigationScaffold( + selectedIndex: widget.selectedIndex, + destinations: const [ + AdaptiveScaffoldDestination(title: 'Page 1', icon: Icons.first_page), + AdaptiveScaffoldDestination(title: 'Page 2', icon: Icons.last_page), + AdaptiveScaffoldDestination(title: 'About', icon: Icons.info), + ], + appBar: AdaptiveAppBar(title: const Text(App.title)), + navigationTypeResolver: (context) => + _drawerSize ? NavigationType.drawer : NavigationType.bottom, + onDestinationSelected: (index) async { + // if there's a drawer, close it + if (_drawerSize) Navigator.pop(context); + + switch (index) { + case 0: + context.go('/'); + break; + case 1: + context.go('/page2'); + break; + case 2: + final packageInfo = await PackageInfo.fromPlatform(); + showAboutDialog( + context: context, + applicationName: packageInfo.appName, + applicationVersion: 'v${packageInfo.version}', + applicationLegalese: 'Copyright © 2022, Acme, Corp.', + ); + break; + default: + throw Exception('Invalid index'); + } + }, + body: widget.body, + ); + + bool get _drawerSize => MediaQuery.of(context).size.width >= 600; +} + +class Page1View extends StatelessWidget { + const Page1View({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ); +} + +class Page2View extends StatelessWidget { + const Page2View({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ); +} + +class ErrorScaffold extends StatelessWidget { + const ErrorScaffold({ + required this.body, + Key? key, + }) : super(key: key); + + final Widget body; + @override + Widget build(BuildContext context) => Scaffold( + appBar: AdaptiveAppBar(title: const Text('Page Not Found')), + body: body, + ); +} + +class ErrorView extends StatelessWidget { + const ErrorView(this.error, {Key? key}) : super(key: key); + final Exception error; + + @override + Widget build(BuildContext context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText(error.toString()), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ); +} diff --git a/packages/go_router/example/lib/state_restoration.dart b/packages/go_router/example/lib/state_restoration.dart new file mode 100644 index 000000000000..bff77aa58937 --- /dev/null +++ b/packages/go_router/example/lib/state_restoration.dart @@ -0,0 +1,94 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp( + const RootRestorationScope(restorationId: 'root', child: App()), + ); + +class App extends StatefulWidget { + const App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: State Restoration'; + + @override + State createState() => _AppState(); +} + +class _AppState extends State with RestorationMixin { + @override + String get restorationId => 'wrapper'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + // todo: implement restoreState for you app + } + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: App.title, + restorationScopeId: 'app', + ); + + final _router = GoRouter( + routes: [ + // restorationId set for the route automatically + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + + // restorationId set for the route automatically + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + restorationScopeId: 'router', + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/sub_routes.dart b/packages/go_router/example/lib/sub_routes.dart new file mode 100644 index 000000000000..956876a78209 --- /dev/null +++ b/packages/go_router/example/lib/sub_routes.dart @@ -0,0 +1,103 @@ +// 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:go_router/go_router.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Sub-routes'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => HomeScreen(families: Families.data), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (context, state) => FamilyScreen( + family: Families.family(state.params['fid']!), + ), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + + return PersonScreen(family: family, person: person); + }, + ), + ], + ), + ], + ), + ], + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({required this.families, Key? key}) : super(key: key); + final List families; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.go('/family/${f.id}'), + ) + ], + ), + ); +} + +class FamilyScreen extends StatelessWidget { + const FamilyScreen({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(family.name)), + body: ListView( + children: [ + for (final p in family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go('/family/${family.id}/person/${p.id}'), + ), + ], + ), + ); +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} diff --git a/packages/go_router/example/lib/transitions.dart b/packages/go_router/example/lib/transitions.dart new file mode 100644 index 000000000000..4750a03e8fcc --- /dev/null +++ b/packages/go_router/example/lib/transitions.dart @@ -0,0 +1,130 @@ +// 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:go_router/go_router.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Custom Transitions'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + ); + + final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_) => '/none', + ), + GoRoute( + path: '/fade', + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'fade', + color: Colors.red, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child), + ), + ), + GoRoute( + path: '/scale', + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'scale', + color: Colors.green, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + ScaleTransition(scale: animation, child: child), + ), + ), + GoRoute( + path: '/slide', + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'slide', + color: Colors.yellow, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0.25, 0.25), + end: Offset.zero, + ).chain(CurveTween(curve: Curves.easeIn)), + ), + child: child), + ), + ), + GoRoute( + path: '/rotation', + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'rotation', + color: Colors.purple, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + RotationTransition(turns: animation, child: child), + ), + ), + GoRoute( + path: '/none', + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'none', + color: Colors.white, + ), + ), + ), + ], + ); +} + +class ExampleTransitionsScreen extends StatelessWidget { + const ExampleTransitionsScreen({ + required this.color, + required this.kind, + Key? key, + }) : super(key: key); + + static final kinds = ['fade', 'scale', 'slide', 'rotation', 'none']; + final Color color; + final String kind; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text('${App.title}: $kind')), + body: Container( + color: color, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final kind in kinds) + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.go('/$kind'), + child: Text('$kind transition'), + ), + ) + ], + ), + ), + ), + ); +} diff --git a/packages/go_router/example/lib/url_strategy.dart b/packages/go_router/example/lib/url_strategy.dart new file mode 100644 index 000000000000..91fbcc11e714 --- /dev/null +++ b/packages/go_router/example/lib/url_strategy.dart @@ -0,0 +1,85 @@ +// 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:go_router/go_router.dart'; + +void main() { + // turn on the # in the URLs on the web (default) + // GoRouter.setUrlPathStrategy(UrlPathStrategy.hash); + + // turn off the # in the URLs on the web + // GoRouter.setUrlPathStrategy(UrlPathStrategy.path); + + runApp(App()); +} + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: URL Path Strategy'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: App.title, + ); + + final _router = GoRouter( + // turn off the # in the URLs on the web + urlPathStrategy: UrlPathStrategy.path, + + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/example/lib/user_input.dart b/packages/go_router/example/lib/user_input.dart new file mode 100644 index 000000000000..26af9d26189b --- /dev/null +++ b/packages/go_router/example/lib/user_input.dart @@ -0,0 +1,364 @@ +// 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:adaptive_dialog/adaptive_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'shared/data.dart'; + +void main() => runApp(App()); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: Navigator Integration'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final _router = GoRouter( + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (context, state) => HomeScreen(families: Families.data), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (context, state) => FamilyScreenWithAdd( + family: Families.family(state.params['fid']!), + ), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + final person = family.person(state.params['pid']!); + return PersonScreen(family: family, person: person); + }, + ), + GoRoute( + name: 'new-person', + path: 'new-person', + builder: (context, state) { + final family = Families.family(state.params['fid']!); + return NewPersonScreen2(family: family); + }, + ), + ], + ), + ], + ), + ], + ); +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({required this.families, Key? key}) : super(key: key); + final List families; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: ListView( + children: [ + for (final f in families) + ListTile( + title: Text(f.name), + onTap: () => context.goNamed('family', params: {'fid': f.id}), + ) + ], + ), + ); +} + +class FamilyScreenWithAdd extends StatefulWidget { + const FamilyScreenWithAdd({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + State createState() => _FamilyScreenWithAddState(); +} + +class _FamilyScreenWithAddState extends State { + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(widget.family.name), + actions: [ + IconButton( + // onPressed: () => _addPerson1(context), // Navigator-style + onPressed: () => _addPerson2(context), // GoRouter-style + tooltip: 'Add Person', + icon: const Icon(Icons.add), + ), + ], + ), + body: ListView( + children: [ + for (final p in widget.family.people) + ListTile( + title: Text(p.name), + onTap: () => context.go(context.namedLocation( + 'person', + params: {'fid': widget.family.id, 'pid': p.id}, + queryParams: {'qid': 'quid'}, + )), + ), + ], + ), + ); + + // using a Navigator and a Navigator result + // ignore: unused_element + Future _addPerson1(BuildContext context) async { + final person = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NewPersonScreen1(family: widget.family), + ), + ); + + if (person != null) { + setState(() => widget.family.people.add(person)); + + // ignore: use_build_context_synchronously + context.goNamed('person', params: { + 'fid': widget.family.id, + 'pid': person.id, + }); + } + } + + // using a GoRouter page + void _addPerson2(BuildContext context) { + context.goNamed('new-person', params: {'fid': widget.family.id}); + } +} + +class PersonScreen extends StatelessWidget { + const PersonScreen({required this.family, required this.person, Key? key}) + : super(key: key); + + final Family family; + final Person person; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); +} + +// returning a Navigator result +class NewPersonScreen1 extends StatefulWidget { + const NewPersonScreen1({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + State createState() => _NewPersonScreen1State(); +} + +class _NewPersonScreen1State extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _ageController = TextEditingController(); + + @override + void dispose() { + super.dispose(); + _nameController.dispose(); + _ageController.dispose(); + } + + @override + Widget build(BuildContext context) => WillPopScope( + // ask the user if they'd like to adandon their data + onWillPop: () async => abandonNewPerson(context), + child: Scaffold( + appBar: AppBar( + title: Text('New person for family ${widget.family.name}'), + ), + body: Form( + key: _formKey, + child: Center( + child: SizedBox( + width: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration(labelText: 'name'), + validator: (value) => value == null || value.isEmpty + ? 'Please enter a name' + : null, + ), + TextFormField( + controller: _ageController, + decoration: const InputDecoration(labelText: 'age'), + validator: (value) => value == null || + value.isEmpty || + int.tryParse(value) == null + ? 'Please enter an age' + : null, + ), + ButtonBar(children: [ + TextButton( + onPressed: () async { + // ask the user if they'd like to adandon their data + if (await abandonNewPerson(context)) { + Navigator.pop(context); + } + }, + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + final person = Person( + id: 'p${widget.family.people.length + 1}', + name: _nameController.text, + age: int.parse(_ageController.text), + ); + + Navigator.pop(context, person); + } + }, + child: const Text('Create'), + ), + ]), + ], + ), + ), + ), + ), + ), + ); + + Future abandonNewPerson(BuildContext context) async { + final result = await showOkCancelAlertDialog( + context: context, + title: 'Abandon New Person', + message: 'Are you sure you abandon this new person?', + okLabel: 'Keep', + cancelLabel: 'Abandon', + ); + + return result == OkCancelResult.cancel; + } +} + +// adding the result to the data directly (GoRouter page) +class NewPersonScreen2 extends StatefulWidget { + const NewPersonScreen2({required this.family, Key? key}) : super(key: key); + final Family family; + + @override + State createState() => _NewPersonScreen2State(); +} + +class _NewPersonScreen2State extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _ageController = TextEditingController(); + + @override + void dispose() { + super.dispose(); + _nameController.dispose(); + _ageController.dispose(); + } + + @override + Widget build(BuildContext context) => WillPopScope( + // ask the user if they'd like to adandon their data + onWillPop: () async => abandonNewPerson(context), + child: Scaffold( + appBar: AppBar( + title: Text('New person for family ${widget.family.name}'), + ), + body: Form( + key: _formKey, + child: Center( + child: SizedBox( + width: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration(labelText: 'name'), + validator: (value) => value == null || value.isEmpty + ? 'Please enter a name' + : null, + ), + TextFormField( + controller: _ageController, + decoration: const InputDecoration(labelText: 'age'), + validator: (value) => value == null || + value.isEmpty || + int.tryParse(value) == null + ? 'Please enter an age' + : null, + ), + ButtonBar(children: [ + TextButton( + onPressed: () async { + // ask the user if they'd like to adandon their data + if (await abandonNewPerson(context)) { + // Navigator.pop(context) would work here, too + context.pop(); + } + }, + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + final person = Person( + id: 'p${widget.family.people.length + 1}', + name: _nameController.text, + age: int.parse(_ageController.text), + ); + + widget.family.people.add(person); + + context.goNamed('person', params: { + 'fid': widget.family.id, + 'pid': person.id, + }); + } + }, + child: const Text('Create'), + ), + ]), + ], + ), + ), + ), + ), + ), + ); + + Future abandonNewPerson(BuildContext context) async { + final result = await showOkCancelAlertDialog( + context: context, + title: 'Abandon New Person', + message: 'Are you sure you abandon this new person?', + okLabel: 'Keep', + cancelLabel: 'Abandon', + ); + + return result == OkCancelResult.cancel; + } +} diff --git a/packages/go_router/example/lib/widgets_app.dart b/packages/go_router/example/lib/widgets_app.dart new file mode 100644 index 000000000000..595bb66c62b9 --- /dev/null +++ b/packages/go_router/example/lib/widgets_app.dart @@ -0,0 +1,116 @@ +// 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/widgets.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +const blue = Color(0xFF2196F3); +const white = Color(0xFFFFFFFF); + +class App extends StatelessWidget { + App({Key? key}) : super(key: key); + + static const title = 'GoRouter Example: WidgetsApp'; + + @override + Widget build(BuildContext context) => WidgetsApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, + title: title, + color: blue, + textStyle: const TextStyle(color: blue), + ); + + final _router = GoRouter( + debugLogDiagnostics: true, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (context, state) => const Page2Screen(), + ), + ], + ); +} + +class Page1Screen extends StatelessWidget { + const Page1Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + App.title, + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Button( + onPressed: () => context.go('/page2'), + child: const Text( + 'Go to page 2', + style: TextStyle(color: white), + ), + ), + ], + ), + ), + ); +} + +class Page2Screen extends StatelessWidget { + const Page2Screen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + App.title, + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Button( + onPressed: () => context.go('/'), + child: const Text( + 'Go to home page', + style: TextStyle(color: white), + ), + ), + ], + ), + ), + ); +} + +class Button extends StatelessWidget { + const Button({ + required this.onPressed, + required this.child, + Key? key, + }) : super(key: key); + + final VoidCallback onPressed; + final Widget child; + + @override + Widget build(BuildContext context) => GestureDetector( + onTap: onPressed, + child: Container( + padding: const EdgeInsets.all(8), + color: blue, + child: child, + ), + ); +} diff --git a/packages/go_router/example/macos/.gitignore b/packages/go_router/example/macos/.gitignore new file mode 100644 index 000000000000..d2fd3772308c --- /dev/null +++ b/packages/go_router/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/packages/go_router/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/go_router/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/go_router/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/go_router/example/macos/Flutter/Flutter-Release.xcconfig b/packages/go_router/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/go_router/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/go_router/example/macos/Podfile b/packages/go_router/example/macos/Podfile new file mode 100644 index 000000000000..22d9caad2e9d --- /dev/null +++ b/packages/go_router/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.12' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/go_router/example/macos/Runner.xcodeproj/project.pbxproj b/packages/go_router/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..ba9dd7c24e29 --- /dev/null +++ b/packages/go_router/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4579CDF431AA5E4C6FE443E0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4CF4108E68F1905F4C00B71 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* go_router_ex.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = go_router_ex.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7718CFB2ECB4B120864B7158 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9A978829DFF67240C8200DEC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + B4CF4108E68F1905F4C00B71 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D385DAFC8FF088B9DF2D10F2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4579CDF431AA5E4C6FE443E0 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 39BE41AD5E7025C012637B9B /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* go_router_ex.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 39BE41AD5E7025C012637B9B /* Pods */ = { + isa = PBXGroup; + children = ( + 7718CFB2ECB4B120864B7158 /* Pods-Runner.debug.xcconfig */, + D385DAFC8FF088B9DF2D10F2 /* Pods-Runner.release.xcconfig */, + 9A978829DFF67240C8200DEC /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B4CF4108E68F1905F4C00B71 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B2D0563F9BABB52878AAF09A /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + FA450CD12AE80B85614A31CD /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* go_router_ex.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + B2D0563F9BABB52878AAF09A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FA450CD12AE80B85614A31CD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/go_router/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/go_router/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/go_router/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/go_router/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/go_router/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..7c0def66ac01 --- /dev/null +++ b/packages/go_router/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/go_router/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/go_router/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/go_router/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/go_router/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/go_router/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/go_router/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/go_router/example/macos/Runner/AppDelegate.swift b/packages/go_router/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/go_router/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// 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 Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..3c4935a7ca84 Binary files /dev/null and b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..ed4cc1642168 Binary files /dev/null and b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..483be6138973 Binary files /dev/null and b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bcbf36df2f2a Binary files /dev/null and b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..9c0a65286476 Binary files /dev/null and b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..e71a726136a4 Binary files /dev/null and b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..8a31fe2dd3f9 Binary files /dev/null and b/packages/go_router/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/go_router/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/go_router/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..537341abf994 --- /dev/null +++ b/packages/go_router/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/go_router/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/go_router/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..37223033e8c9 --- /dev/null +++ b/packages/go_router/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = go_router_ex + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.builderUp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. diff --git a/packages/go_router/example/macos/Runner/Configs/Debug.xcconfig b/packages/go_router/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/go_router/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/go_router/example/macos/Runner/Configs/Release.xcconfig b/packages/go_router/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/go_router/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/go_router/example/macos/Runner/Configs/Warnings.xcconfig b/packages/go_router/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/go_router/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/go_router/example/macos/Runner/DebugProfile.entitlements b/packages/go_router/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dddb8a30c851 --- /dev/null +++ b/packages/go_router/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/go_router/example/macos/Runner/Info.plist b/packages/go_router/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/go_router/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/go_router/example/macos/Runner/MainFlutterWindow.swift b/packages/go_router/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/go_router/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// 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 Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/go_router/example/macos/Runner/Release.entitlements b/packages/go_router/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/packages/go_router/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/go_router/example/pubspec.yaml b/packages/go_router/example/pubspec.yaml new file mode 100644 index 000000000000..bb1ee24564ce --- /dev/null +++ b/packages/go_router/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: go_router_examples +description: go_router examples +version: 3.0.1 +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + adaptive_dialog: ^1.2.0 + adaptive_navigation: ^0.0.4 + collection: ^1.15.0 + cupertino_icons: ^1.0.2 + flutter: + sdk: flutter + go_router: + path: .. + logging: ^1.0.0 + package_info_plus: ^1.3.0 + provider: ^5.0.0 + shared_preferences: ^2.0.11 + url_launcher: ^6.0.7 + +dev_dependencies: + all_lint_rules_community: ^0.0.4 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/go_router/example/web/favicon.png b/packages/go_router/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/go_router/example/web/favicon.png differ diff --git a/packages/go_router/example/web/icons/Icon-192.png b/packages/go_router/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/go_router/example/web/icons/Icon-192.png differ diff --git a/packages/go_router/example/web/icons/Icon-512.png b/packages/go_router/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/go_router/example/web/icons/Icon-512.png differ diff --git a/packages/go_router/example/web/index.html b/packages/go_router/example/web/index.html new file mode 100644 index 000000000000..26266a4f3870 --- /dev/null +++ b/packages/go_router/example/web/index.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + simple + + + + + + + diff --git a/packages/go_router/example/web/manifest.json b/packages/go_router/example/web/manifest.json new file mode 100644 index 000000000000..c181e97b4987 --- /dev/null +++ b/packages/go_router/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "simple", + "short_name": "simple", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart new file mode 100644 index 000000000000..5a3150e2a2c1 --- /dev/null +++ b/packages/go_router/lib/go_router.dart @@ -0,0 +1,74 @@ +// 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. + +/// A declarative router for Flutter based on Navigation 2 supporting +/// deep linking, data-driven routes and more +library go_router; + +import 'package:flutter/widgets.dart'; + +import 'src/go_router.dart'; + +export 'src/custom_transition_page.dart'; +export 'src/go_route.dart'; +export 'src/go_router.dart'; +export 'src/go_router_refresh_stream.dart'; +export 'src/go_router_state.dart'; +export 'src/typedefs.dart' show GoRouterPageBuilder, GoRouterRedirect; +export 'src/url_path_strategy.dart'; + +/// Dart extension to add navigation function to a BuildContext object, e.g. +/// context.go('/'); +// NOTE: adding this here instead of in /src to work-around a Dart analyzer bug +// and fix: https://github.com/csells/go_router/issues/116 +extension GoRouterHelper on BuildContext { + /// Get a location from route name and parameters. + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => + GoRouter.of(this) + .namedLocation(name, params: params, queryParams: queryParams); + + /// Navigate to a location. + void go(String location, {Object? extra}) => + GoRouter.of(this).go(location, extra: extra); + + /// Navigate to a named route. + void goNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + GoRouter.of(this).goNamed( + name, + params: params, + queryParams: queryParams, + extra: extra, + ); + + /// Push a location onto the page stack. + void push(String location, {Object? extra}) => + GoRouter.of(this).push(location, extra: extra); + + /// Navigate to a named route onto the page stack. + void pushNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + GoRouter.of(this).pushNamed( + name, + params: params, + queryParams: queryParams, + extra: extra, + ); + + /// Pop the top page off the Navigator's page stack by calling + /// [Navigator.pop]. + void pop() => GoRouter.of(this).pop(); +} diff --git a/packages/go_router/lib/src/custom_transition_page.dart b/packages/go_router/lib/src/custom_transition_page.dart new file mode 100644 index 000000000000..ab0c8e54fa2a --- /dev/null +++ b/packages/go_router/lib/src/custom_transition_page.dart @@ -0,0 +1,184 @@ +// 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/widgets.dart'; + +/// Page with custom transition functionality. +/// +/// To be used instead of MaterialPage or CupertinoPage, which provide +/// their own transitions. +class CustomTransitionPage extends Page { + /// Constructor for a page with custom transition functionality. + /// + /// To be used instead of MaterialPage or CupertinoPage, which provide + /// their own transitions. + const CustomTransitionPage({ + required this.child, + required this.transitionsBuilder, + this.transitionDuration = const Duration(milliseconds: 300), + this.maintainState = true, + this.fullscreenDialog = false, + this.opaque = true, + this.barrierDismissible = false, + this.barrierColor, + this.barrierLabel, + LocalKey? key, + String? name, + Object? arguments, + String? restorationId, + }) : super( + key: key, + name: name, + arguments: arguments, + restorationId: restorationId, + ); + + /// The content to be shown in the Route created by this page. + final Widget child; + + /// A duration argument to customize the duration of the custom page + /// transition. + /// + /// Defaults to 300ms. + final Duration transitionDuration; + + /// Whether the route should remain in memory when it is inactive. + /// + /// If this is true, then the route is maintained, so that any futures it is + /// holding from the next route will properly resolve when the next route + /// pops. If this is not necessary, this can be set to false to allow the + /// framework to entirely discard the route's widget hierarchy when it is + /// not visible. + final bool maintainState; + + /// Whether this page route is a full-screen dialog. + /// + /// In Material and Cupertino, being fullscreen has the effects of making the + /// app bars have a close button instead of a back button. On iOS, dialogs + /// transitions animate differently and are also not closeable with the + /// back swipe gesture. + final bool fullscreenDialog; + + /// Whether the route obscures previous routes when the transition is + /// complete. + /// + /// When an opaque route's entrance transition is complete, the routes + /// behind the opaque route will not be built to save resources. + final bool opaque; + + /// Whether you can dismiss this route by tapping the modal barrier. + final bool barrierDismissible; + + /// The color to use for the modal barrier. + /// + /// If this is null, the barrier will be transparent. + final Color? barrierColor; + + /// The semantic label used for a dismissible barrier. + /// + /// If the barrier is dismissible, this label will be read out if + /// accessibility tools (like VoiceOver on iOS) focus on the barrier. + final String? barrierLabel; + + /// Override this method to wrap the child with one or more transition + /// widgets that define how the route arrives on and leaves the screen. + /// + /// By default, the child (which contains the widget returned by buildPage) is + /// not wrapped in any transition widgets. + /// + /// The transitionsBuilder method, is called each time the Route's state + /// changes while it is visible (e.g. if the value of canPop changes on the + /// active route). + /// + /// The transitionsBuilder method is typically used to define transitions + /// that animate the new topmost route's comings and goings. When the + /// Navigator pushes a route on the top of its stack, the new route's + /// primary animation runs from 0.0 to 1.0. When the Navigator pops the + /// topmost route, e.g. because the use pressed the back button, the primary + /// animation runs from 1.0 to 0.0. + final Widget Function(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) transitionsBuilder; + + @override + Route createRoute(BuildContext context) => + _CustomTransitionPageRoute(this); +} + +class _CustomTransitionPageRoute extends PageRoute { + _CustomTransitionPageRoute(CustomTransitionPage page) + : super(settings: page); + + CustomTransitionPage get _page => settings as CustomTransitionPage; + + @override + Color? get barrierColor => _page.barrierColor; + + @override + String? get barrierLabel => _page.barrierLabel; + + @override + Duration get transitionDuration => _page.transitionDuration; + + @override + bool get maintainState => _page.maintainState; + + @override + bool get fullscreenDialog => _page.fullscreenDialog; + + @override + bool get opaque => _page.opaque; + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + Semantics( + scopesRoute: true, + explicitChildNodes: true, + child: _page.child, + ); + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => + _page.transitionsBuilder( + context, + animation, + secondaryAnimation, + child, + ); +} + +/// Custom transition page with no transition. +class NoTransitionPage extends CustomTransitionPage { + /// Constructor for a page with no transition functionality. + const NoTransitionPage({ + required Widget child, + String? name, + Object? arguments, + String? restorationId, + LocalKey? key, + }) : super( + transitionsBuilder: _transitionsBuilder, + transitionDuration: const Duration(microseconds: 1), // hack for #205 + key: key, + name: name, + arguments: arguments, + restorationId: restorationId, + child: child, + ); + + static Widget _transitionsBuilder( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + child; +} diff --git a/packages/go_router/lib/src/go_route.dart b/packages/go_router/lib/src/go_route.dart new file mode 100644 index 000000000000..e58dbdfadbb3 --- /dev/null +++ b/packages/go_router/lib/src/go_route.dart @@ -0,0 +1,206 @@ +// 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:collection/collection.dart'; +import 'package:flutter/widgets.dart'; + +import 'custom_transition_page.dart'; +import 'go_router_state.dart'; +import 'path_parser.dart'; +import 'typedefs.dart'; + +/// A declarative mapping between a route path and a page builder. +class GoRoute { + /// Default constructor used to create mapping between a + /// route path and a page builder. + GoRoute({ + required this.path, + this.name, + this.pageBuilder, + this.builder = _builder, + this.routes = const [], + this.redirect = _redirect, + }) { + if (path.isEmpty) { + throw Exception('GoRoute path cannot be empty'); + } + + if (name != null && name!.isEmpty) { + throw Exception('GoRoute name cannot be empty'); + } + + // cache the path regexp and parameters + _pathRE = patternToRegExp(path, _pathParams); + + // check path params + final groupedParams = _pathParams.groupListsBy((p) => p); + final dupParams = Map>.fromEntries( + groupedParams.entries.where((e) => e.value.length > 1), + ); + if (dupParams.isNotEmpty) { + throw Exception( + 'duplicate path params: ${dupParams.keys.join(', ')}', + ); + } + + // check sub-routes + for (final route in routes) { + // check paths + if (route.path != '/' && + (route.path.startsWith('/') || route.path.endsWith('/'))) { + throw Exception( + 'sub-route path may not start or end with /: ${route.path}', + ); + } + } + } + + final _pathParams = []; + late final RegExp _pathRE; + + /// Optional name of the route. + /// + /// If used, a unique string name must be provided and it can not be empty. + final String? name; + + /// The path of this go route. + /// + /// For example in: + /// ``` + /// GoRoute( + /// path: '/', + /// pageBuilder: (context, state) => MaterialPage( + /// key: state.pageKey, + /// child: HomePage(families: Families.data), + /// ), + /// ), + /// ``` + final String path; + + /// A page builder for this route. + /// + /// Typically a MaterialPage, as in: + /// ``` + /// GoRoute( + /// path: '/', + /// pageBuilder: (context, state) => MaterialPage( + /// key: state.pageKey, + /// child: HomePage(families: Families.data), + /// ), + /// ), + /// ``` + /// + /// You can also use CupertinoPage, and for a custom page builder to use + /// custom page transitions, you can use [CustomTransitionPage]. + final GoRouterPageBuilder? pageBuilder; + + /// A custom builder for this route. + /// + /// For example: + /// ``` + /// GoRoute( + /// path: '/', + /// builder: (context, state) => FamilyPage( + /// families: Families.family( + /// state.params['id'], + /// ), + /// ), + /// ), + /// ``` + /// + final GoRouterWidgetBuilder builder; + + /// A list of sub go routes for this route. + /// + /// To create sub-routes for a route, provide them as a [GoRoute] list + /// with the sub routes. + /// + /// For example these routes: + /// ``` + /// / => HomePage() + /// family/f1 => FamilyPage('f1') + /// person/p2 => PersonPage('f1', 'p2') ← showing this page, Back pops ↑ + /// ``` + /// + /// Can be represented as: + /// + /// ``` + /// final _router = GoRouter( + /// routes: [ + /// GoRoute( + /// path: '/', + /// pageBuilder: (context, state) => MaterialPage( + /// key: state.pageKey, + /// child: HomePage(families: Families.data), + /// ), + /// routes: [ + /// GoRoute( + /// path: 'family/:fid', + /// pageBuilder: (context, state) { + /// final family = Families.family(state.params['fid']!); + /// return MaterialPage( + /// key: state.pageKey, + /// child: FamilyPage(family: family), + /// ); + /// }, + /// routes: [ + /// GoRoute( + /// path: 'person/:pid', + /// pageBuilder: (context, state) { + /// final family = Families.family(state.params['fid']!); + /// final person = family.person(state.params['pid']!); + /// return MaterialPage( + /// key: state.pageKey, + /// child: PersonPage(family: family, person: person), + /// ); + /// }, + /// ), + /// ], + /// ), + /// ], + /// ), + /// ], + /// ); + /// + final List routes; + + /// An optional redirect function for this route. + /// + /// In the case that you like to make a redirection decision for a specific + /// route (or sub-route), you can do so by passing a redirect function to + /// the GoRoute constructor. + /// + /// For example: + /// ``` + /// final _router = GoRouter( + /// routes: [ + /// GoRoute( + /// path: '/', + /// redirect: (_) => '/family/${Families.data[0].id}', + /// ), + /// GoRoute( + /// path: '/family/:fid', + /// pageBuilder: (context, state) => ..., + /// ), + /// ], + /// ); + /// ``` + final GoRouterRedirect redirect; + + /// Match this route against a location. + RegExpMatch? matchPatternAsPrefix(String loc) => + _pathRE.matchAsPrefix(loc) as RegExpMatch?; + + /// Extract the path parameters from a match. + Map extractPathParams(RegExpMatch match) => + extractPathParameters(_pathParams, match); + + static String? _redirect(GoRouterState state) => null; + + static Widget _builder(BuildContext context, GoRouterState state) => + throw Exception( + 'GoRoute builder parameter not set\n' + 'See gorouter.dev/redirection#considerations for details', + ); +} diff --git a/packages/go_router/lib/src/go_route_information_parser.dart b/packages/go_router/lib/src/go_route_information_parser.dart new file mode 100644 index 000000000000..c38b5593f771 --- /dev/null +++ b/packages/go_router/lib/src/go_route_information_parser.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/foundation.dart'; +import 'package:flutter/widgets.dart'; + +/// GoRouter implementation of the RouteInformationParser base class +class GoRouteInformationParser extends RouteInformationParser { + /// for use by the Router architecture as part of the RouteInformationParser + @override + Future parseRouteInformation( + RouteInformation routeInformation, + ) => + // Use [SynchronousFuture] so that the initial url is processed + // synchronously and remove unwanted initial animations on deep-linking + SynchronousFuture(Uri.parse(routeInformation.location!)); + + /// for use by the Router architecture as part of the RouteInformationParser + @override + RouteInformation restoreRouteInformation(Uri configuration) => + RouteInformation(location: configuration.toString()); +} diff --git a/packages/go_router/lib/src/go_route_match.dart b/packages/go_router/lib/src/go_route_match.dart new file mode 100644 index 000000000000..d126fcb23e3e --- /dev/null +++ b/packages/go_router/lib/src/go_route_match.dart @@ -0,0 +1,148 @@ +// 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/foundation.dart'; + +import 'go_route.dart'; +import 'go_router_delegate.dart'; +import 'path_parser.dart'; + +/// Each GoRouteMatch instance represents an instance of a GoRoute for a +/// specific portion of a location. +class GoRouteMatch { + /// Constructor for GoRouteMatch, each instance represents an instance of a + /// GoRoute for a specific portion of a location. + GoRouteMatch({ + required this.route, + required this.subloc, + required this.fullpath, + required this.encodedParams, + required this.queryParams, + required this.extra, + required this.error, + this.pageKey, + }) : assert(subloc.startsWith('/')), + assert(Uri.parse(subloc).queryParameters.isEmpty), + assert(fullpath.startsWith('/')), + assert(Uri.parse(fullpath).queryParameters.isEmpty) { + if (kDebugMode) { + for (final p in encodedParams.entries) { + assert(p.value == Uri.encodeComponent(Uri.decodeComponent(p.value)), + 'encodedParams[${p.key}] is not encoded properly: "${p.value}"'); + } + } + } + + // ignore: public_member_api_docs + factory GoRouteMatch.matchNamed({ + required GoRoute route, + required String name, // e.g. person + required String fullpath, // e.g. /family/:fid/person/:pid + required Map params, // e.g. {'fid': 'f2', 'pid': 'p1'} + required Map queryParams, // e.g. {'from': '/family/f2'} + required Object? extra, + }) { + assert(route.name != null); + assert(route.name!.toLowerCase() == name.toLowerCase()); + + // check that we have all the params we need + final paramNames = []; + patternToRegExp(fullpath, paramNames); + for (final paramName in paramNames) { + if (!params.containsKey(paramName)) { + throw Exception('missing param "$paramName" for $fullpath'); + } + } + + // check that we have don't have extra params + for (final key in params.keys) { + if (!paramNames.contains(key)) { + throw Exception('unknown param "$key" for $fullpath'); + } + } + + final encodedParams = { + for (final param in params.entries) + param.key: Uri.encodeComponent(param.value) + }; + + final subloc = _locationFor(fullpath, encodedParams); + return GoRouteMatch( + route: route, + subloc: subloc, + fullpath: fullpath, + encodedParams: encodedParams, + queryParams: queryParams, + extra: extra, + error: null, + ); + } + + // ignore: public_member_api_docs + static GoRouteMatch? match({ + required GoRoute route, + required String restLoc, // e.g. person/p1 + required String parentSubloc, // e.g. /family/f2 + required String path, // e.g. person/:pid + required String fullpath, // e.g. /family/:fid/person/:pid + required Map queryParams, + required Object? extra, + }) { + assert(!path.contains('//')); + + final match = route.matchPatternAsPrefix(restLoc); + if (match == null) return null; + + final encodedParams = route.extractPathParams(match); + final pathLoc = _locationFor(path, encodedParams); + final subloc = GoRouterDelegate.fullLocFor(parentSubloc, pathLoc); + return GoRouteMatch( + route: route, + subloc: subloc, + fullpath: fullpath, + encodedParams: encodedParams, + queryParams: queryParams, + extra: extra, + error: null, + ); + } + + /// The matched route. + final GoRoute route; + + /// Matched sub-location. + final String subloc; // e.g. /family/f2 + + /// Matched full path. + final String fullpath; // e.g. /family/:fid + + /// Parameters for the matched route, URI-encoded. + final Map encodedParams; + + /// Query parameters for the matched route. + final Map queryParams; + + /// An extra object to pass along with the navigation. + final Object? extra; + + /// An exception if there was an error during matching. + final Exception? error; + + /// Optional value key of type string, to hold a unique reference to a page. + final ValueKey? pageKey; + + /// Parameters for the matched route, URI-decoded. + Map get decodedParams => { + for (final param in encodedParams.entries) + param.key: Uri.decodeComponent(param.value) + }; + + /// for use by the Router architecture as part of the GoRouteMatch + @override + String toString() => 'GoRouteMatch($fullpath, $encodedParams)'; + + /// expand a path w/ param slots using params, e.g. family/:fid => family/f1 + static String _locationFor(String pattern, Map params) => + patternToPath(pattern, params); +} diff --git a/packages/go_router/lib/src/go_router.dart b/packages/go_router/lib/src/go_router.dart new file mode 100644 index 000000000000..ccc3b4763230 --- /dev/null +++ b/packages/go_router/lib/src/go_router.dart @@ -0,0 +1,160 @@ +// 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/widgets.dart'; + +import 'go_route.dart'; +import 'go_route_information_parser.dart'; +import 'go_router_delegate.dart'; +import 'inherited_go_router.dart'; +import 'logging.dart'; +import 'path_strategy_nonweb.dart' + if (dart.library.html) 'path_strategy_web.dart'; +import 'typedefs.dart'; +import 'url_path_strategy.dart'; + +/// The top-level go router class. +/// +/// Create one of these to initialize your app's routing policy. +// ignore: prefer_mixin +class GoRouter extends ChangeNotifier with NavigatorObserver { + /// Default constructor to configure a GoRouter with a routes builder + /// and an error page builder. + GoRouter({ + required List routes, + GoRouterPageBuilder? errorPageBuilder, + GoRouterWidgetBuilder? errorBuilder, + GoRouterRedirect? redirect, + Listenable? refreshListenable, + int redirectLimit = 5, + bool routerNeglect = false, + String initialLocation = '/', + UrlPathStrategy? urlPathStrategy, + List? observers, + bool debugLogDiagnostics = false, + GoRouterNavigatorBuilder? navigatorBuilder, + String? restorationScopeId, + }) { + if (urlPathStrategy != null) setUrlPathStrategy(urlPathStrategy); + + setLogging(enabled: debugLogDiagnostics); + + routerDelegate = GoRouterDelegate( + routes: routes, + errorPageBuilder: errorPageBuilder, + errorBuilder: errorBuilder, + topRedirect: redirect ?? (_) => null, + redirectLimit: redirectLimit, + refreshListenable: refreshListenable, + routerNeglect: routerNeglect, + initUri: Uri.parse(initialLocation), + observers: [...observers ?? [], this], + debugLogDiagnostics: debugLogDiagnostics, + restorationScopeId: restorationScopeId, + // wrap the returned Navigator to enable GoRouter.of(context).go() et al, + // allowing the caller to wrap the navigator themselves + builderWithNav: (context, state, nav) => InheritedGoRouter( + goRouter: this, + child: navigatorBuilder?.call(context, state, nav) ?? nav, + ), + ); + } + + /// The route information parser used by the go router. + final routeInformationParser = GoRouteInformationParser(); + + /// The router delegate used by the go router. + late final GoRouterDelegate routerDelegate; + + /// Get the current location. + String get location => routerDelegate.currentConfiguration.toString(); + + /// Get a location from route name and parameters. + /// This is useful for redirecting to a named location. + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => + routerDelegate.namedLocation( + name, + params: params, + queryParams: queryParams, + ); + + /// Navigate to a URI location w/ optional query parameters, e.g. + /// `/family/f2/person/p1?color=blue` + void go(String location, {Object? extra}) => + routerDelegate.go(location, extra: extra); + + /// Navigate to a named route w/ optional parameters, e.g. + /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + /// Navigate to the named route. + void goNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + go( + namedLocation(name, params: params, queryParams: queryParams), + extra: extra, + ); + + /// Push a URI location onto the page stack w/ optional query parameters, e.g. + /// `/family/f2/person/p1?color=blue` + void push(String location, {Object? extra}) => + routerDelegate.push(location, extra: extra); + + /// Push a named route onto the page stack w/ optional parameters, e.g. + /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + void pushNamed( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) => + push( + namedLocation(name, params: params, queryParams: queryParams), + extra: extra, + ); + + /// Pop the top page off the GoRouter's page stack. + void pop() => routerDelegate.pop(); + + /// Refresh the route. + void refresh() => routerDelegate.refresh(); + + /// Set the app's URL path strategy (defaults to hash). call before runApp(). + static void setUrlPathStrategy(UrlPathStrategy strategy) => + setUrlPathStrategyImpl(strategy); + + /// Find the current GoRouter in the widget tree. + static GoRouter of(BuildContext context) { + final inherited = + context.dependOnInheritedWidgetOfExactType(); + assert(inherited != null, 'No GoRouter found in context'); + return inherited!.goRouter; + } + + /// The [Navigator] pushed `route`. + @override + void didPush(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] popped `route`. + @override + void didPop(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] removed `route`. + @override + void didRemove(Route route, Route? previousRoute) => + notifyListeners(); + + /// The [Navigator] replaced `oldRoute` with `newRoute`. + @override + void didReplace({Route? newRoute, Route? oldRoute}) => + notifyListeners(); +} diff --git a/packages/go_router/lib/src/go_router_cupertino.dart b/packages/go_router/lib/src/go_router_cupertino.dart new file mode 100644 index 000000000000..a356ffb77350 --- /dev/null +++ b/packages/go_router/lib/src/go_router_cupertino.dart @@ -0,0 +1,55 @@ +// 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: diagnostic_describe_all_properties + +import 'package:flutter/cupertino.dart'; +import '../go_router.dart'; + +/// Checks for CupertinoApp in the widget tree. +bool isCupertinoApp(Element elem) => + elem.findAncestorWidgetOfExactType() != null; + +/// Builds a Cupertino page. +CupertinoPage pageBuilderForCupertinoApp({ + required LocalKey key, + required String? name, + required Object? arguments, + required String restorationId, + required Widget child, +}) => + CupertinoPage( + name: name, + arguments: arguments, + key: key, + restorationId: restorationId, + child: child, + ); + +/// Default error page implementation for Cupertino. +class GoRouterCupertinoErrorScreen extends StatelessWidget { + /// Provide an exception to this page for it to be displayed. + const GoRouterCupertinoErrorScreen(this.error, {Key? key}) : super(key: key); + + /// The exception to be displayed. + final Exception? error; + + @override + Widget build(BuildContext context) => CupertinoPageScaffold( + navigationBar: + const CupertinoNavigationBar(middle: Text('Page Not Found')), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(error?.toString() ?? 'page not found'), + CupertinoButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/lib/src/go_router_delegate.dart b/packages/go_router/lib/src/go_router_delegate.dart new file mode 100644 index 000000000000..e4279dfd130d --- /dev/null +++ b/packages/go_router/lib/src/go_router_delegate.dart @@ -0,0 +1,881 @@ +// 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 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'custom_transition_page.dart'; +import 'go_route.dart'; +import 'go_route_match.dart'; +import 'go_router_cupertino.dart'; +import 'go_router_error_page.dart'; +import 'go_router_material.dart'; +import 'go_router_state.dart'; +import 'logging.dart'; +import 'typedefs.dart'; + +/// GoRouter implementation of the RouterDelegate base class. +class GoRouterDelegate extends RouterDelegate + with + PopNavigatorRouterDelegateMixin, + // ignore: prefer_mixin + ChangeNotifier { + /// Constructor for GoRouter's implementation of the + /// RouterDelegate base class. + GoRouterDelegate({ + required this.builderWithNav, + required this.routes, + required this.errorPageBuilder, + required this.errorBuilder, + required this.topRedirect, + required this.redirectLimit, + required this.refreshListenable, + required Uri initUri, + required this.observers, + required this.debugLogDiagnostics, + required this.routerNeglect, + this.restorationScopeId, + }) { + // check top-level route paths are valid + for (final route in routes) { + if (!route.path.startsWith('/')) { + throw Exception('top-level path must start with "/": ${route.path}'); + } + } + + // cache the set of named routes for fast lookup + _cacheNamedRoutes(routes, '', _namedMatches); + + // output known routes + _outputKnownRoutes(); + + // build the list of route matches + log.info('setting initial location $initUri'); + _go(initUri.toString()); + + // when the listener changes, refresh the route + refreshListenable?.addListener(refresh); + } + + /// Builder function for a go router with Navigator. + final GoRouterBuilderWithNav builderWithNav; + + /// List of top level routes used by the go router delegate. + final List routes; + + /// Error page builder for the go router delegate. + final GoRouterPageBuilder? errorPageBuilder; + + /// Error widget builder for the go router delegate. + final GoRouterWidgetBuilder? errorBuilder; + + /// Top level page redirect. + final GoRouterRedirect topRedirect; + + /// The limit for the number of consecutive redirects. + final int redirectLimit; + + /// Listenable used to cause the router to refresh it's route. + final Listenable? refreshListenable; + + /// NavigatorObserver used to receive change notifications when + /// navigation changes. + final List observers; + + /// Set to true to log diagnostic info for your routes. + final bool debugLogDiagnostics; + + /// Set to true to disable creating history entries on the web. + final bool routerNeglect; + + /// Restoration ID to save and restore the state of the navigator, including + /// its history. + final String? restorationScopeId; + + final _key = GlobalKey(); + final List _matches = []; + final _namedMatches = {}; + final _pushCounts = {}; + + void _cacheNamedRoutes( + List routes, + String parentFullpath, + Map namedFullpaths, + ) { + for (final route in routes) { + final fullpath = fullLocFor(parentFullpath, route.path); + + if (route.name != null) { + final name = route.name!.toLowerCase(); + if (namedFullpaths.containsKey(name)) { + throw Exception('duplication fullpaths for name "$name":' + '${namedFullpaths[name]!.fullpath}, $fullpath'); + } + + // we only have a partial match until we have a location; + // we're really only caching the route and fullpath at this point + final match = GoRouteMatch( + route: route, + subloc: '/TBD', + fullpath: fullpath, + encodedParams: {}, + queryParams: {}, + extra: null, + error: null, + ); + + namedFullpaths[name] = match; + } + + if (route.routes.isNotEmpty) { + _cacheNamedRoutes(route.routes, fullpath, namedFullpaths); + } + } + } + + /// Get a location from route name and parameters. + /// This is useful for redirecting to a named location. + String namedLocation( + String name, { + required Map params, + required Map queryParams, + }) { + log.info('getting location for name: ' + '"$name"' + '${params.isEmpty ? '' : ', params: $params'}' + '${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}'); + + // find route and build up the full path along the way + final match = _getNameRouteMatch( + name.toLowerCase(), // case-insensitive name matching + params: params, + queryParams: queryParams, + ); + if (match == null) throw Exception('unknown route name: $name'); + + assert(identical(match.queryParams, queryParams)); + return _addQueryParams(match.subloc, queryParams); + } + + /// Navigate to the given location. + void go(String location, {Object? extra}) { + log.info('going to $location'); + _go(location, extra: extra); + notifyListeners(); + } + + /// Push the given location onto the page stack + void push(String location, {Object? extra}) { + log.info('pushing $location'); + _push(location, extra: extra); + notifyListeners(); + } + + /// Pop the top page off the GoRouter's page stack. + void pop() { + _matches.remove(_matches.last); + if (_matches.isEmpty) { + throw Exception( + 'have popped the last page off of the stack; ' + 'there are no pages left to show', + ); + } + notifyListeners(); + } + + /// Refresh the current location, including re-evaluating redirections. + void refresh() { + log.info('refreshing $location'); + _go(location, extra: _matches.last.extra); + notifyListeners(); + } + + /// Get the current location, e.g. /family/f2/person/p1 + String get location => + _addQueryParams(_matches.last.subloc, _matches.last.queryParams); + + /// For internal use; visible for testing only. + @visibleForTesting + List get matches => _matches; + + /// Dispose resources held by the router delegate. + @override + void dispose() { + refreshListenable?.removeListener(refresh); + super.dispose(); + } + + /// For use by the Router architecture as part of the RouterDelegate. + @override + GlobalKey get navigatorKey => _key; + + /// For use by the Router architecture as part of the RouterDelegate. + @override + Uri get currentConfiguration => Uri.parse(location); + + /// For use by the Router architecture as part of the RouterDelegate. + @override + Widget build(BuildContext context) => _builder(context, _matches); + + /// For use by the Router architecture as part of the RouterDelegate. + @override + Future setInitialRoutePath(Uri configuration) { + // if the initial location is /, then use the dev initial location; + // otherwise, we're cruising to a deep link, so ignore dev initial location + final config = configuration.toString(); + if (config == '/') { + _go(location); + } else { + log.info('deep linking to $config'); + _go(config); + } + + // Use [SynchronousFuture] so that the initial url is processed + // synchronously and remove unwanted initial animations on deep-linking + return SynchronousFuture(null); + } + + /// For use by the Router architecture as part of the RouterDelegate. + @override + Future setNewRoutePath(Uri configuration) async { + final config = configuration.toString(); + log.info('going to $config'); + _go(config); + } + + void _go(String location, {Object? extra}) { + final matches = _getLocRouteMatchesWithRedirects(location, extra: extra); + assert(matches.isNotEmpty); + + // replace the stack of matches w/ the new ones + _matches + ..clear() + ..addAll(matches); + } + + void _push(String location, {Object? extra}) { + final matches = _getLocRouteMatchesWithRedirects(location, extra: extra); + assert(matches.isNotEmpty); + final top = matches.last; + + // remap the pageKey so allow any number of the same page on the stack + final fullpath = top.fullpath; + final count = (_pushCounts[fullpath] ?? 0) + 1; + _pushCounts[fullpath] = count; + final pageKey = ValueKey('$fullpath-p$count'); + final match = GoRouteMatch( + route: top.route, + subloc: top.subloc, + fullpath: top.fullpath, + encodedParams: top.encodedParams, + queryParams: top.queryParams, + extra: extra, + error: null, + pageKey: pageKey, + ); + + // add a new match onto the stack of matches + assert(matches.isNotEmpty); + _matches.add(match); + } + + List _getLocRouteMatchesWithRedirects( + String location, { + required Object? extra, + }) { + // start redirecting from the initial location + List matches; + + try { + // watch redirects for loops + final redirects = [_canonicalUri(location)]; + bool redirected(String? redir) { + if (redir == null) return false; + + if (Uri.tryParse(redir) == null) { + throw Exception('invalid redirect: $redir'); + } + + if (redirects.contains(redir)) { + redirects.add(redir); + final msg = 'redirect loop detected: ${redirects.join(' => ')}'; + throw Exception(msg); + } + + redirects.add(redir); + if (redirects.length - 1 > redirectLimit) { + final msg = 'too many redirects: ${redirects.join(' => ')}'; + throw Exception(msg); + } + + log.info('redirecting to $redir'); + return true; + } + + // keep looping till we're done redirecting + for (;;) { + final loc = redirects.last; + + // check for top-level redirect + final uri = Uri.parse(loc); + if (redirected( + topRedirect( + GoRouterState( + this, + location: loc, + name: null, // no name available at the top level + // trim the query params off the subloc to match route.redirect + subloc: uri.path, + // pass along the query params 'cuz that's all we have right now + queryParams: uri.queryParameters, + ), + ), + )) continue; + + // get stack of route matches + matches = _getLocRouteMatches(loc, extra: extra); + + var params = {}; + for (final match in matches) { + // merge new params to keep params from previously matched paths, e.g. + // /family/:fid/person/:pid provides fid and pid to person/:pid + params = {...params, ...match.decodedParams}; + } + + // check top route for redirect + final top = matches.last; + if (redirected( + top.route.redirect( + GoRouterState( + this, + location: loc, + subloc: top.subloc, + name: top.route.name, + path: top.route.path, + fullpath: top.fullpath, + params: params, + queryParams: top.queryParams, + extra: extra, + ), + ), + )) continue; + + // let Router know to update the address bar + // (the initial route is not a redirect) + if (redirects.length > 1) notifyListeners(); + + // no more redirects! + break; + } + + // note that we need to catch it this way to get all the info, e.g. the + // file/line info for an error in an inline function impl, e.g. an inline + // `redirect` impl + // ignore: avoid_catches_without_on_clauses + } catch (err, stack) { + log.severe('Exception during GoRouter navigation', err, stack); + + // create a match that routes to the error page + final error = err is Exception ? err : Exception(err); + final uri = Uri.parse(location); + matches = [ + GoRouteMatch( + subloc: uri.path, + fullpath: uri.path, + encodedParams: {}, + queryParams: uri.queryParameters, + extra: null, + error: error, + route: GoRoute( + path: location, + pageBuilder: (context, state) => _errorPageBuilder( + context, + GoRouterState( + this, + location: state.location, + subloc: state.subloc, + name: state.name, + path: state.path, + error: error, + fullpath: state.path, + params: state.params, + queryParams: state.queryParams, + extra: state.extra, + ), + ), + ), + ), + ]; + } + + assert(matches.isNotEmpty); + return matches; + } + + List _getLocRouteMatches( + String location, { + Object? extra, + }) { + final uri = Uri.parse(location); + final matchStacks = _getLocRouteMatchStacks( + loc: uri.path, + restLoc: uri.path, + routes: routes, + parentFullpath: '', + parentSubloc: '', + queryParams: uri.queryParameters, + extra: extra, + ).toList(); + + if (matchStacks.isEmpty) { + throw Exception('no routes for location: $location'); + } + + if (matchStacks.length > 1) { + final sb = StringBuffer() + ..writeln('too many routes for location: $location'); + + for (final stack in matchStacks) { + sb.writeln('\t${stack.map((m) => m.route.path).join(' => ')}'); + } + + throw Exception(sb.toString()); + } + + if (kDebugMode) { + assert(matchStacks.length == 1); + final match = matchStacks.first.last; + final loc1 = _addQueryParams(match.subloc, match.queryParams); + final uri2 = Uri.parse(location); + final loc2 = _addQueryParams(uri2.path, uri2.queryParameters); + + // NOTE: match the lower case, since subloc is canonicalized to match the + // path case whereas the location can be any case + assert(loc1.toLowerCase() == loc2.toLowerCase(), '$loc1 != $loc2'); + } + + return matchStacks.first; + } + + /// turns a list of routes into a list of routes match stacks for the location + /// e.g. routes: [ + /// / + /// family/:fid + /// /login + /// ] + /// + /// loc: / + /// stacks: [ + /// matches: [ + /// match(route.path=/, loc=/) + /// ] + /// ] + /// + /// loc: /login + /// stacks: [ + /// matches: [ + /// match(route.path=/login, loc=login) + /// ] + /// ] + /// + /// loc: /family/f2 + /// stacks: [ + /// matches: [ + /// match(route.path=/, loc=/), + /// match(route.path=family/:fid, loc=family/f2, params=[fid=f2]) + /// ] + /// ] + /// + /// loc: /family/f2/person/p1 + /// stacks: [ + /// matches: [ + /// match(route.path=/, loc=/), + /// match(route.path=family/:fid, loc=family/f2, params=[fid=f2]) + /// match(route.path=person/:pid, loc=person/p1, params=[fid=f2, pid=p1]) + /// ] + /// ] + /// + /// A stack count of 0 means there's no match. + /// A stack count of >1 means there's a malformed set of routes. + /// + /// NOTE: Uses recursion, which is why _getLocRouteMatchStacks calls this + /// function and does the actual error checking, using the returned stacks to + /// provide better errors + static Iterable> _getLocRouteMatchStacks({ + required String loc, + required String restLoc, + required String parentSubloc, + required List routes, + required String parentFullpath, + required Map queryParams, + required Object? extra, + }) sync* { + // find the set of matches at this level of the tree + for (final route in routes) { + final fullpath = fullLocFor(parentFullpath, route.path); + final match = GoRouteMatch.match( + route: route, + restLoc: restLoc, + parentSubloc: parentSubloc, + path: route.path, + fullpath: fullpath, + queryParams: queryParams, + extra: extra, + ); + if (match == null) continue; + + // if we have a complete match, then return the matched route + // NOTE: need a lower case match because subloc is canonicalized to match + // the path case whereas the location can be of any case and still match + if (match.subloc.toLowerCase() == loc.toLowerCase()) { + yield [match]; + continue; + } + + // if we have a partial match but no sub-routes, bail + if (route.routes.isEmpty) continue; + + // otherwise recurse + final childRestLoc = + loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1)); + assert(loc.startsWith(match.subloc)); + assert(restLoc.isNotEmpty); + + // if there's no sub-route matches, then we don't have a match for this + // location + final subRouteMatchStacks = _getLocRouteMatchStacks( + loc: loc, + restLoc: childRestLoc, + parentSubloc: match.subloc, + routes: route.routes, + parentFullpath: fullpath, + queryParams: queryParams, + extra: extra, + ).toList(); + if (subRouteMatchStacks.isEmpty) continue; + + // add the match to each of the sub-route match stacks and return them + for (final stack in subRouteMatchStacks) { + yield [match, ...stack]; + } + } + } + + GoRouteMatch? _getNameRouteMatch( + String name, { + Map params = const {}, + Map queryParams = const {}, + Object? extra, + }) { + final partialMatch = _namedMatches[name]; + return partialMatch == null + ? null + : GoRouteMatch.matchNamed( + name: name, + route: partialMatch.route, + fullpath: partialMatch.fullpath, + params: params, + queryParams: queryParams, + extra: extra, + ); + } + + // e.g. + // parentFullLoc: '', path => '/' + // parentFullLoc: '/', path => 'family/:fid' => '/family/:fid' + // parentFullLoc: '/', path => 'family/f2' => '/family/f2' + // parentFullLoc: '/family/f2', path => 'parent/p1' => '/family/f2/person/p1' + // ignore: public_member_api_docs + static String fullLocFor(String parentFullLoc, String path) { + // at the root, just return the path + if (parentFullLoc.isEmpty) { + assert(path.startsWith('/')); + assert(path == '/' || !path.endsWith('/')); + return path; + } + + // not at the root, so append the parent path + assert(path.isNotEmpty); + assert(!path.startsWith('/')); + assert(!path.endsWith('/')); + return '${parentFullLoc == '/' ? '' : parentFullLoc}/$path'; + } + + Widget _builder(BuildContext context, Iterable matches) { + List>? pages; + Exception? error; + + try { + // build the stack of pages + if (routerNeglect) { + Router.neglect( + context, + () => pages = getPages(context, matches.toList()).toList(), + ); + } else { + pages = getPages(context, matches.toList()).toList(); + } + + // note that we need to catch it this way to get all the info, e.g. the + // file/line info for an error in an inline function impl, e.g. an inline + // `redirect` impl + // ignore: avoid_catches_without_on_clauses + } catch (err, stack) { + log.severe('Exception during GoRouter navigation', err, stack); + + // if there's an error, show an error page + error = err is Exception ? err : Exception(err); + final uri = Uri.parse(location); + pages = [ + _errorPageBuilder( + context, + GoRouterState( + this, + location: location, + subloc: uri.path, + name: null, + queryParams: uri.queryParameters, + error: error, + ), + ), + ]; + } + + // we should've set pages to something by now + assert(pages != null); + + // pass either the match error or the build error along to the navigator + // builder, preferring the match error + if (matches.length == 1 && matches.first.error != null) { + error = matches.first.error; + } + + // wrap the returned Navigator to enable GoRouter.of(context).go() + final uri = Uri.parse(location); + return builderWithNav( + context, + GoRouterState( + this, + location: location, + name: null, // no name available at the top level + // trim the query params off the subloc to match route.redirect + subloc: uri.path, + // pass along the query params 'cuz that's all we have right now + queryParams: uri.queryParameters, + // pass along the error, if there is one + error: error, + ), + Navigator( + restorationScopeId: restorationScopeId, + key: _key, // needed to enable Android system Back button + pages: pages!, + observers: observers, + onPopPage: (route, dynamic result) { + if (!route.didPop(result)) return false; + pop(); + return true; + }, + ), + ); + } + + /// Get the stack of sub-routes that matches the location and turn it into a + /// stack of pages, e.g. + /// routes: [ + /// / + /// family/:fid + /// person/:pid + /// /login + /// ] + /// + /// loc: / + /// pages: [ HomePage()] + /// + /// loc: /login + /// pages: [ LoginPage() ] + /// + /// loc: /family/f2 + /// pages: [ HomePage(), FamilyPage(f2) ] + /// + /// loc: /family/f2/person/p1 + /// pages: [ HomePage(), FamilyPage(f2), PersonPage(f2, p1) ] + @visibleForTesting + Iterable> getPages( + BuildContext context, + List matches, + ) sync* { + assert(matches.isNotEmpty); + + var params = {}; + for (final match in matches) { + // merge new params to keep params from previously matched paths, e.g. + // /family/:fid/person/:pid provides fid and pid to person/:pid + params = {...params, ...match.decodedParams}; + + // get a page from the builder and associate it with a sub-location + final state = GoRouterState( + this, + location: location, + subloc: match.subloc, + name: match.route.name, + path: match.route.path, + fullpath: match.fullpath, + params: params, + queryParams: match.queryParams, + extra: match.extra, + pageKey: match.pageKey, // push() remaps the page key for uniqueness + ); + + yield match.route.pageBuilder != null + ? match.route.pageBuilder!(context, state) + : _pageBuilder(context, state, match.route.builder); + } + } + + Page Function({ + required LocalKey key, + required String? name, + required Object? arguments, + required String restorationId, + required Widget child, + })? _pageBuilderForAppType; + + Widget Function( + BuildContext context, + GoRouterState state, + )? _errorBuilderForAppType; + + void _cacheAppType(BuildContext context) { + // cache app type-specific page and error builders + if (_pageBuilderForAppType == null) { + assert(_errorBuilderForAppType == null); + + // can be null during testing + final elem = context is Element ? context : null; + + if (elem != null && isMaterialApp(elem)) { + log.info('MaterialApp found'); + _pageBuilderForAppType = pageBuilderForMaterialApp; + _errorBuilderForAppType = + (c, s) => GoRouterMaterialErrorScreen(s.error); + } else if (elem != null && isCupertinoApp(elem)) { + log.info('CupertinoApp found'); + _pageBuilderForAppType = pageBuilderForCupertinoApp; + _errorBuilderForAppType = + (c, s) => GoRouterCupertinoErrorScreen(s.error); + } else { + log.info('WidgetsApp assumed'); + _pageBuilderForAppType = pageBuilderForWidgetApp; + _errorBuilderForAppType = (c, s) => GoRouterErrorScreen(s.error); + } + } + + assert(_pageBuilderForAppType != null); + assert(_errorBuilderForAppType != null); + } + + // builds the page based on app type, i.e. MaterialApp vs. CupertinoApp + Page _pageBuilder( + BuildContext context, + GoRouterState state, + GoRouterWidgetBuilder builder, + ) { + // build the page based on app type + _cacheAppType(context); + return _pageBuilderForAppType!( + key: state.pageKey, + name: state.name ?? state.fullpath, + arguments: {...state.params, ...state.queryParams}, + restorationId: state.pageKey.value, + child: builder(context, state), + ); + } + + /// Builds a page without any transitions. + Page pageBuilderForWidgetApp({ + required LocalKey key, + required String? name, + required Object? arguments, + required String restorationId, + required Widget child, + }) => + NoTransitionPage( + name: name, + arguments: arguments, + key: key, + restorationId: restorationId, + child: child, + ); + + Page _errorPageBuilder( + BuildContext context, + GoRouterState state, + ) { + // if the error page builder is provided, use that; otherwise, if the error + // builder is provided, wrap that in an app-specific page, e.g. + // MaterialPage; finally, if nothing is provided, use a default error page + // wrapped in the app-specific page, e.g. + // MaterialPage(GoRouterMaterialErrorPage(...)) + _cacheAppType(context); + return errorPageBuilder != null + ? errorPageBuilder!(context, state) + : _pageBuilder( + context, + state, + errorBuilder ?? _errorBuilderForAppType!, + ); + } + + void _outputKnownRoutes() { + log.info('known full paths for routes:'); + _outputFullPathsFor(routes, '', 0); + + if (_namedMatches.isNotEmpty) { + log.info('known full paths for route names:'); + for (final e in _namedMatches.entries) { + log.info(' ${e.key} => ${e.value.fullpath}'); + } + } + } + + void _outputFullPathsFor( + List routes, + String parentFullpath, + int depth, + ) { + for (final route in routes) { + final fullpath = fullLocFor(parentFullpath, route.path); + log.info(' => ${''.padLeft(depth * 2)}$fullpath'); + _outputFullPathsFor(route.routes, fullpath, depth + 1); + } + } + + static String _canonicalUri(String loc) { + var canon = Uri.parse(loc).toString(); + canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon; + + // remove trailing slash except for when you shouldn't, e.g. + // /profile/ => /profile + // / => / + // /login?from=/ => login?from=/ + canon = canon.endsWith('/') && canon != '/' && !canon.contains('?') + ? canon.substring(0, canon.length - 1) + : canon; + + // /login/?from=/ => /login?from=/ + // /?from=/ => /?from=/ + canon = canon.replaceFirst('/?', '?', 1); + + return canon; + } + + static String _addQueryParams(String loc, Map queryParams) { + final uri = Uri.parse(loc); + assert(uri.queryParameters.isEmpty); + return _canonicalUri( + Uri(path: uri.path, queryParameters: queryParams).toString()); + } +} diff --git a/packages/go_router/lib/src/go_router_error_page.dart b/packages/go_router/lib/src/go_router_error_page.dart new file mode 100644 index 000000000000..230021920c47 --- /dev/null +++ b/packages/go_router/lib/src/go_router_error_page.dart @@ -0,0 +1,81 @@ +// 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: diagnostic_describe_all_properties + +import 'package:flutter/widgets.dart'; +import '../go_router.dart'; + +/// Default error page implementation for WidgetsApp. +class GoRouterErrorScreen extends StatelessWidget { + /// Provide an exception to this page for it to be displayed. + const GoRouterErrorScreen(this.error, {Key? key}) : super(key: key); + + /// The exception to be displayed. + final Exception? error; + + static const _white = Color(0xFFFFFFFF); + + @override + Widget build(BuildContext context) => SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Page Not Found', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text(error?.toString() ?? 'page not found'), + const SizedBox(height: 16), + _Button( + onPressed: () => context.go('/'), + child: const Text( + 'Go to home page', + style: TextStyle(color: _white), + ), + ), + ], + ), + ), + ); +} + +class _Button extends StatefulWidget { + const _Button({ + required this.onPressed, + required this.child, + Key? key, + }) : super(key: key); + + final VoidCallback onPressed; + final Widget child; + + @override + State<_Button> createState() => _ButtonState(); +} + +class _ButtonState extends State<_Button> { + late final Color _color; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _color = (context as Element) + .findAncestorWidgetOfExactType() + ?.color ?? + const Color(0xFF2196F3); // blue + } + + @override + Widget build(BuildContext context) => GestureDetector( + onTap: widget.onPressed, + child: Container( + padding: const EdgeInsets.all(8), + color: _color, + child: widget.child, + ), + ); +} diff --git a/packages/go_router/lib/src/go_router_material.dart b/packages/go_router/lib/src/go_router_material.dart new file mode 100644 index 000000000000..7a949ded7359 --- /dev/null +++ b/packages/go_router/lib/src/go_router_material.dart @@ -0,0 +1,54 @@ +// 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: diagnostic_describe_all_properties + +import 'package:flutter/material.dart'; +import '../go_router.dart'; + +/// Checks for MaterialApp in the widget tree. +bool isMaterialApp(Element elem) => + elem.findAncestorWidgetOfExactType() != null; + +/// Builds a Material page. +MaterialPage pageBuilderForMaterialApp({ + required LocalKey key, + required String? name, + required Object? arguments, + required String restorationId, + required Widget child, +}) => + MaterialPage( + name: name, + arguments: arguments, + key: key, + restorationId: restorationId, + child: child, + ); + +/// Default error page implementation for Material. +class GoRouterMaterialErrorScreen extends StatelessWidget { + /// Provide an exception to this page for it to be displayed. + const GoRouterMaterialErrorScreen(this.error, {Key? key}) : super(key: key); + + /// The exception to be displayed. + final Exception? error; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Page Not Found')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText(error?.toString() ?? 'page not found'), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ), + ); +} diff --git a/packages/go_router/lib/src/go_router_refresh_stream.dart b/packages/go_router/lib/src/go_router_refresh_stream.dart new file mode 100644 index 000000000000..5452252d0cc9 --- /dev/null +++ b/packages/go_router/lib/src/go_router_refresh_stream.dart @@ -0,0 +1,44 @@ +// 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 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'go_router.dart'; + +/// This class can be used to make `refreshListenable` react to events in the +/// the provided stream. This allows you to listen to stream based state +/// management solutions like for example BLoC. +/// +/// {@tool snippet} +/// Typical usage is as follows: +/// +/// ```dart +/// GoRouter( +/// refreshListenable: GoRouterRefreshStream(stream), +/// ); +/// ``` +/// {@end-tool} +class GoRouterRefreshStream extends ChangeNotifier { + /// Creates a [GoRouterRefreshStream]. + /// + /// Every time the [stream] receives an event the [GoRouter] will refresh its + /// current route. + GoRouterRefreshStream(Stream stream) { + notifyListeners(); + _subscription = stream.asBroadcastStream().listen( + (dynamic _) => notifyListeners(), + ); + } + + late final StreamSubscription _subscription; + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} diff --git a/packages/go_router/lib/src/go_router_state.dart b/packages/go_router/lib/src/go_router_state.dart new file mode 100644 index 000000000000..ea3ac9873a23 --- /dev/null +++ b/packages/go_router/lib/src/go_router_state.dart @@ -0,0 +1,72 @@ +// 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/foundation.dart'; + +import 'go_router_delegate.dart'; + +/// The route state during routing. +class GoRouterState { + /// Default constructor for creating route state during routing. + GoRouterState( + this._delegate, { + required this.location, + required this.subloc, + required this.name, + this.path, + this.fullpath, + this.params = const {}, + this.queryParams = const {}, + this.extra, + this.error, + ValueKey? pageKey, + }) : pageKey = pageKey ?? + ValueKey(error != null + ? 'error' + : fullpath != null && fullpath.isNotEmpty + ? fullpath + : subloc), + assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty); + + final GoRouterDelegate _delegate; + + /// The full location of the route, e.g. /family/f2/person/p1 + final String location; + + /// The location of this sub-route, e.g. /family/f2 + final String subloc; + + /// The optional name of the route. + final String? name; + + /// The path to this sub-route, e.g. family/:fid + final String? path; + + /// The full path to this sub-route, e.g. /family/:fid + final String? fullpath; + + /// The parameters for this sub-route, e.g. {'fid': 'f2'} + final Map params; + + /// The query parameters for the location, e.g. {'from': '/family/f2'} + final Map queryParams; + + /// An extra object to pass along with the navigation. + final Object? extra; + + /// The error associated with this sub-route. + final Exception? error; + + /// A unique string key for this sub-route, e.g. ValueKey('/family/:fid') + final ValueKey pageKey; + + /// Get a location from route name and parameters. + /// This is useful for redirecting to a named location. + String namedLocation( + String name, { + Map params = const {}, + Map queryParams = const {}, + }) => + _delegate.namedLocation(name, params: params, queryParams: queryParams); +} diff --git a/packages/go_router/lib/src/inherited_go_router.dart b/packages/go_router/lib/src/inherited_go_router.dart new file mode 100644 index 000000000000..e5c9d73b70bc --- /dev/null +++ b/packages/go_router/lib/src/inherited_go_router.dart @@ -0,0 +1,38 @@ +// 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/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'go_router.dart'; + +/// GoRouter implementation of InheritedWidget. +/// +/// Used for to find the current GoRouter in the widget tree. This is useful +/// when routing from anywhere in your app. +class InheritedGoRouter extends InheritedWidget { + /// Default constructor for the inherited go router. + const InheritedGoRouter({ + required Widget child, + required this.goRouter, + Key? key, + }) : super(child: child, key: key); + + /// The [GoRouter] that is made available to the widget tree. + final GoRouter goRouter; + + /// Used by the Router architecture as part of the InheritedWidget. + @override + // ignore: prefer_expression_function_bodies + bool updateShouldNotify(covariant InheritedWidget oldWidget) { + // avoid rebuilding the widget tree if the router has not changed + return false; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('goRouter', goRouter)); + } +} diff --git a/packages/go_router/lib/src/logging.dart b/packages/go_router/lib/src/logging.dart new file mode 100644 index 000000000000..9e8adb58e672 --- /dev/null +++ b/packages/go_router/lib/src/logging.dart @@ -0,0 +1,47 @@ +// 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 'dart:async'; +import 'dart:developer' as developer; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +/// The logger for this package. +final log = Logger('GoRouter'); + +StreamSubscription? _subscription; + +/// Forwards diagnostic messages to the dart:developer log() API. +void setLogging({bool enabled = false}) { + _subscription?.cancel(); + if (!enabled) return; + + _subscription = log.onRecord.listen((e) { + // use `dumpErrorToConsole` for severe messages to ensure that severe + // exceptions are formatted consistently with other Flutter examples and + // avoids printing duplicate exceptions + if (e.level >= Level.SEVERE) { + final error = e.error; + FlutterError.dumpErrorToConsole( + FlutterErrorDetails( + exception: error is Exception ? error : Exception(error), + stack: e.stackTrace, + library: e.loggerName, + context: ErrorDescription(e.message), + ), + ); + } else { + developer.log( + e.message, + time: e.time, + sequenceNumber: e.sequenceNumber, + level: e.level.value, + name: e.loggerName, + zone: e.zone, + error: e.error, + stackTrace: e.stackTrace, + ); + } + }); +} diff --git a/packages/go_router/lib/src/path_parser.dart b/packages/go_router/lib/src/path_parser.dart new file mode 100644 index 000000000000..bb4d2e429c71 --- /dev/null +++ b/packages/go_router/lib/src/path_parser.dart @@ -0,0 +1,96 @@ +// 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. + +final _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?'); + +/// Converts a [pattern] such as `/user/:id` into [RegExp]. +/// +/// The path parameters can be specified by prefixing them with `:`. The +/// `parameters` are used for storing path parameter names. +/// +/// +/// For example: +/// +/// `pattern` = `/user/:id/book/:bookId` +/// +/// The `parameters` would contain `['id', 'bookId']` as a result of calling +/// this method. +/// +/// To extract the path parameter values from a [RegExpMatch], pass the +/// [RegExpMatch] into [extractPathParameters] with the `parameters` that are +/// used for generating the [RegExp]. +RegExp patternToRegExp(String pattern, List parameters) { + final StringBuffer buffer = StringBuffer('^'); + int start = 0; + for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) { + if (match.start > start) { + buffer.write(RegExp.escape(pattern.substring(start, match.start))); + } + final String name = match[1]!; + final String? optionalPattern = match[2]; + final String regex = optionalPattern != null + ? _escapeGroup(optionalPattern, name) + : '(?<$name>[^/]+)'; + buffer.write(regex); + parameters.add(name); + start = match.end; + } + + if (start < pattern.length) { + buffer.write(RegExp.escape(pattern.substring(start))); + } + + if (!pattern.endsWith('/')) { + buffer.write(r'(?=/|$)'); + } + return RegExp(buffer.toString(), caseSensitive: false); +} + +String _escapeGroup(String group, String name) { + final String escapedGroup = group.replaceFirstMapped( + RegExp(r'[:=!]'), (Match match) => '\\${match[0]}'); + return '(?<$name>$escapedGroup)'; +} + +/// Reconstructs the full path from a [pattern] and path parameters. +/// +/// This is useful for restoring the original path from a [RegExpMatch]. +/// +/// For example, A path matched a [RegExp] returned from [patternToRegExp] and +/// produced a [RegExpMatch]. To reconstruct the path from the match, one +/// can follow these steps: +/// +/// 1. Get the `pathParameters` by calling [extractPathParameters] with the +/// [RegExpMatch] and the parameters used for generating the [RegExp]. +/// 2. Call [patternToPath] with the `pathParameters` from the first step and +/// the original `pattern` used for generating the [RegExp]. +String patternToPath(String pattern, Map pathParameters) { + final StringBuffer buffer = StringBuffer(''); + int start = 0; + for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) { + if (match.start > start) { + buffer.write(pattern.substring(start, match.start)); + } + final String name = match[1]!; + buffer.write(pathParameters[name]); + start = match.end; + } + + if (start < pattern.length) { + buffer.write(pattern.substring(start)); + } + return buffer.toString(); +} + +/// Extracts arguments from the `match` and maps them by parameter name. +/// +/// The [parameters] should originate from the call to [patternToRegExp] that +/// creates the [RegExp]. +Map extractPathParameters( + List parameters, RegExpMatch match) { + return { + for (var i = 0; i < parameters.length; ++i) + parameters[i]: match.namedGroup(parameters[i])! + }; +} diff --git a/packages/go_router/lib/src/path_strategy_nonweb.dart b/packages/go_router/lib/src/path_strategy_nonweb.dart new file mode 100644 index 000000000000..3b9240031f46 --- /dev/null +++ b/packages/go_router/lib/src/path_strategy_nonweb.dart @@ -0,0 +1,10 @@ +// 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 'url_path_strategy.dart'; + +/// no-op implementation of the URL path strategy for non-web target platforms +void setUrlPathStrategyImpl(UrlPathStrategy strategy) { + // no-op +} diff --git a/packages/go_router/lib/src/path_strategy_web.dart b/packages/go_router/lib/src/path_strategy_web.dart new file mode 100644 index 000000000000..ede76ec4350d --- /dev/null +++ b/packages/go_router/lib/src/path_strategy_web.dart @@ -0,0 +1,16 @@ +// 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. + +// from https://flutter.dev/docs/development/ui/navigation/url-strategies +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'url_path_strategy.dart'; + +/// forwarding implementation of the URL path strategy for the web target +/// platform +void setUrlPathStrategyImpl(UrlPathStrategy strategy) { + setUrlStrategy(strategy == UrlPathStrategy.path + ? PathUrlStrategy() + : const HashUrlStrategy()); +} diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart new file mode 100644 index 000000000000..bec6eaa0fcfb --- /dev/null +++ b/packages/go_router/lib/src/typedefs.dart @@ -0,0 +1,43 @@ +// 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/widgets.dart'; + +import 'go_route_match.dart'; +import 'go_router_state.dart'; + +/// Signature of a go router builder function with matchers. +typedef GoRouterBuilderWithMatches = Widget Function( + BuildContext context, + Iterable matches, +); + +/// Signature of a go router builder function with navigator. +typedef GoRouterBuilderWithNav = Widget Function( + BuildContext context, + GoRouterState state, + Navigator navigator, +); + +/// The signature of the page builder callback for a matched GoRoute. +typedef GoRouterPageBuilder = Page Function( + BuildContext context, + GoRouterState state, +); + +/// The signature of the widget builder callback for a matched GoRoute. +typedef GoRouterWidgetBuilder = Widget Function( + BuildContext context, + GoRouterState state, +); + +/// The signature of the redirect callback. +typedef GoRouterRedirect = String? Function(GoRouterState state); + +/// The signature of the navigatorBuilder callback. +typedef GoRouterNavigatorBuilder = Widget Function( + BuildContext context, + GoRouterState state, + Widget child, +); diff --git a/packages/go_router/lib/src/url_path_strategy.dart b/packages/go_router/lib/src/url_path_strategy.dart new file mode 100644 index 000000000000..92a12f0d2dee --- /dev/null +++ b/packages/go_router/lib/src/url_path_strategy.dart @@ -0,0 +1,12 @@ +// 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. + +/// The path strategy for use in GoRouter.setUrlPathStrategy. +enum UrlPathStrategy { + /// Use hash url strategy. + hash, + + /// Use path url strategy. + path, +} diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml new file mode 100644 index 000000000000..467e3beeb0d7 --- /dev/null +++ b/packages/go_router/pubspec.yaml @@ -0,0 +1,22 @@ +name: go_router +description: A declarative router for Flutter based on Navigation 2 supporting + deep linking, data-driven routes and more +version: 3.0.2 +repository: https://github.com/flutter/packages/tree/main/packages/go_router +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + collection: ^1.15.0 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + logging: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart new file mode 100644 index 000000000000..1c702f3b0635 --- /dev/null +++ b/packages/go_router/test/go_router_test.dart @@ -0,0 +1,1569 @@ +// 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: cascade_invocations, diagnostic_describe_all_properties + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/src/foundation/diagnostics.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:go_router/src/go_route_match.dart'; +import 'package:logging/logging.dart'; + +const enableLogs = true; +final log = Logger('GoRouter tests'); + +void main() { + if (enableLogs) Logger.root.onRecord.listen((e) => debugPrint('$e')); + + group('path routes', () { + test('match home route', () { + final routes = [ + GoRoute(path: '/', builder: (builder, state) => const HomeScreen()), + ]; + + final router = _router(routes); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.fullpath, '/'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + }); + + test('match too many routes', () { + final routes = [ + GoRoute(path: '/', builder: _dummy), + GoRoute(path: '/', builder: _dummy), + ]; + + final router = _router(routes); + router.go('/'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.fullpath, '/'); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + }); + + test('empty path', () { + expect(() { + GoRoute(path: ''); + }, throwsException); + }); + + test('leading / on sub-route', () { + expect(() { + GoRoute( + path: '/', + builder: _dummy, + routes: [ + GoRoute( + path: '/foo', + builder: _dummy, + ), + ], + ); + }, throwsException); + }); + + test('trailing / on sub-route', () { + expect(() { + GoRoute( + path: '/', + builder: _dummy, + routes: [ + GoRoute( + path: 'foo/', + builder: _dummy, + ), + ], + ); + }, throwsException); + }); + + test('lack of leading / on top-level route', () { + expect(() { + final routes = [ + GoRoute(path: 'foo', builder: _dummy), + ]; + _router(routes); + }, throwsException); + }); + + test('match no routes', () { + final routes = [ + GoRoute(path: '/', builder: _dummy), + ]; + + final router = _router(routes); + router.go('/foo'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + }); + + test('match 2nd top level route', () { + final routes = [ + GoRoute(path: '/', builder: (builder, state) => const HomeScreen()), + GoRoute( + path: '/login', builder: (builder, state) => const LoginScreen()), + ]; + + final router = _router(routes); + router.go('/login'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.subloc, '/login'); + expect(router.screenFor(matches.first).runtimeType, LoginScreen); + }); + + test('match top level route when location has trailing /', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + path: '/login', + builder: (builder, state) => const LoginScreen(), + ), + ]; + + final router = _router(routes); + router.go('/login/'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.subloc, '/login'); + expect(router.screenFor(matches.first).runtimeType, LoginScreen); + }); + + test('match top level route when location has trailing / (2)', () { + final routes = [ + GoRoute(path: '/profile', redirect: (_) => '/profile/foo'), + GoRoute(path: '/profile/:kind', builder: _dummy), + ]; + + final router = _router(routes); + router.go('/profile/'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.subloc, '/profile/foo'); + expect(router.screenFor(matches.first).runtimeType, DummyScreen); + }); + + test('match top level route when location has trailing / (3)', () { + final routes = [ + GoRoute(path: '/profile', redirect: (_) => '/profile/foo'), + GoRoute(path: '/profile/:kind', builder: _dummy), + ]; + + final router = _router(routes); + router.go('/profile/?bar=baz'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.subloc, '/profile/foo'); + expect(router.screenFor(matches.first).runtimeType, DummyScreen); + }); + + test('match sub-route', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'login', + builder: (builder, state) => const LoginScreen(), + ), + ], + ), + ]; + + final router = _router(routes); + router.go('/login'); + final matches = router.routerDelegate.matches; + expect(matches.length, 2); + expect(matches.first.subloc, '/'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + expect(matches[1].subloc, '/login'); + expect(router.screenFor(matches[1]).runtimeType, LoginScreen); + }); + + test('match sub-routes', () { + final routes = [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (context, state) => const FamilyScreen('dummy'), + routes: [ + GoRoute( + path: 'person/:pid', + builder: (context, state) => + const PersonScreen('dummy', 'dummy'), + ), + ], + ), + GoRoute( + path: 'login', + builder: (context, state) => const LoginScreen(), + ), + ], + ), + ]; + + final router = _router(routes); + { + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.fullpath, '/'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + } + + router.go('/login'); + { + final matches = router.routerDelegate.matches; + expect(matches.length, 2); + expect(matches.first.subloc, '/'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + expect(matches[1].subloc, '/login'); + expect(router.screenFor(matches[1]).runtimeType, LoginScreen); + } + + router.go('/family/f2'); + { + final matches = router.routerDelegate.matches; + expect(matches.length, 2); + expect(matches.first.subloc, '/'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + expect(matches[1].subloc, '/family/f2'); + expect(router.screenFor(matches[1]).runtimeType, FamilyScreen); + } + + router.go('/family/f2/person/p1'); + { + final matches = router.routerDelegate.matches; + expect(matches.length, 3); + expect(matches.first.subloc, '/'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + expect(matches[1].subloc, '/family/f2'); + expect(router.screenFor(matches[1]).runtimeType, FamilyScreen); + expect(matches[2].subloc, '/family/f2/person/p1'); + expect(router.screenFor(matches[2]).runtimeType, PersonScreen); + } + }); + + test('match too many sub-routes', () { + final routes = [ + GoRoute( + path: '/', + builder: _dummy, + routes: [ + GoRoute( + path: 'foo/bar', + builder: _dummy, + ), + GoRoute( + path: 'foo', + builder: _dummy, + routes: [ + GoRoute( + path: 'bar', + builder: _dummy, + ), + ], + ), + ], + ), + ]; + + final router = _router(routes); + router.go('/foo/bar'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + }); + + test('router state', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (builder, state) { + expect( + state.location, + anyOf(['/', '/login', '/family/f2', '/family/f2/person/p1']), + ); + expect(state.subloc, '/'); + expect(state.name, 'home'); + expect(state.path, '/'); + expect(state.fullpath, '/'); + expect(state.params, {}); + expect(state.error, null); + expect(state.extra! as int, 1); + return const HomeScreen(); + }, + routes: [ + GoRoute( + name: 'login', + path: 'login', + builder: (builder, state) { + expect(state.location, '/login'); + expect(state.subloc, '/login'); + expect(state.name, 'login'); + expect(state.path, 'login'); + expect(state.fullpath, '/login'); + expect(state.params, {}); + expect(state.error, null); + expect(state.extra! as int, 2); + return const LoginScreen(); + }, + ), + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (builder, state) { + expect( + state.location, + anyOf(['/family/f2', '/family/f2/person/p1']), + ); + expect(state.subloc, '/family/f2'); + expect(state.name, 'family'); + expect(state.path, 'family/:fid'); + expect(state.fullpath, '/family/:fid'); + expect(state.params, {'fid': 'f2'}); + expect(state.error, null); + expect(state.extra! as int, 3); + return FamilyScreen(state.params['fid']!); + }, + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (context, state) { + expect(state.location, '/family/f2/person/p1'); + expect(state.subloc, '/family/f2/person/p1'); + expect(state.name, 'person'); + expect(state.path, 'person/:pid'); + expect(state.fullpath, '/family/:fid/person/:pid'); + expect( + state.params, + {'fid': 'f2', 'pid': 'p1'}, + ); + expect(state.error, null); + expect(state.extra! as int, 4); + return PersonScreen( + state.params['fid']!, state.params['pid']!); + }, + ), + ], + ), + ], + ), + ]; + + final router = _router(routes); + router.go('/', extra: 1); + router.go('/login', extra: 2); + router.go('/family/f2', extra: 3); + router.go('/family/f2/person/p1', extra: 4); + }); + + test('match path case insensitively', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + path: '/family/:fid', + builder: (builder, state) => FamilyScreen(state.params['fid']!), + ), + ]; + + final router = _router(routes); + const loc = '/FaMiLy/f2'; + router.go(loc); + final matches = router.routerDelegate.matches; + + // NOTE: match the lower case, since subloc is canonicalized to match the + // path case whereas the location can be any case; so long as the path + // produces a match regardless of the location case, we win! + expect(router.location.toLowerCase(), loc.toLowerCase()); + + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, FamilyScreen); + }); + + test('match too many routes, ignoring case', () { + final routes = [ + GoRoute(path: '/page1', builder: _dummy), + GoRoute(path: '/PaGe1', builder: _dummy), + ]; + + final router = _router(routes); + router.go('/PAGE1'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + }); + }); + + group('named routes', () { + test('match home route', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (builder, state) => const HomeScreen()), + ]; + + final router = _router(routes); + router.goNamed('home'); + }); + + test('match too many routes', () { + final routes = [ + GoRoute(name: 'home', path: '/', builder: _dummy), + GoRoute(name: 'home', path: '/', builder: _dummy), + ]; + + expect(() { + _router(routes); + }, throwsException); + }); + + test('empty name', () { + expect(() { + GoRoute(name: '', path: '/'); + }, throwsException); + }); + + test('match no routes', () { + expect(() { + final routes = [ + GoRoute(name: 'home', path: '/', builder: _dummy), + ]; + final router = _router(routes); + router.goNamed('work'); + }, throwsException); + }); + + test('match 2nd top level route', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + name: 'login', + path: '/login', + builder: (builder, state) => const LoginScreen(), + ), + ]; + + final router = _router(routes); + router.goNamed('login'); + }); + + test('match sub-route', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + name: 'login', + path: 'login', + builder: (builder, state) => const LoginScreen(), + ), + ], + ), + ]; + + final router = _router(routes); + router.goNamed('login'); + }); + + test('match sub-route case insensitive', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + name: 'page1', + path: 'page1', + builder: (builder, state) => const Page1Screen(), + ), + GoRoute( + name: 'page2', + path: 'Page2', + builder: (builder, state) => const Page2Screen(), + ), + ], + ), + ]; + + final router = _router(routes); + router.goNamed('Page1'); + router.goNamed('page2'); + }); + + test('match w/ params', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (context, state) => const FamilyScreen('dummy'), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (context, state) { + expect(state.params, {'fid': 'f2', 'pid': 'p1'}); + return const PersonScreen('dummy', 'dummy'); + }, + ), + ], + ), + ], + ), + ]; + + final router = _router(routes); + router.goNamed('person', params: {'fid': 'f2', 'pid': 'p1'}); + }); + + test('too few params', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (context, state) => const FamilyScreen('dummy'), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (context, state) => + const PersonScreen('dummy', 'dummy'), + ), + ], + ), + ], + ), + ]; + expect(() { + final router = _router(routes); + router.goNamed('person', params: {'fid': 'f2'}); + }, throwsException); + }); + + test('match case insensitive w/ params', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (context, state) => const FamilyScreen('dummy'), + routes: [ + GoRoute( + name: 'PeRsOn', + path: 'person/:pid', + builder: (context, state) { + expect(state.params, {'fid': 'f2', 'pid': 'p1'}); + return const PersonScreen('dummy', 'dummy'); + }, + ), + ], + ), + ], + ), + ]; + + final router = _router(routes); + router.goNamed('person', params: {'fid': 'f2', 'pid': 'p1'}); + }); + + test('too few params', () { + final routes = [ + GoRoute( + name: 'family', + path: '/family/:fid', + builder: (context, state) => const FamilyScreen('dummy'), + ), + ]; + expect(() { + final router = _router(routes); + router.goNamed('family'); + }, throwsException); + }); + + test('too many params', () { + final routes = [ + GoRoute( + name: 'family', + path: '/family/:fid', + builder: (context, state) => const FamilyScreen('dummy'), + ), + ]; + expect(() { + final router = _router(routes); + router.goNamed('family', params: {'fid': 'f2', 'pid': 'p1'}); + }, throwsException); + }); + + test('sparsely named routes', () { + final routes = [ + GoRoute( + path: '/', + redirect: (_) => '/family/f2', + ), + GoRoute( + path: '/family/:fid', + builder: (context, state) => FamilyScreen( + state.params['fid']!, + ), + routes: [ + GoRoute( + name: 'person', + path: 'person:pid', + builder: (context, state) => PersonScreen( + state.params['fid']!, + state.params['pid']!, + ), + ), + ], + ), + ]; + + final router = _router(routes); + router.goNamed('person', params: {'fid': 'f2', 'pid': 'p1'}); + + final matches = router.routerDelegate.matches; + expect(router.screenFor(matches.last).runtimeType, PersonScreen); + }); + + test('preserve path param spaces and slashes', () { + const param1 = 'param w/ spaces and slashes'; + final routes = [ + GoRoute( + name: 'page1', + path: '/page1/:param1', + builder: (c, s) { + expect(s.params['param1'], param1); + return const DummyScreen(); + }, + ), + ]; + + final router = _router(routes); + final loc = router.namedLocation('page1', params: {'param1': param1}); + log.info('loc= $loc'); + router.go(loc); + + final matches = router.routerDelegate.matches; + log.info('param1= ${matches.first.decodedParams['param1']}'); + expect(router.screenFor(matches.first).runtimeType, DummyScreen); + expect(matches.first.decodedParams['param1'], param1); + }); + + test('preserve query param spaces and slashes', () { + const param1 = 'param w/ spaces and slashes'; + final routes = [ + GoRoute( + name: 'page1', + path: '/page1', + builder: (c, s) { + expect(s.queryParams['param1'], param1); + return const DummyScreen(); + }, + ), + ]; + + final router = _router(routes); + final loc = + router.namedLocation('page1', queryParams: {'param1': param1}); + log.info('loc= $loc'); + router.go(loc); + + final matches = router.routerDelegate.matches; + log.info('param1= ${matches.first.queryParams['param1']}'); + expect(router.screenFor(matches.first).runtimeType, DummyScreen); + expect(matches.first.queryParams['param1'], param1); + }); + }); + + group('redirects', () { + test('top-level redirect', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'dummy', + builder: (builder, state) => const DummyScreen()), + GoRoute( + path: 'login', + builder: (builder, state) => const LoginScreen()), + ], + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + redirect: (state) => state.subloc == '/login' ? null : '/login', + ); + expect(router.location, '/login'); + }); + + test('top-level redirect w/ named routes', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + name: 'dummy', + path: 'dummy', + builder: (builder, state) => const DummyScreen(), + ), + GoRoute( + name: 'login', + path: 'login', + builder: (builder, state) => const LoginScreen(), + ), + ], + ), + ]; + + final router = GoRouter( + debugLogDiagnostics: true, + routes: routes, + errorBuilder: _dummy, + redirect: (state) => + state.subloc == '/login' ? null : state.namedLocation('login'), + ); + expect(router.location, '/login'); + }); + + test('route-level redirect', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'dummy', + builder: (builder, state) => const DummyScreen(), + redirect: (state) => '/login', + ), + GoRoute( + path: 'login', + builder: (builder, state) => const LoginScreen(), + ), + ], + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + ); + router.go('/dummy'); + expect(router.location, '/login'); + }); + + test('route-level redirect w/ named routes', () { + final routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + name: 'dummy', + path: 'dummy', + builder: (builder, state) => const DummyScreen(), + redirect: (state) => state.namedLocation('login'), + ), + GoRoute( + name: 'login', + path: 'login', + builder: (builder, state) => const LoginScreen(), + ), + ], + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + ); + router.go('/dummy'); + expect(router.location, '/login'); + }); + + test('multiple mixed redirect', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'dummy1', + builder: (builder, state) => const DummyScreen(), + ), + GoRoute( + path: 'dummy2', + builder: (builder, state) => const DummyScreen(), + redirect: (state) => '/', + ), + ], + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + redirect: (state) => state.subloc == '/dummy1' ? '/dummy2' : null, + ); + router.go('/dummy1'); + expect(router.location, '/'); + }); + + test('top-level redirect loop', () { + final router = GoRouter( + routes: [], + errorBuilder: (context, state) => ErrorScreen(state.error!), + redirect: (state) => state.subloc == '/' + ? '/login' + : state.subloc == '/login' + ? '/' + : null, + ); + + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as ErrorScreen).ex); + }); + + test('route-level redirect loop', () { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (state) => '/login', + ), + GoRoute( + path: '/login', + redirect: (state) => '/', + ), + ], + errorBuilder: (context, state) => ErrorScreen(state.error!), + ); + + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as ErrorScreen).ex); + }); + + test('mixed redirect loop', () { + final router = GoRouter( + routes: [ + GoRoute( + path: '/login', + redirect: (state) => '/', + ), + ], + errorBuilder: (context, state) => ErrorScreen(state.error!), + redirect: (state) => state.subloc == '/' ? '/login' : null, + ); + + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as ErrorScreen).ex); + }); + + test('top-level redirect loop w/ query params', () { + final router = GoRouter( + routes: [], + errorBuilder: (context, state) => ErrorScreen(state.error!), + redirect: (state) => state.subloc == '/' + ? '/login?from=${state.location}' + : state.subloc == '/login' + ? '/' + : null, + ); + + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as ErrorScreen).ex); + }); + + test('expect null path/fullpath on top-level redirect', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + path: '/dummy', + redirect: (state) => '/', + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + initialLocation: '/dummy', + ); + expect(router.location, '/'); + }); + + test('top-level redirect state', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + path: '/login', + builder: (builder, state) => const LoginScreen(), + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + initialLocation: '/login?from=/', + debugLogDiagnostics: true, + redirect: (state) { + expect(Uri.parse(state.location).queryParameters, isNotEmpty); + expect(Uri.parse(state.subloc).queryParameters, isEmpty); + expect(state.path, isNull); + expect(state.fullpath, isNull); + expect(state.params.length, 0); + expect(state.queryParams.length, 1); + expect(state.queryParams['from'], '/'); + return null; + }, + ); + + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, LoginScreen); + }); + + test('route-level redirect state', () { + const loc = '/book/0'; + final routes = [ + GoRoute( + path: '/book/:bookId', + redirect: (state) { + expect(state.location, loc); + expect(state.subloc, loc); + expect(state.path, '/book/:bookId'); + expect(state.fullpath, '/book/:bookId'); + expect(state.params, {'bookId': '0'}); + expect(state.queryParams.length, 0); + return null; + }, + builder: (c, s) => const HomeScreen(), + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + initialLocation: loc, + debugLogDiagnostics: true, + ); + + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + }); + + test('sub-sub-route-level redirect params', () { + final routes = [ + GoRoute( + path: '/', + builder: (c, s) => const HomeScreen(), + routes: [ + GoRoute( + path: 'family/:fid', + builder: (c, s) => FamilyScreen(s.params['fid']!), + routes: [ + GoRoute( + path: 'person/:pid', + redirect: (s) { + expect(s.params['fid'], 'f2'); + expect(s.params['pid'], 'p1'); + return null; + }, + builder: (c, s) => PersonScreen( + s.params['fid']!, + s.params['pid']!, + ), + ), + ], + ), + ], + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + initialLocation: '/family/f2/person/p1', + debugLogDiagnostics: true, + ); + + final matches = router.routerDelegate.matches; + expect(matches.length, 3); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + expect(router.screenFor(matches[1]).runtimeType, FamilyScreen); + final page = router.screenFor(matches[2]) as PersonScreen; + expect(page.fid, 'f2'); + expect(page.pid, 'p1'); + }); + + test('redirect limit', () { + final router = GoRouter( + routes: [], + errorBuilder: (context, state) => ErrorScreen(state.error!), + debugLogDiagnostics: true, + redirect: (state) => '${state.location}+', + redirectLimit: 10, + ); + + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as ErrorScreen).ex); + }); + }); + + group('initial location', () { + test('initial location', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'dummy', + builder: (builder, state) => const DummyScreen(), + ), + ], + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + initialLocation: '/dummy', + ); + expect(router.location, '/dummy'); + }); + + test('initial location w/ redirection', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + path: '/dummy', + redirect: (state) => '/', + ), + ]; + + final router = GoRouter( + routes: routes, + errorBuilder: _dummy, + initialLocation: '/dummy', + ); + expect(router.location, '/'); + }); + }); + + group('params', () { + test('preserve path param case', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + path: '/family/:fid', + builder: (builder, state) => FamilyScreen(state.params['fid']!), + ), + ]; + + final router = _router(routes); + for (final fid in ['f2', 'F2']) { + final loc = '/family/$fid'; + router.go(loc); + final matches = router.routerDelegate.matches; + + expect(router.location, loc); + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, FamilyScreen); + expect(matches.first.decodedParams['fid'], fid); + } + }); + + test('preserve query param case', () { + final routes = [ + GoRoute( + path: '/', + builder: (builder, state) => const HomeScreen(), + ), + GoRoute( + path: '/family', + builder: (builder, state) => FamilyScreen( + state.queryParams['fid']!, + ), + ), + ]; + + final router = _router(routes); + for (final fid in ['f2', 'F2']) { + final loc = '/family?fid=$fid'; + router.go(loc); + final matches = router.routerDelegate.matches; + + expect(router.location, loc); + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, FamilyScreen); + expect(matches.first.queryParams['fid'], fid); + } + }); + + test('preserve path param spaces and slashes', () { + const param1 = 'param w/ spaces and slashes'; + final routes = [ + GoRoute( + path: '/page1/:param1', + builder: (c, s) { + expect(s.params['param1'], param1); + return const DummyScreen(); + }, + ), + ]; + + final router = _router(routes); + final loc = '/page1/${Uri.encodeComponent(param1)}'; + router.go(loc); + + final matches = router.routerDelegate.matches; + log.info('param1= ${matches.first.decodedParams['param1']}'); + expect(router.screenFor(matches.first).runtimeType, DummyScreen); + expect(matches.first.decodedParams['param1'], param1); + }); + + test('preserve query param spaces and slashes', () { + const param1 = 'param w/ spaces and slashes'; + final routes = [ + GoRoute( + path: '/page1', + builder: (c, s) { + expect(s.queryParams['param1'], param1); + return const DummyScreen(); + }, + ), + ]; + + final router = _router(routes); + router.go('/page1?param1=$param1'); + + final matches = router.routerDelegate.matches; + expect(router.screenFor(matches.first).runtimeType, DummyScreen); + expect(matches.first.queryParams['param1'], param1); + + final loc = '/page1?param1=${Uri.encodeQueryComponent(param1)}'; + router.go(loc); + + final matches2 = router.routerDelegate.matches; + expect(router.screenFor(matches2[0]).runtimeType, DummyScreen); + expect(matches2[0].queryParams['param1'], param1); + }); + + test('error: duplicate path param', () { + try { + GoRouter( + routes: [ + GoRoute( + path: '/:id/:blah/:bam/:id/:blah', + builder: _dummy, + ), + ], + errorBuilder: (context, state) => ErrorScreen(state.error!), + initialLocation: '/0/1/2/0/1', + ); + expect(false, true); + } on Exception catch (ex) { + log.info(ex); + } + }); + + test('duplicate query param', () { + final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) { + log.info('id= ${state.params['id']}'); + expect(state.params.length, 0); + expect(state.queryParams.length, 1); + expect(state.queryParams['id'], anyOf('0', '1')); + return const HomeScreen(); + }, + ), + ], + errorBuilder: (context, state) => ErrorScreen(state.error!), + ); + + router.go('/?id=0&id=1'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.fullpath, '/'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + }); + + test('duplicate path + query param', () { + final router = GoRouter( + routes: [ + GoRoute( + path: '/:id', + builder: (context, state) { + expect(state.params, {'id': '0'}); + expect(state.queryParams, {'id': '1'}); + return const HomeScreen(); + }, + ), + ], + errorBuilder: _dummy, + ); + + router.go('/0?id=1'); + final matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(matches.first.fullpath, '/:id'); + expect(router.screenFor(matches.first).runtimeType, HomeScreen); + }); + + test('push + query param', () { + final router = GoRouter( + routes: [ + GoRoute(path: '/', builder: _dummy), + GoRoute( + path: '/family', + builder: (context, state) => FamilyScreen( + state.queryParams['fid']!, + ), + ), + GoRoute( + path: '/person', + builder: (context, state) => PersonScreen( + state.queryParams['fid']!, + state.queryParams['pid']!, + ), + ), + ], + errorBuilder: _dummy, + ); + + router.go('/family?fid=f2'); + router.push('/person?fid=f2&pid=p1'); + final page1 = + router.screenFor(router.routerDelegate.matches.first) as FamilyScreen; + expect(page1.fid, 'f2'); + + final page2 = + router.screenFor(router.routerDelegate.matches[1]) as PersonScreen; + expect(page2.fid, 'f2'); + expect(page2.pid, 'p1'); + }); + + test('push + extra param', () { + final router = GoRouter( + routes: [ + GoRoute(path: '/', builder: _dummy), + GoRoute( + path: '/family', + builder: (context, state) => FamilyScreen( + (state.extra! as Map)['fid']!, + ), + ), + GoRoute( + path: '/person', + builder: (context, state) => PersonScreen( + (state.extra! as Map)['fid']!, + (state.extra! as Map)['pid']!, + ), + ), + ], + errorBuilder: _dummy, + ); + + router.go('/family', extra: {'fid': 'f2'}); + router.push('/person', extra: {'fid': 'f2', 'pid': 'p1'}); + final page1 = + router.screenFor(router.routerDelegate.matches.first) as FamilyScreen; + expect(page1.fid, 'f2'); + + final page2 = + router.screenFor(router.routerDelegate.matches[1]) as PersonScreen; + expect(page2.fid, 'f2'); + expect(page2.pid, 'p1'); + }); + }); + + group('refresh listenable', () { + late StreamController streamController; + + setUpAll(() async { + streamController = StreamController.broadcast(); + await streamController.addStream(Stream.value(0)); + }); + + tearDownAll(() { + streamController.close(); + }); + + group('stream', () { + test('no stream emits', () async { + // Act + final notifyListener = MockGoRouterRefreshStream( + streamController.stream, + ); + + // Assert + expect(notifyListener.notifyCount, equals(1)); + + // Cleanup + notifyListener.dispose(); + }); + + test('three stream emits', () async { + // Arrange + final toEmit = [1, 2, 3]; + + // Act + final notifyListener = MockGoRouterRefreshStream( + streamController.stream, + ); + + await streamController.addStream(Stream.fromIterable(toEmit)); + + // Assert + expect(notifyListener.notifyCount, equals(toEmit.length + 1)); + + // Cleanup + notifyListener.dispose(); + }); + }); + }); +} + +class MockGoRouterRefreshStream extends GoRouterRefreshStream { + MockGoRouterRefreshStream( + Stream stream, + ) : notifyCount = 0, + super(stream); + + late int notifyCount; + + @override + void notifyListeners() { + notifyCount++; + super.notifyListeners(); + } +} + +GoRouter _router(List routes) => GoRouter( + routes: routes, + errorBuilder: (context, state) => ErrorScreen(state.error!), + debugLogDiagnostics: true, + ); + +class ErrorScreen extends DummyScreen { + const ErrorScreen(this.ex, {Key? key}) : super(key: key); + final Exception ex; +} + +class HomeScreen extends DummyScreen { + const HomeScreen({Key? key}) : super(key: key); +} + +class Page1Screen extends DummyScreen { + const Page1Screen({Key? key}) : super(key: key); +} + +class Page2Screen extends DummyScreen { + const Page2Screen({Key? key}) : super(key: key); +} + +class LoginScreen extends DummyScreen { + const LoginScreen({Key? key}) : super(key: key); +} + +class FamilyScreen extends DummyScreen { + const FamilyScreen(this.fid, {Key? key}) : super(key: key); + final String fid; +} + +class FamiliesScreen extends DummyScreen { + const FamiliesScreen({required this.selectedFid, Key? key}) : super(key: key); + final String selectedFid; +} + +class PersonScreen extends DummyScreen { + const PersonScreen(this.fid, this.pid, {Key? key}) : super(key: key); + final String fid; + final String pid; +} + +class DummyScreen extends StatelessWidget { + const DummyScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => throw UnimplementedError(); +} + +Widget _dummy(BuildContext context, GoRouterState state) => const DummyScreen(); + +extension on GoRouter { + Page _pageFor(GoRouteMatch match) { + final matches = routerDelegate.matches; + final i = matches.indexOf(match); + final pages = + routerDelegate.getPages(DummyBuildContext(), matches).toList(); + return pages[i]; + } + + Widget screenFor(GoRouteMatch match) => + (_pageFor(match) as NoTransitionPage).child; +} + +class DummyBuildContext implements BuildContext { + @override + bool get debugDoingBuild => throw UnimplementedError(); + + @override + InheritedWidget dependOnInheritedElement(InheritedElement ancestor, + {Object aspect = 1}) { + throw UnimplementedError(); + } + + @override + T? dependOnInheritedWidgetOfExactType( + {Object? aspect}) { + throw UnimplementedError(); + } + + @override + DiagnosticsNode describeElement(String name, + {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) { + throw UnimplementedError(); + } + + @override + List describeMissingAncestor( + {required Type expectedAncestorType}) { + throw UnimplementedError(); + } + + @override + DiagnosticsNode describeOwnershipChain(String name) { + throw UnimplementedError(); + } + + @override + DiagnosticsNode describeWidget(String name, + {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) { + throw UnimplementedError(); + } + + @override + T? findAncestorRenderObjectOfType() { + throw UnimplementedError(); + } + + @override + T? findAncestorStateOfType>() { + throw UnimplementedError(); + } + + @override + T? findAncestorWidgetOfExactType() { + throw UnimplementedError(); + } + + @override + RenderObject? findRenderObject() { + throw UnimplementedError(); + } + + @override + T? findRootAncestorStateOfType>() { + throw UnimplementedError(); + } + + @override + InheritedElement? + getElementForInheritedWidgetOfExactType() { + throw UnimplementedError(); + } + + @override + BuildOwner? get owner => throw UnimplementedError(); + + @override + Size? get size => throw UnimplementedError(); + + @override + void visitAncestorElements(bool Function(Element element) visitor) {} + + @override + void visitChildElements(ElementVisitor visitor) {} + + @override + Widget get widget => throw UnimplementedError(); +} diff --git a/packages/go_router/test/path_parser_test.dart b/packages/go_router/test/path_parser_test.dart new file mode 100644 index 000000000000..85876ecb249b --- /dev/null +++ b/packages/go_router/test/path_parser_test.dart @@ -0,0 +1,75 @@ +// 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_test/flutter_test.dart'; +import 'package:go_router/src/path_parser.dart'; + +void main() { + test('patternToRegExp without path parameter', () async { + final String pattern = '/settings/detail'; + final List pathParameter = []; + final RegExp regex = patternToRegExp(pattern, pathParameter); + expect(pathParameter.isEmpty, isTrue); + expect(regex.hasMatch('/settings/detail'), isTrue); + expect(regex.hasMatch('/settings/'), isFalse); + expect(regex.hasMatch('/settings'), isFalse); + expect(regex.hasMatch('/'), isFalse); + expect(regex.hasMatch('/settings/details'), isFalse); + expect(regex.hasMatch('/setting/detail'), isFalse); + }); + + test('patternToRegExp with path parameter', () async { + final String pattern = '/user/:id/book/:bookId'; + final List pathParameter = []; + final RegExp regex = patternToRegExp(pattern, pathParameter); + expect(pathParameter.length, 2); + expect(pathParameter[0], 'id'); + expect(pathParameter[1], 'bookId'); + + final RegExpMatch? match = regex.firstMatch('/user/123/book/456/'); + expect(match, isNotNull); + final Map parameterValues = + extractPathParameters(pathParameter, match!); + expect(parameterValues.length, 2); + expect(parameterValues[pathParameter[0]], '123'); + expect(parameterValues[pathParameter[1]], '456'); + + expect(regex.hasMatch('/user/123/book/'), isFalse); + expect(regex.hasMatch('/user/123'), isFalse); + expect(regex.hasMatch('/user/'), isFalse); + expect(regex.hasMatch('/'), isFalse); + }); + + test('patternToPath without path parameter', () async { + final String pattern = '/settings/detail'; + final List pathParameter = []; + final RegExp regex = patternToRegExp(pattern, pathParameter); + + final String url = '/settings/detail'; + final RegExpMatch? match = regex.firstMatch(url); + expect(match, isNotNull); + + final Map parameterValues = + extractPathParameters(pathParameter, match!); + final String restoredUrl = patternToPath(pattern, parameterValues); + + expect(url, restoredUrl); + }); + + test('patternToPath with path parameter', () async { + final String pattern = '/user/:id/book/:bookId'; + final List pathParameter = []; + final RegExp regex = patternToRegExp(pattern, pathParameter); + + final String url = '/user/123/book/456'; + final RegExpMatch? match = regex.firstMatch(url); + expect(match, isNotNull); + + final Map parameterValues = + extractPathParameters(pathParameter, match!); + final String restoredUrl = patternToPath(pattern, parameterValues); + + expect(url, restoredUrl); + }); +}