From e95c34018e449da79b2ce7659a2ca3c34d4dfddd Mon Sep 17 00:00:00 2001 From: toly <1981462002@qq.com> Date: Fri, 22 Sep 2023 09:15:11 +0800 Subject: [PATCH] 9 --- lib/06/02/main.dart | 19 +- lib/06/02/pages/home_page.dart | 28 ++ lib/06/02/pages/page_b.dart | 6 +- lib/{07/02 => 08/01}/main.dart | 0 lib/{07/02 => 08/01}/pages/page_a.dart | 0 lib/{07/02 => 08/01}/pages/page_b.dart | 0 lib/{06/02 => 08/01}/pages/page_c.dart | 0 lib/{07/03 => 08/02}/main.dart | 0 lib/{07/03 => 08/02}/pages/page_a.dart | 2 +- lib/{07/03 => 08/02}/pages/page_b.dart | 0 lib/{07/03 => 08/02}/pages/page_c.dart | 0 lib/{07/03 => 08/02}/pages/page_d.dart | 0 lib/08/节点介绍.txt | 5 + lib/09/01/main.dart | 27 + lib/09/01/pages/page_a.dart | 28 ++ lib/09/01/pages/page_b.dart | 29 ++ lib/{07/02 => 09/01}/pages/page_c.dart | 5 + lib/09/02/main.dart | 50 ++ lib/09/02/pages/home_page.dart | 29 ++ lib/09/02/pages/page_a.dart | 28 ++ lib/09/02/pages/page_b.dart | 29 ++ lib/09/02/pages/page_c.dart | 34 ++ lib/12/01/main.dart | 11 + lib/12/01/pages/app/app.dart | 28 ++ lib/12/01/pages/app/app_navigation.dart | 50 ++ lib/12/01/pages/app/app_tool_bar.dart | 64 +++ lib/12/01/pages/home_page.dart | 29 ++ lib/12/01/pages/page_a.dart | 29 ++ lib/12/01/pages/page_b.dart | 39 ++ lib/12/01/pages/page_c.dart | 29 ++ lib/13/01/main.dart | 15 + lib/13/01/pages/app.dart | 50 ++ lib/13/01/pages/color_add_page.dart | 67 +++ lib/13/01/pages/home_page.dart | 46 ++ lib/13/01/route/parsed_route.dart | 27 + lib/13/01/route/parser.dart | 135 +++++ lib/13/01/route/route_state.dart | 48 ++ lib/13/02/main.dart | 15 + lib/13/02/pages/app.dart | 56 +++ lib/13/02/pages/color_add_page.dart | 67 +++ lib/13/02/pages/home_page.dart | 43 ++ lib/13/02/route/parsed_route.dart | 49 ++ lib/13/02/route/parser.dart | 153 ++++++ lib/13/02/route/route_state.dart | 48 ++ lib/13/02/store/app_state.dart | 32 ++ lib/13/02_/main.dart | 13 + lib/13/02_/menus.dart | 83 ++++ lib/13/02_/pages/app/app.dart | 107 ++++ lib/13/02_/pages/app/root_content.dart | 140 ++++++ lib/13/02_/pages/color_page.dart | 11 + lib/13/02_/transition/fade_transition.dart | 58 +++ lib/13/03/main.dart | 13 + lib/13/03/menus.dart | 76 +++ lib/13/03/pages/app/app.dart | 89 ++++ lib/13/03/pages/app/root_content.dart | 43 ++ lib/13/go/async_redirection.dart | 244 ++++++++++ lib/13/go/books/main.dart | 171 +++++++ lib/13/go/books/src/auth.dart | 46 ++ lib/13/go/books/src/data.dart | 7 + lib/13/go/books/src/data/author.dart | 23 + lib/13/go/books/src/data/book.dart | 32 ++ lib/13/go/books/src/data/library.dart | 76 +++ .../go/books/src/screens/author_details.dart | 49 ++ lib/13/go/books/src/screens/authors.dart | 31 ++ lib/13/go/books/src/screens/book_details.dart | 77 +++ lib/13/go/books/src/screens/books.dart | 118 +++++ lib/13/go/books/src/screens/scaffold.dart | 71 +++ lib/13/go/books/src/screens/settings.dart | 101 ++++ lib/13/go/books/src/screens/sign_in.dart | 77 +++ lib/13/go/books/src/widgets/author_list.dart | 37 ++ lib/13/go/books/src/widgets/book_list.dart | 37 ++ lib/13/go/exception_handling.dart | 87 ++++ lib/13/go/main.dart | 87 ++++ lib/13/go/named_routes.dart | 181 +++++++ .../others/custom_stateful_shell_route.dart | 460 ++++++++++++++++++ lib/13/go/others/error_screen.dart | 110 +++++ lib/13/go/others/extra_param.dart | 133 +++++ lib/13/go/others/init_loc.dart | 110 +++++ lib/13/go/others/nav_observer.dart | 167 +++++++ lib/13/go/others/push.dart | 101 ++++ lib/13/go/others/router_neglect.dart | 95 ++++ lib/13/go/others/state_restoration.dart | 102 ++++ .../stateful_shell_state_restoration.dart | 236 +++++++++ lib/13/go/others/transitions.dart | 163 +++++++ lib/13/go/path_and_query_parameters.dart | 169 +++++++ lib/13/go/raw_book/main.dart | 27 + lib/13/go/raw_book/src/app.dart | 81 +++ lib/13/go/raw_book/src/auth.dart | 47 ++ lib/13/go/raw_book/src/data.dart | 7 + lib/13/go/raw_book/src/data/author.dart | 13 + lib/13/go/raw_book/src/data/book.dart | 15 + lib/13/go/raw_book/src/data/library.dart | 61 +++ lib/13/go/raw_book/src/routing.dart | 8 + lib/13/go/raw_book/src/routing/delegate.dart | 47 ++ .../go/raw_book/src/routing/parsed_route.dart | 51 ++ lib/13/go/raw_book/src/routing/parser.dart | 62 +++ .../go/raw_book/src/routing/route_state.dart | 48 ++ .../raw_book/src/screens/author_details.dart | 39 ++ lib/13/go/raw_book/src/screens/authors.dart | 28 ++ .../go/raw_book/src/screens/book_details.dart | 66 +++ lib/13/go/raw_book/src/screens/books.dart | 111 +++++ lib/13/go/raw_book/src/screens/navigator.dart | 108 ++++ lib/13/go/raw_book/src/screens/scaffold.dart | 54 ++ .../raw_book/src/screens/scaffold_body.dart | 63 +++ lib/13/go/raw_book/src/screens/settings.dart | 95 ++++ lib/13/go/raw_book/src/screens/sign_in.dart | 69 +++ .../go/raw_book/src/widgets/author_list.dart | 32 ++ lib/13/go/raw_book/src/widgets/book_list.dart | 32 ++ .../src/widgets/fade_transition_page.dart | 53 ++ lib/13/go/redirection.dart | 146 ++++++ lib/13/go/shell_route.dart | 289 +++++++++++ lib/13/go/stateful_shell_route.dart | 324 ++++++++++++ lib/13/go/transition_animations.dart | 175 +++++++ lib/cases/cases.dart | 3 + lib/cases/counter/counter_page.dart | 45 ++ lib/cases/guess/guess_app_bar.dart | 47 ++ lib/cases/guess/guess_page.dart | 125 +++++ lib/cases/guess/result_notice.dart | 35 ++ lib/cases/paper/color_selector.dart | 52 ++ lib/cases/paper/conform_dialog.dart | 102 ++++ lib/cases/paper/model.dart | 14 + lib/cases/paper/paper.dart | 194 ++++++++ lib/cases/paper/paper_app_bar.dart | 76 +++ lib/cases/paper/stork_width_selector.dart | 50 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 147 +++++- pubspec.yaml | 10 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + windows/runner/main.cpp | 2 +- 132 files changed, 8527 insertions(+), 17 deletions(-) create mode 100644 lib/06/02/pages/home_page.dart rename lib/{07/02 => 08/01}/main.dart (100%) rename lib/{07/02 => 08/01}/pages/page_a.dart (100%) rename lib/{07/02 => 08/01}/pages/page_b.dart (100%) rename lib/{06/02 => 08/01}/pages/page_c.dart (100%) rename lib/{07/03 => 08/02}/main.dart (100%) rename lib/{07/03 => 08/02}/pages/page_a.dart (93%) rename lib/{07/03 => 08/02}/pages/page_b.dart (100%) rename lib/{07/03 => 08/02}/pages/page_c.dart (100%) rename lib/{07/03 => 08/02}/pages/page_d.dart (100%) create mode 100644 lib/08/节点介绍.txt create mode 100644 lib/09/01/main.dart create mode 100644 lib/09/01/pages/page_a.dart create mode 100644 lib/09/01/pages/page_b.dart rename lib/{07/02 => 09/01}/pages/page_c.dart (78%) create mode 100644 lib/09/02/main.dart create mode 100644 lib/09/02/pages/home_page.dart create mode 100644 lib/09/02/pages/page_a.dart create mode 100644 lib/09/02/pages/page_b.dart create mode 100644 lib/09/02/pages/page_c.dart create mode 100644 lib/12/01/main.dart create mode 100644 lib/12/01/pages/app/app.dart create mode 100644 lib/12/01/pages/app/app_navigation.dart create mode 100644 lib/12/01/pages/app/app_tool_bar.dart create mode 100644 lib/12/01/pages/home_page.dart create mode 100644 lib/12/01/pages/page_a.dart create mode 100644 lib/12/01/pages/page_b.dart create mode 100644 lib/12/01/pages/page_c.dart create mode 100644 lib/13/01/main.dart create mode 100644 lib/13/01/pages/app.dart create mode 100644 lib/13/01/pages/color_add_page.dart create mode 100644 lib/13/01/pages/home_page.dart create mode 100644 lib/13/01/route/parsed_route.dart create mode 100644 lib/13/01/route/parser.dart create mode 100644 lib/13/01/route/route_state.dart create mode 100644 lib/13/02/main.dart create mode 100644 lib/13/02/pages/app.dart create mode 100644 lib/13/02/pages/color_add_page.dart create mode 100644 lib/13/02/pages/home_page.dart create mode 100644 lib/13/02/route/parsed_route.dart create mode 100644 lib/13/02/route/parser.dart create mode 100644 lib/13/02/route/route_state.dart create mode 100644 lib/13/02/store/app_state.dart create mode 100644 lib/13/02_/main.dart create mode 100644 lib/13/02_/menus.dart create mode 100644 lib/13/02_/pages/app/app.dart create mode 100644 lib/13/02_/pages/app/root_content.dart create mode 100644 lib/13/02_/pages/color_page.dart create mode 100644 lib/13/02_/transition/fade_transition.dart create mode 100644 lib/13/03/main.dart create mode 100644 lib/13/03/menus.dart create mode 100644 lib/13/03/pages/app/app.dart create mode 100644 lib/13/03/pages/app/root_content.dart create mode 100644 lib/13/go/async_redirection.dart create mode 100644 lib/13/go/books/main.dart create mode 100644 lib/13/go/books/src/auth.dart create mode 100644 lib/13/go/books/src/data.dart create mode 100644 lib/13/go/books/src/data/author.dart create mode 100644 lib/13/go/books/src/data/book.dart create mode 100644 lib/13/go/books/src/data/library.dart create mode 100644 lib/13/go/books/src/screens/author_details.dart create mode 100644 lib/13/go/books/src/screens/authors.dart create mode 100644 lib/13/go/books/src/screens/book_details.dart create mode 100644 lib/13/go/books/src/screens/books.dart create mode 100644 lib/13/go/books/src/screens/scaffold.dart create mode 100644 lib/13/go/books/src/screens/settings.dart create mode 100644 lib/13/go/books/src/screens/sign_in.dart create mode 100644 lib/13/go/books/src/widgets/author_list.dart create mode 100644 lib/13/go/books/src/widgets/book_list.dart create mode 100644 lib/13/go/exception_handling.dart create mode 100644 lib/13/go/main.dart create mode 100644 lib/13/go/named_routes.dart create mode 100644 lib/13/go/others/custom_stateful_shell_route.dart create mode 100644 lib/13/go/others/error_screen.dart create mode 100644 lib/13/go/others/extra_param.dart create mode 100644 lib/13/go/others/init_loc.dart create mode 100644 lib/13/go/others/nav_observer.dart create mode 100644 lib/13/go/others/push.dart create mode 100644 lib/13/go/others/router_neglect.dart create mode 100644 lib/13/go/others/state_restoration.dart create mode 100644 lib/13/go/others/stateful_shell_state_restoration.dart create mode 100644 lib/13/go/others/transitions.dart create mode 100644 lib/13/go/path_and_query_parameters.dart create mode 100644 lib/13/go/raw_book/main.dart create mode 100644 lib/13/go/raw_book/src/app.dart create mode 100644 lib/13/go/raw_book/src/auth.dart create mode 100644 lib/13/go/raw_book/src/data.dart create mode 100644 lib/13/go/raw_book/src/data/author.dart create mode 100644 lib/13/go/raw_book/src/data/book.dart create mode 100644 lib/13/go/raw_book/src/data/library.dart create mode 100644 lib/13/go/raw_book/src/routing.dart create mode 100644 lib/13/go/raw_book/src/routing/delegate.dart create mode 100644 lib/13/go/raw_book/src/routing/parsed_route.dart create mode 100644 lib/13/go/raw_book/src/routing/parser.dart create mode 100644 lib/13/go/raw_book/src/routing/route_state.dart create mode 100644 lib/13/go/raw_book/src/screens/author_details.dart create mode 100644 lib/13/go/raw_book/src/screens/authors.dart create mode 100644 lib/13/go/raw_book/src/screens/book_details.dart create mode 100644 lib/13/go/raw_book/src/screens/books.dart create mode 100644 lib/13/go/raw_book/src/screens/navigator.dart create mode 100644 lib/13/go/raw_book/src/screens/scaffold.dart create mode 100644 lib/13/go/raw_book/src/screens/scaffold_body.dart create mode 100644 lib/13/go/raw_book/src/screens/settings.dart create mode 100644 lib/13/go/raw_book/src/screens/sign_in.dart create mode 100644 lib/13/go/raw_book/src/widgets/author_list.dart create mode 100644 lib/13/go/raw_book/src/widgets/book_list.dart create mode 100644 lib/13/go/raw_book/src/widgets/fade_transition_page.dart create mode 100644 lib/13/go/redirection.dart create mode 100644 lib/13/go/shell_route.dart create mode 100644 lib/13/go/stateful_shell_route.dart create mode 100644 lib/13/go/transition_animations.dart create mode 100644 lib/cases/cases.dart create mode 100644 lib/cases/counter/counter_page.dart create mode 100644 lib/cases/guess/guess_app_bar.dart create mode 100644 lib/cases/guess/guess_page.dart create mode 100644 lib/cases/guess/result_notice.dart create mode 100644 lib/cases/paper/color_selector.dart create mode 100644 lib/cases/paper/conform_dialog.dart create mode 100644 lib/cases/paper/model.dart create mode 100644 lib/cases/paper/paper.dart create mode 100644 lib/cases/paper/paper_app_bar.dart create mode 100644 lib/cases/paper/stork_width_selector.dart diff --git a/lib/06/02/main.dart b/lib/06/02/main.dart index 77940c4..5d5047c 100644 --- a/lib/06/02/main.dart +++ b/lib/06/02/main.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'pages/page_a.dart'; import 'pages/page_b.dart'; -import 'pages/page_c.dart'; +import 'pages/home_page.dart'; void main() { runApp(const MyApp()); @@ -23,25 +23,26 @@ class MyApp extends StatelessWidget { titleTextStyle: TextStyle(color: Colors.black,fontSize: 18,fontWeight: FontWeight.bold) ) ), - home: const PageA(), - onUnknownRoute: _onUnknownRoute, + initialRoute: '/a/b', + onGenerateRoute: _onGenerateRoute, ); } - Route? _onUnknownRoute(RouteSettings settings) { + Route? _onGenerateRoute(RouteSettings settings) { String? name = settings.name; Widget? page ; + if(name=='/'){ + page = const HomePage(); + } if(name=='/a'){ page = const PageA(); } - if(name=='/b'){ + if(name=='/a/b'){ page = const PageB(); } - if(name=='/c'){ - page = const PageC(); - } + if(page!=null){ - return MaterialPageRoute(builder: (_)=> const PageB()); + return MaterialPageRoute(builder: (_)=> page!,settings: settings); } return null; } diff --git a/lib/06/02/pages/home_page.dart b/lib/06/02/pages/home_page.dart new file mode 100644 index 0000000..f1579ab --- /dev/null +++ b/lib/06/02/pages/home_page.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffFFE6CD); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text('主页面'), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: ()=>toPageA(context), + child: const Text('Push A'), + ), + ), + ); + } + + void toPageA(BuildContext context){ + Navigator.of(context).pushNamed('/a'); + } +} diff --git a/lib/06/02/pages/page_b.dart b/lib/06/02/pages/page_b.dart index a132264..f7a1814 100644 --- a/lib/06/02/pages/page_b.dart +++ b/lib/06/02/pages/page_b.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'page_c.dart'; +import 'home_page.dart'; class PageB extends StatelessWidget { const PageB({super.key}); @@ -17,13 +17,13 @@ class PageB extends StatelessWidget { body: Center( child: ElevatedButton( onPressed: ()=>toPageC(context), - child: const Text('Push C'), + child: const Text('回到主界面'), ), ), ); } void toPageC(BuildContext context){ - Navigator.of(context).push(MaterialPageRoute(builder: (_)=> const PageC())); + Navigator.of(context).popUntil(ModalRoute.withName('/')); } } \ No newline at end of file diff --git a/lib/07/02/main.dart b/lib/08/01/main.dart similarity index 100% rename from lib/07/02/main.dart rename to lib/08/01/main.dart diff --git a/lib/07/02/pages/page_a.dart b/lib/08/01/pages/page_a.dart similarity index 100% rename from lib/07/02/pages/page_a.dart rename to lib/08/01/pages/page_a.dart diff --git a/lib/07/02/pages/page_b.dart b/lib/08/01/pages/page_b.dart similarity index 100% rename from lib/07/02/pages/page_b.dart rename to lib/08/01/pages/page_b.dart diff --git a/lib/06/02/pages/page_c.dart b/lib/08/01/pages/page_c.dart similarity index 100% rename from lib/06/02/pages/page_c.dart rename to lib/08/01/pages/page_c.dart diff --git a/lib/07/03/main.dart b/lib/08/02/main.dart similarity index 100% rename from lib/07/03/main.dart rename to lib/08/02/main.dart diff --git a/lib/07/03/pages/page_a.dart b/lib/08/02/pages/page_a.dart similarity index 93% rename from lib/07/03/pages/page_a.dart rename to lib/08/02/pages/page_a.dart index c68192d..805b165 100644 --- a/lib/07/03/pages/page_a.dart +++ b/lib/08/02/pages/page_a.dart @@ -23,6 +23,6 @@ class PageA extends StatelessWidget { } void toPageB(BuildContext context){ - Navigator.of(context).pushNamed('/a'); + Navigator.of(context).pushNamed('/b'); } } diff --git a/lib/07/03/pages/page_b.dart b/lib/08/02/pages/page_b.dart similarity index 100% rename from lib/07/03/pages/page_b.dart rename to lib/08/02/pages/page_b.dart diff --git a/lib/07/03/pages/page_c.dart b/lib/08/02/pages/page_c.dart similarity index 100% rename from lib/07/03/pages/page_c.dart rename to lib/08/02/pages/page_c.dart diff --git a/lib/07/03/pages/page_d.dart b/lib/08/02/pages/page_d.dart similarity index 100% rename from lib/07/03/pages/page_d.dart rename to lib/08/02/pages/page_d.dart diff --git a/lib/08/节点介绍.txt b/lib/08/节点介绍.txt new file mode 100644 index 0000000..c7300da --- /dev/null +++ b/lib/08/节点介绍.txt @@ -0,0 +1,5 @@ +01: 分析 pushReplacement 方法。 + A、B、C 三个界面跳转。 + +02: 分析 pushAndRemoveUntil 方法。 + A、B、C、D 四个界面跳转。 \ No newline at end of file diff --git a/lib/09/01/main.dart b/lib/09/01/main.dart new file mode 100644 index 0000000..27f540b --- /dev/null +++ b/lib/09/01/main.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import 'pages/page_a.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + debugShowCheckedModeBanner: false, + theme: ThemeData( + appBarTheme: AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle(color: Colors.black,fontSize: 18,fontWeight: FontWeight.bold) + ) + ), + home: const PageA(), + ); + } +} diff --git a/lib/09/01/pages/page_a.dart b/lib/09/01/pages/page_a.dart new file mode 100644 index 0000000..940ff84 --- /dev/null +++ b/lib/09/01/pages/page_a.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'page_b.dart'; + +class PageA extends StatelessWidget { + const PageA({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffCCFFFF); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar(title: const Text('A'), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: ()=>toPageB(context), + child: const Text('Push B'), + ), + ), + ); + } + + void toPageB(BuildContext context){ + Navigator.of(context).push(MaterialPageRoute(builder: (_)=> const PageB())); + } +} diff --git a/lib/09/01/pages/page_b.dart b/lib/09/01/pages/page_b.dart new file mode 100644 index 0000000..a132264 --- /dev/null +++ b/lib/09/01/pages/page_b.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'page_c.dart'; + +class PageB extends StatelessWidget { + const PageB({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffCCE5FF); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar(title: const Text('B',), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: ()=>toPageC(context), + child: const Text('Push C'), + ), + ), + ); + } + + void toPageC(BuildContext context){ + Navigator.of(context).push(MaterialPageRoute(builder: (_)=> const PageC())); + } +} \ No newline at end of file diff --git a/lib/07/02/pages/page_c.dart b/lib/09/01/pages/page_c.dart similarity index 78% rename from lib/07/02/pages/page_c.dart rename to lib/09/01/pages/page_c.dart index ac595f0..5a1957a 100644 --- a/lib/07/02/pages/page_c.dart +++ b/lib/09/01/pages/page_c.dart @@ -12,6 +12,11 @@ class PageC extends StatelessWidget { appBar: AppBar( title: const Text('C'), backgroundColor: bgColor, + leading: BackButton( + onPressed: (){ + Navigator.of(context).pop(); + }, + ), ), body: const Center( child: Text('到达终点'), diff --git a/lib/09/02/main.dart b/lib/09/02/main.dart new file mode 100644 index 0000000..d966c58 --- /dev/null +++ b/lib/09/02/main.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'pages/page_a.dart'; +import 'pages/page_b.dart'; +import 'pages/page_c.dart'; +import 'pages/home_page.dart'; +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + debugShowCheckedModeBanner: false, + theme: ThemeData( + appBarTheme: AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle(color: Colors.black,fontSize: 18,fontWeight: FontWeight.bold) + ) + ), + onUnknownRoute: _onUnknownRoute, + ); + } + + Route? _onUnknownRoute(RouteSettings settings) { + String? name = settings.name; + Widget? page ; + if(name=='/'){ + page = const HomePage(); + } + if(name=='/a'){ + page = const PageA(); + } + if(name=='/b'){ + page = const PageB(); + } + if(name=='/c'){ + page = const PageC(); + } + if(page!=null){ + return MaterialPageRoute(builder: (_)=> page!,settings: settings); + } + return null; + } +} diff --git a/lib/09/02/pages/home_page.dart b/lib/09/02/pages/home_page.dart new file mode 100644 index 0000000..ebb98b5 --- /dev/null +++ b/lib/09/02/pages/home_page.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:iroute/06/02/pages/page_a.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffFFCCFF); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text('起点'), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: ()=>toPageA(context), + child: const Text('Push A'), + ), + ), + ); + } + + void toPageA(BuildContext context){ + Navigator.of(context).pushNamed('/a'); + } +} diff --git a/lib/09/02/pages/page_a.dart b/lib/09/02/pages/page_a.dart new file mode 100644 index 0000000..805b165 --- /dev/null +++ b/lib/09/02/pages/page_a.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'page_b.dart'; + +class PageA extends StatelessWidget { + const PageA({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffCCFFFF); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar(title: const Text('A'), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: ()=>toPageB(context), + child: const Text('Push B'), + ), + ), + ); + } + + void toPageB(BuildContext context){ + Navigator.of(context).pushNamed('/b'); + } +} diff --git a/lib/09/02/pages/page_b.dart b/lib/09/02/pages/page_b.dart new file mode 100644 index 0000000..b36561e --- /dev/null +++ b/lib/09/02/pages/page_b.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'page_c.dart'; + +class PageB extends StatelessWidget { + const PageB({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffCCE5FF); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar(title: const Text('B',), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: ()=>toPageC(context), + child: const Text('Push C'), + ), + ), + ); + } + + void toPageC(BuildContext context){ + Navigator.of(context).pushNamed('/c'); + } +} \ No newline at end of file diff --git a/lib/09/02/pages/page_c.dart b/lib/09/02/pages/page_c.dart new file mode 100644 index 0000000..f6ffee8 --- /dev/null +++ b/lib/09/02/pages/page_c.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + + +class PageC extends StatelessWidget { + const PageC({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffFFE6CD); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text('C'), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: ()=>toPageD(context), + child: const Text('pop Until A'), + ), + ), + ); + } + + void toPageD(BuildContext context){ + Navigator.of(context).popUntil( + ModalRoute.withName('/a') + ); + } +} + + + diff --git a/lib/12/01/main.dart b/lib/12/01/main.dart new file mode 100644 index 0000000..0ecfe3f --- /dev/null +++ b/lib/12/01/main.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'pages/app/app.dart'; + + +void main() { + runApp(const App()); +} + + + + diff --git a/lib/12/01/pages/app/app.dart b/lib/12/01/pages/app/app.dart new file mode 100644 index 0000000..ba3946c --- /dev/null +++ b/lib/12/01/pages/app/app.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'app_navigation.dart'; +import 'app_tool_bar.dart'; + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + appBarTheme: const AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ))), + debugShowCheckedModeBanner: false, + home: const Scaffold( + appBar: AppToolBar(), + body: AppNavigation(), + )); + } +} + diff --git a/lib/12/01/pages/app/app_navigation.dart b/lib/12/01/pages/app/app_navigation.dart new file mode 100644 index 0000000..75780da --- /dev/null +++ b/lib/12/01/pages/app/app_navigation.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import '../page_a.dart'; +import '../page_b.dart'; +import '../page_c.dart'; +import '../home_page.dart'; + +ValueNotifier> router = ValueNotifier(['/']); + +class AppNavigation extends StatefulWidget { + const AppNavigation({super.key}); + + @override + State createState() => _AppNavigationState(); +} + +class _AppNavigationState extends State { + + + @override + void dispose() { + router.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: router, + builder: (_, List value, __) => buildNavigatorByConfig(value), + ); + } + + final Map _pageMap = const { + '/': MaterialPage(child: HomePage()), + 'a': MaterialPage(child: PageA()), + 'b': MaterialPage(child: PageB()), + 'c': MaterialPage(child: PageC()), + }; + + Widget buildNavigatorByConfig(List value) { + return Navigator( + onPopPage: _onPopPage, + pages: router.value.map((e) => _pageMap[e]!).toList(), + ); + } + + bool _onPopPage(Route route, result) { + return route.didPop(result); + } +} diff --git a/lib/12/01/pages/app/app_tool_bar.dart b/lib/12/01/pages/app/app_tool_bar.dart new file mode 100644 index 0000000..002f6fd --- /dev/null +++ b/lib/12/01/pages/app/app_tool_bar.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'app_navigation.dart'; + +class AppToolBar extends StatefulWidget implements PreferredSizeWidget{ + const AppToolBar({super.key}); + + @override + State createState() => _AppToolBarState(); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _AppToolBarState extends State { + + TextEditingController _ctrl = TextEditingController(); + + @override + void initState() { + super.initState(); + _changRoute(); + router.addListener(_changRoute); + } + + @override + void dispose() { + router.removeListener(_changRoute); + _ctrl.dispose(); + super.dispose(); + } + + void _changRoute() { + _ctrl.text=router.value.join(','); + } + + @override + Widget build(BuildContext context) { + + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + title : TextField( + controller: _ctrl, + onSubmitted: _onSubmitted, + decoration: InputDecoration( //装饰 + filled: true, //填充 + fillColor: Color(0xffF3F6F9), //填充颜色 + constraints: BoxConstraints(maxHeight: 34), //约束信息 + contentPadding: EdgeInsets.only(top: -14,left: 10), + border: UnderlineInputBorder( //边线信息 + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + hintText: "请输入路由", //提示字 + hintStyle: TextStyle(fontSize: 14) //提示字样式 + ), + ), + ); + } + + void _onSubmitted(String value) { + router.value = value.split(','); + } +} diff --git a/lib/12/01/pages/home_page.dart b/lib/12/01/pages/home_page.dart new file mode 100644 index 0000000..2f9001a --- /dev/null +++ b/lib/12/01/pages/home_page.dart @@ -0,0 +1,29 @@ + +import 'package:flutter/material.dart'; + +import 'app/app_navigation.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffCCFFCC); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text('主页起点'), + backgroundColor: bgColor, + ), + body: Center(child: ElevatedButton( + onPressed: () => toPageA(context), + child: const Text('Push A'), + ), + )); + } + + void toPageA(BuildContext context) { + router.value = ['/', 'a']; + } +} diff --git a/lib/12/01/pages/page_a.dart b/lib/12/01/pages/page_a.dart new file mode 100644 index 0000000..ceca6a6 --- /dev/null +++ b/lib/12/01/pages/page_a.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'app/app_navigation.dart'; + +class PageA extends StatelessWidget { + const PageA({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffCCFFFF); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text('A 界面'), + backgroundColor: bgColor, + ), + body: Center( + child: ElevatedButton( + onPressed: () => toPageB(context), + child: const Text('Push B'), + ), + )); + } + + void toPageB(BuildContext context) { + router.value = ['/', 'a', 'b']; + } + +} diff --git a/lib/12/01/pages/page_b.dart b/lib/12/01/pages/page_b.dart new file mode 100644 index 0000000..bf4eb83 --- /dev/null +++ b/lib/12/01/pages/page_b.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import 'app/app_navigation.dart'; + + +class PageB extends StatelessWidget { + const PageB({super.key}); + + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffCCE5FF); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text('B 界面'), + backgroundColor: bgColor, + leading: BackButton( + onPressed: _pop, + ), + ), + body: Center( + child: ElevatedButton( + onPressed: () => toPageC(context), + child: const Text('Push C'), + ), + )); + } + + + void toPageC(BuildContext context){ + router.value = ['/','a','b','c']; + } + + void _pop() { + router.value = List.of(router.value)..removeLast(); + } +} \ No newline at end of file diff --git a/lib/12/01/pages/page_c.dart b/lib/12/01/pages/page_c.dart new file mode 100644 index 0000000..e37cdbe --- /dev/null +++ b/lib/12/01/pages/page_c.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'app/app_navigation.dart'; + +class PageC extends StatelessWidget { + const PageC({super.key}); + + @override + Widget build(BuildContext context) { + const Color bgColor = Color(0xffFFE6CC); + + return Scaffold( + backgroundColor: bgColor, + appBar: AppBar( + title: const Text('C 界面'), + backgroundColor: bgColor, + leading: BackButton( + onPressed: _pop, + ), + ), + body: Center( + child: Text('到达终点'), + )); + } + + void _pop() { + router.value = List.of(router.value)..removeLast(); + } +} diff --git a/lib/13/01/main.dart b/lib/13/01/main.dart new file mode 100644 index 0000000..75b32c0 --- /dev/null +++ b/lib/13/01/main.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:url_strategy/url_strategy.dart'; + +import 'pages/app.dart'; + + + +void main() { + setHashUrlStrategy(); + runApp(const App()); +} + + + + diff --git a/lib/13/01/pages/app.dart b/lib/13/01/pages/app.dart new file mode 100644 index 0000000..cfd36b8 --- /dev/null +++ b/lib/13/01/pages/app.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import '../route/parser.dart'; +import '../route/route_state.dart'; + +class App extends StatefulWidget { + const App({super.key}); + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + late final RootRouterDelegate _delegate; + late final AppRouteParser _parser; + late final RouteState _routeState; + + @override + void initState() { + super.initState(); + _parser = AppRouteParser(initialRoute: '/color', allowPaths: [ + '/color', + '/color/add', + '/color/detail/:colorHex', + ]); + _routeState = RouteState(_parser); + _delegate = RootRouterDelegate(_routeState); + } + + @override + Widget build(BuildContext context) { + return RouteStateScope( + notifier: _routeState, + child: MaterialApp.router( + routerDelegate: _delegate, + routeInformationParser: _parser, + theme: ThemeData( + useMaterial3: true, + appBarTheme: const AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ))), + debugShowCheckedModeBanner: false, + ), + ); + } +} diff --git a/lib/13/01/pages/color_add_page.dart b/lib/13/01/pages/color_add_page.dart new file mode 100644 index 0000000..bee43b4 --- /dev/null +++ b/lib/13/01/pages/color_add_page.dart @@ -0,0 +1,67 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class ColorAddPage extends StatefulWidget { + const ColorAddPage({super.key}); + + @override + State createState() => _ColorAddPageState(); +} + +class _ColorAddPageState extends State { + late Color _color; + + @override + void initState() { + super.initState(); + _color = randomColor; + } + + @override + Widget build(BuildContext context) { + String text = '# ${_color.value.toRadixString(16)}'; + return Scaffold( + appBar: AppBar( + title: Text('添加颜色'), + actions: [IconButton(onPressed: _selectColor, icon: Icon(Icons.check ))], + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 20), + child: Row( + children: [ + Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)), + Container( + margin: EdgeInsets.only(left: 10), + width: 40, + height: 40, + child: Icon( + Icons.sell_outlined, + color: Colors.white, + ), + decoration: BoxDecoration( + color: _color, + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), + ), + ); + } + + Random _random = Random(); + + Color get randomColor { + return Color.fromARGB( + 255, + _random.nextInt(256), + _random.nextInt(256), + _random.nextInt(256), + ); + } + + void _selectColor() { + Navigator.of(context).pop(_color); + } +} diff --git a/lib/13/01/pages/home_page.dart b/lib/13/01/pages/home_page.dart new file mode 100644 index 0000000..47b4dc9 --- /dev/null +++ b/lib/13/01/pages/home_page.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import '../route/route_state.dart'; +import 'color_add_page.dart'; + +import '../../../common/components/colors_panel.dart'; +import '../../../common/pages/stl_color_page.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final List _colors = [ + Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange, + Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan, + Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent, + Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent, + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title:const Text('颜色主页')), + floatingActionButton: FloatingActionButton( + onPressed: _toAddPage, + child: const Icon(Icons.add), + ), + body: ColorsPanel( + colors: _colors, + onSelect: _selectColor, + ), + ); + } + + void _selectColor(Color color){ + String value = '#${color.value.toRadixString(16)}'; + RouteStateScope.of(context).go('/color/detail/$value'); + } + + void _toAddPage() async { + RouteStateScope.of(context).go('/color/add'); + } +} diff --git a/lib/13/01/route/parsed_route.dart b/lib/13/01/route/parsed_route.dart new file mode 100644 index 0000000..f03f0a8 --- /dev/null +++ b/lib/13/01/route/parsed_route.dart @@ -0,0 +1,27 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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:equatable/equatable.dart'; +import 'package:quiver/core.dart'; + +/// A route path that has been parsed by [TemplateRouteParser]. +class ParsedRoute extends Equatable{ + /// The current path location without query parameters. (/book/123) + final String path; + + /// The path template (/book/:id) + final String pathTemplate; + + /// The path parameters ({id: 123}) + final Map parameters; + + /// The query parameters ({search: abc}) + final Map queryParameters; + + const ParsedRoute(this.path, this.pathTemplate, this.parameters, this.queryParameters); + + @override + List get props => [path,pathTemplate,parameters,queryParameters]; +} diff --git a/lib/13/01/route/parser.dart b/lib/13/01/route/parser.dart new file mode 100644 index 0000000..bb85717 --- /dev/null +++ b/lib/13/01/route/parser.dart @@ -0,0 +1,135 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:iroute/common/pages/stl_color_page.dart'; +import 'package:path_to_regexp/path_to_regexp.dart'; +import '../pages/home_page.dart'; +import '../pages/color_add_page.dart'; +import 'parsed_route.dart'; +import 'route_state.dart'; + +class AppRouteParser extends RouteInformationParser { + final List allowPaths; + final ParsedRoute initialRoute; + + AppRouteParser({ + required this.allowPaths, + String initialRoute = '/', + }):initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {}); + + @override + Future parseRouteInformation( + RouteInformation routeInformation) async { + print("=======parseRouteInformation:${routeInformation.uri.path}==================="); + + final uri = routeInformation.uri; + final path = uri.toString(); + final queryParams = uri.queryParameters; + + ParsedRoute parsedRoute = initialRoute; + + for (var pathTemplate in allowPaths) { + final parameters = []; + var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters); + if (pathRegExp.hasMatch(path)) { + final match = pathRegExp.matchAsPrefix(path); + if (match == null) continue; + final params = extract(parameters, match); + parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams); + } + } + return parsedRoute; + } + + @override + RouteInformation? restoreRouteInformation(ParsedRoute configuration) { + print("=======restoreRouteInformation:${configuration}==================="); + + return RouteInformation(uri: Uri.parse(configuration.path)); + } +} + +class RootRouterDelegate extends RouterDelegate + with ChangeNotifier, PopNavigatorRouterDelegateMixin { + + final RouteState routeState; + + + RootRouterDelegate(this.routeState) { + routeState.addListener(notifyListeners); + } + + @override + final GlobalKey navigatorKey = GlobalKey(); + + @override + ParsedRoute get currentConfiguration { + print("=======currentConfiguration:${routeState.route.path}==================="); + + return routeState.route; + } + + @override + void dispose() { + routeState.removeListener(notifyListeners); + routeState.dispose(); + super.dispose(); + } + final _addColorKey = const ValueKey('addColor'); + + @override + Widget build(BuildContext context) { + final routeState = RouteStateScope.of(context); + List pages =[]; + final String pathTemplate = routeState.route.pathTemplate; + + pages.add(MaterialPage(child: HomePage())); + + String? selectedColor; + if (pathTemplate == '/color/detail/:colorHex') { + selectedColor = routeState.route.parameters['colorHex']; + } + + if(selectedColor!=null){ + pages.add(MaterialPage(child: StlColorPage( + color: Colors.redAccent, + ))); + } + + bool isAddPage = pathTemplate == '/color/add'; + if(isAddPage){ + pages.add(MaterialPage( + key: _addColorKey, + child: ColorAddPage())); + } + print("===build===================="); + return Navigator( + key: navigatorKey, + pages: pages, + onPopPage: (route, result) { + print(result); + if (route.settings is Page && + (route.settings as Page).key == _addColorKey) { + routeState.go('/color'); + } + // appState.selectedBook = null; + // notifyListeners(); + return route.didPop(result); + }, + ); + } + + /// 格式: /a /b + + @override + Future setNewRoutePath(ParsedRoute configuration) async { + print("===setNewRoutePath===${configuration}================="); + routeState.route = configuration; + return; + } + +// @override +// Future setInitialRoutePath( configuration) async{ +// _pages = [_pageMap['/']!]; +// notifyListeners(); +// } +} diff --git a/lib/13/01/route/route_state.dart b/lib/13/01/route/route_state.dart new file mode 100644 index 0000000..a2d2e0f --- /dev/null +++ b/lib/13/01/route/route_state.dart @@ -0,0 +1,48 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 'parsed_route.dart'; +import 'parser.dart'; + +/// The current route state. To change the current route, call obtain the state +/// using `RouteStateScope.of(context)` and call `go()`: +/// +/// ``` +/// RouteStateScope.of(context).go('/book/2'); +/// ``` +class RouteState extends ChangeNotifier { + final AppRouteParser _parser; + ParsedRoute _route; + + RouteState(this._parser) : _route = _parser.initialRoute; + + ParsedRoute get route => _route; + + set route(ParsedRoute route) { + // Don't notify listeners if the path hasn't changed. + if (_route == route) return; + + _route = route; + notifyListeners(); + } + + Future go(String route) async { + this.route = await _parser + .parseRouteInformation(RouteInformation(uri: Uri.parse(route))); + } +} + +/// Provides the current [RouteState] to descendant widgets in the tree. +class RouteStateScope extends InheritedNotifier { + const RouteStateScope({ + required super.notifier, + required super.child, + super.key, + }); + + static RouteState of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.notifier!; +} diff --git a/lib/13/02/main.dart b/lib/13/02/main.dart new file mode 100644 index 0000000..75b32c0 --- /dev/null +++ b/lib/13/02/main.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:url_strategy/url_strategy.dart'; + +import 'pages/app.dart'; + + + +void main() { + setHashUrlStrategy(); + runApp(const App()); +} + + + + diff --git a/lib/13/02/pages/app.dart b/lib/13/02/pages/app.dart new file mode 100644 index 0000000..650d6b4 --- /dev/null +++ b/lib/13/02/pages/app.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:iroute/13/02/store/app_state.dart'; +import '../route/parser.dart'; +import '../route/route_state.dart'; + +class App extends StatefulWidget { + const App({super.key}); + + @override + State createState() => _AppState(); +} + +class _AppState extends State { + late final RootRouterDelegate _delegate; + late final AppRouteParser _parser; + late final AppState _appState; + late final RouteState _routeState; + + @override + void initState() { + super.initState(); + _parser = AppRouteParser(initialRoute: '/color', allowPaths: [ + '/color', + '/color/add', + '/color/detail/:colorHex', + ]); + _appState = AppState.initial(); + _routeState = RouteState(_parser); + _delegate = RootRouterDelegate(_routeState); + } + + @override + Widget build(BuildContext context) { + return AppStateScope( + notifier: _appState, + child: RouteStateScope( + notifier: _routeState, + child: MaterialApp.router( + routerDelegate: _delegate, + routeInformationParser: _parser, + theme: ThemeData( + useMaterial3: true, + appBarTheme: const AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ))), + debugShowCheckedModeBanner: false, + ), + ), + ); + } +} diff --git a/lib/13/02/pages/color_add_page.dart b/lib/13/02/pages/color_add_page.dart new file mode 100644 index 0000000..bee43b4 --- /dev/null +++ b/lib/13/02/pages/color_add_page.dart @@ -0,0 +1,67 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class ColorAddPage extends StatefulWidget { + const ColorAddPage({super.key}); + + @override + State createState() => _ColorAddPageState(); +} + +class _ColorAddPageState extends State { + late Color _color; + + @override + void initState() { + super.initState(); + _color = randomColor; + } + + @override + Widget build(BuildContext context) { + String text = '# ${_color.value.toRadixString(16)}'; + return Scaffold( + appBar: AppBar( + title: Text('添加颜色'), + actions: [IconButton(onPressed: _selectColor, icon: Icon(Icons.check ))], + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 20), + child: Row( + children: [ + Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)), + Container( + margin: EdgeInsets.only(left: 10), + width: 40, + height: 40, + child: Icon( + Icons.sell_outlined, + color: Colors.white, + ), + decoration: BoxDecoration( + color: _color, + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), + ), + ); + } + + Random _random = Random(); + + Color get randomColor { + return Color.fromARGB( + 255, + _random.nextInt(256), + _random.nextInt(256), + _random.nextInt(256), + ); + } + + void _selectColor() { + Navigator.of(context).pop(_color); + } +} diff --git a/lib/13/02/pages/home_page.dart b/lib/13/02/pages/home_page.dart new file mode 100644 index 0000000..a4653e6 --- /dev/null +++ b/lib/13/02/pages/home_page.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:iroute/13/02/store/app_state.dart'; +import '../route/route_state.dart'; +import 'color_add_page.dart'; + +import '../../../common/components/colors_panel.dart'; +import '../../../common/pages/stl_color_page.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + + + @override + Widget build(BuildContext context) { + AppState state = AppStateScope.of(context); + return Scaffold( + appBar: AppBar(title:const Text('颜色主页')), + floatingActionButton: FloatingActionButton( + onPressed: _toAddPage, + child: const Icon(Icons.add), + ), + body: ColorsPanel( + colors: state.colors, + onSelect: _selectColor, + ), + ); + } + + void _selectColor(Color color){ + String value = '#${color.value.toRadixString(16)}'; + RouteStateScope.of(context).update('/color/detail/$value'); + } + + void _toAddPage() async { + RouteStateScope.of(context).update('/color/add'); + } +} diff --git a/lib/13/02/route/parsed_route.dart b/lib/13/02/route/parsed_route.dart new file mode 100644 index 0000000..778d667 --- /dev/null +++ b/lib/13/02/route/parsed_route.dart @@ -0,0 +1,49 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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:quiver/core.dart'; + +/// A route path that has been parsed by [TemplateRouteParser]. +class ParsedRoute { + /// The current path location without query parameters. (/book/123) + final String path; + + /// The path template (/book/:id) + final String pathTemplate; + + /// The path parameters ({id: 123}) + final Map parameters; + + /// The query parameters ({search: abc}) + final Map queryParameters; + + static const _mapEquality = MapEquality(); + + ParsedRoute( + this.path, this.pathTemplate, this.parameters, this.queryParameters); + + @override + bool operator ==(Object other) => + other is ParsedRoute && + other.pathTemplate == pathTemplate && + other.path == path && + _mapEquality.equals(parameters, other.parameters) && + _mapEquality.equals(queryParameters, other.queryParameters); + + @override + int get hashCode => hash4( + path, + pathTemplate, + _mapEquality.hash(parameters), + _mapEquality.hash(queryParameters), + ); + + @override + String toString() => ''; +} diff --git a/lib/13/02/route/parser.dart b/lib/13/02/route/parser.dart new file mode 100644 index 0000000..f27a266 --- /dev/null +++ b/lib/13/02/route/parser.dart @@ -0,0 +1,153 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:iroute/common/pages/stl_color_page.dart'; +import 'package:path_to_regexp/path_to_regexp.dart'; +import '../pages/home_page.dart'; +import '../pages/color_add_page.dart'; +import '../store/app_state.dart'; +import 'parsed_route.dart'; +import 'route_state.dart'; + +class AppRouteParser extends RouteInformationParser { + final List allowPaths; + final ParsedRoute initialRoute; + + AppRouteParser({ + required this.allowPaths, + String initialRoute = '/', + }) : initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {}); + + @override + Future parseRouteInformation( + RouteInformation routeInformation) async { + print( + "=======parseRouteInformation:${routeInformation.uri.path}==================="); + + final uri = routeInformation.uri; + final path = uri.toString(); + final queryParams = uri.queryParameters; + + ParsedRoute parsedRoute = initialRoute; + + for (var pathTemplate in allowPaths) { + final parameters = []; + var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters); + if (pathRegExp.hasMatch(path)) { + final match = pathRegExp.matchAsPrefix(path); + if (match == null) continue; + final params = extract(parameters, match); + parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams); + } + } + return parsedRoute; + } + + @override + RouteInformation? restoreRouteInformation(ParsedRoute configuration) { + print("=======restoreRouteInformation:${configuration}==================="); + + return RouteInformation(uri: Uri.parse(configuration.path)); + } +} + +class RootRouterDelegate extends RouterDelegate + with ChangeNotifier, PopNavigatorRouterDelegateMixin { + final RouteState routeState; + + RootRouterDelegate(this.routeState) { + routeState.addListener(notifyListeners); + } + + @override + final GlobalKey navigatorKey = GlobalKey(); + + @override + ParsedRoute get currentConfiguration { + print("=======currentConfiguration:${routeState.route.path}==================="); + + return routeState.route; + } + + @override + void dispose() { + routeState.removeListener(notifyListeners); + routeState.dispose(); + super.dispose(); + } + + final _addColorKey = const ValueKey('addColor'); + final _detailColorKey = const ValueKey('detailColor'); + + @override + Widget build(BuildContext context) { + final routeState = RouteStateScope.of(context); + List pages = []; + final String pathTemplate = routeState.route.pathTemplate; + + pages.add(MaterialPage(child: HomePage())); + + String? selectedColor; + + if (pathTemplate == '/color/detail/:colorHex') { + selectedColor = routeState.route.parameters['colorHex']; + } + + if (selectedColor != null) { + int value = int.parse( + selectedColor.substring( + 1, + ), + radix: 16); + pages.add(MaterialPage( + key: _detailColorKey, + child: StlColorPage( + color: Color(value), + ))); + } + + bool isAddPage = pathTemplate == '/color/add'; + if (isAddPage) { + pages.add( + MaterialPage(key: _addColorKey, child: ColorAddPage()), + ); + } + print("===build===================="); + return Navigator( + key: navigatorKey, + pages: pages, + onPopPage: (route, result) { + print(result); + if (route.settings is Page && + (route.settings as Page).key == _addColorKey) { + if(result is Color){ + AppStateScope.of(context).addColor(result); + } + routeState.update('/color'); + } + + if (route.settings is Page && + (route.settings as Page).key == _detailColorKey) { + routeState.update('/color'); + } + // appState.selectedBook = null; + // notifyListeners(); + return route.didPop(result); + }, + ); + } + + /// 格式: /a /b + + @override + Future setNewRoutePath(ParsedRoute configuration) async { + print("===setNewRoutePath===${configuration}================="); + routeState.route = configuration; + return; + } + +// @override +// Future setInitialRoutePath( configuration) async{ +// _pages = [_pageMap['/']!]; +// notifyListeners(); +// } +} diff --git a/lib/13/02/route/route_state.dart b/lib/13/02/route/route_state.dart new file mode 100644 index 0000000..a2ef01a --- /dev/null +++ b/lib/13/02/route/route_state.dart @@ -0,0 +1,48 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 'parsed_route.dart'; +import 'parser.dart'; + +/// The current route state. To change the current route, call obtain the state +/// using `RouteStateScope.of(context)` and call `go()`: +/// +/// ``` +/// RouteStateScope.of(context).go('/book/2'); +/// ``` +class RouteState extends ChangeNotifier { + final AppRouteParser _parser; + ParsedRoute _route; + + RouteState(this._parser) : _route = _parser.initialRoute; + + ParsedRoute get route => _route; + + set route(ParsedRoute route) { + // Don't notify listeners if the path hasn't changed. + if (_route == route) return; + + _route = route; + notifyListeners(); + } + + Future update(String route) async { + this.route = await _parser + .parseRouteInformation(RouteInformation(uri: Uri.parse(route))); + } +} + +/// Provides the current [RouteState] to descendant widgets in the tree. +class RouteStateScope extends InheritedNotifier { + const RouteStateScope({ + required super.notifier, + required super.child, + super.key, + }); + + static RouteState of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.notifier!; +} diff --git a/lib/13/02/store/app_state.dart b/lib/13/02/store/app_state.dart new file mode 100644 index 0000000..57d820f --- /dev/null +++ b/lib/13/02/store/app_state.dart @@ -0,0 +1,32 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class AppState extends ChangeNotifier{ + final List colors; + + AppState({required this.colors,}); + + AppState.initial():colors=[ + Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange, + Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan, + Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent, + Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent, + ]; + + void addColor(Color color){ + colors.add(color); + notifyListeners(); + } + +} + +/// Provides the current [RouteState] to descendant widgets in the tree. +class AppStateScope extends InheritedNotifier { + const AppStateScope({super.key, required super.child, required super.notifier,}); + + + + static AppState of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.notifier!; +} diff --git a/lib/13/02_/main.dart b/lib/13/02_/main.dart new file mode 100644 index 0000000..881b6ec --- /dev/null +++ b/lib/13/02_/main.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +import 'pages/app/app.dart'; + + + +void main() { + runApp(const App()); +} + + + + diff --git a/lib/13/02_/menus.dart b/lib/13/02_/menus.dart new file mode 100644 index 0000000..8533c5d --- /dev/null +++ b/lib/13/02_/menus.dart @@ -0,0 +1,83 @@ +import 'package:toly_menu/toly_menu.dart'; + +List menus = [ + MenuData(path: '/dashboard', label: '总览面板', children: [ + MenuData(path: '/dashboard/data_analyse', label: '数据分析', deep: 1), + MenuData(path: '/dashboard/work_board', label: '工作台', deep: 1, children: [ + MenuData(path: '/dashboard/work_board/a', label: '第一工作区', deep: 2), + MenuData(path: '/dashboard/work_board/b', label: '第二工作区', deep: 2), + MenuData(path: '/dashboard/work_board/c', label: '第三工作区', deep: 2), + ]), + ]), + MenuData(path: '/cases', label: '案例演示', children: [ + MenuData(path: '/cases/counter', label: '计数器项目', deep: 1), + MenuData(path: '/cases/guess', label: '猜数字项目', deep: 1), + MenuData(path: '/cases/muyu', label: '电子木鱼项目', deep: 1), + MenuData(path: '/cases/canvas', label: '白板绘制项目', deep: 1), + MenuData(path: '/cases/timer', label: '计时器项目', deep: 1), + ]), + MenuData(path: '/manager', label: '系统管理', children: [ + MenuData(path: '/manager/account', label: '账号管理', deep: 1), + MenuData(path: '/manager/role', label: '角色管理', deep: 1), + MenuData(path: '/manager/menu', label: '菜单管理', deep: 1), + ]), + MenuData(path: '/knowledge', label: '知识库管理', children: [ + MenuData(path: '/knowledge/a', label: '语文知识', deep: 1, children: [ + MenuData( + path: '/knowledge/a/1', + label: '诗词歌赋', + deep: 2, + ), + MenuData( + path: '/knowledge/a/2', + label: '人物故事', + deep: 2, + ), + MenuData( + path: '/knowledge/a/3', + label: '名著推荐', + deep: 2, + ) + ]), + MenuData( + path: '/knowledge/b', + label: '数学知识', + children: [ + MenuData( + path: '/knowledge/b/1', + label: '人物故事', + deep: 2, + ), + MenuData( + path: '/knowledge/b/2', + label: '数学定理', + deep: 2, + ), + MenuData( + path: '/knowledge/b/3', + label: '几何知识', + deep: 2, + ), + MenuData( + path: '/knowledge/b/4', + label: '代数知识', + deep: 2, + ) + ], + deep: 1), + MenuData( + path: '/knowledge/c', + label: '英语知识', + deep: 1, + ), + ]), + MenuData(path: '/widgets', label: '组件集录', children: [ + MenuData(path: '/widgets/stateless', label: '无状态组件', deep: 1), + MenuData(path: '/widgets/stateful', label: '有状态组件', deep: 1), + MenuData(path: '/widgets/single_child', label: '单子渲染组件', deep: 1), + MenuData(path: '/widgets/mutli_child', label: '多子渲染组件', deep: 1), + MenuData(path: '/widgets/sliver', label: '滑片组件', deep: 1), + MenuData(path: '/widgets/proxy', label: '代理组件', deep: 1), + MenuData(path: '/widgets/other', label: '其他组件', deep: 1), + ]) +]; diff --git a/lib/13/02_/pages/app/app.dart b/lib/13/02_/pages/app/app.dart new file mode 100644 index 0000000..3349008 --- /dev/null +++ b/lib/13/02_/pages/app/app.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:toly_menu/toly_menu.dart'; + +import '../../menus.dart'; +import '../../transition/fade_transition.dart'; +import 'root_content.dart'; + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + pageTransitionsTheme: PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.macOS: FadeUpwardsPageTransitionsBuilder(), + TargetPlatform.windows: FadeTransitionsBuilder(), + } + ), + + appBarTheme: const AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + backgroundColor: Colors.white, + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ))), + debugShowCheckedModeBanner: false, + home: AppLayout()); + } +} + +class AppLayout extends StatelessWidget { + const AppLayout({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + SizedBox( + width: 210, + child: MenuPanel(), + ), + // Container() + Expanded(child: RootContent()) + ], + ), + // appBar: AppToolBar(), + // body: AppNavigation(), + ); + } +} + +class MenuPanel extends StatefulWidget { + const MenuPanel({super.key}); + + @override + State createState() => _MenuPanelState(); +} + +class _MenuPanelState extends State { + MenuState state = MenuState(expandMenus: ['/dashboard'], activeMenu: '/dashboard/data_analyse', items: menus); + + @override + Widget build(BuildContext context) { + return TolyMenu(state: state, onSelect: _onSelect); + } + + void _onSelect(MenuData menu) { + + + if(menu.isLeaf){ + state = state.copyWith(activeMenu: menu.path); + print("menu.path:${menu.path}"); + goRouter.go(menu.path); + }else{ + List menus = []; + String path = menu.path.substring(1); + List parts = path.split('/'); + + if(parts.isNotEmpty){ + String path = ''; + for (String part in parts) { + path+='/$part'; + menus.add(path); + } + } + + if(state.expandMenus.contains(menu.path)){ + menus.remove(menu.path); + } + + state = state.copyWith(expandMenus: menus); + + } + setState(() { + + }); + } +} diff --git a/lib/13/02_/pages/app/root_content.dart b/lib/13/02_/pages/app/root_content.dart new file mode 100644 index 0000000..a7d78cc --- /dev/null +++ b/lib/13/02_/pages/app/root_content.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:iroute/cases/cases.dart'; + +import '../color_page.dart'; + +// GoRouter configuration +final goRouter = GoRouter( + initialLocation: '/manager', + routes: [ + GoRoute( + path: '/dashboard', + builder: (_,__)=>SizedBox(), + routes: [ + GoRoute( + path: 'data_analyse', + // builder: (context, state) => ColorPage(title: 'data_analyse',), + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: ColorPage(title: 'data_analyse',), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + // Change the opacity of the screen using a Curve based on the the animation's + // value + return FadeTransition( + opacity: + CurveTween(curve: Curves.easeInOutCirc).animate(animation), + child: child, + ); + }, + ); + }, + ), + + GoRoute( + path: 'work_board', + builder: (_,__)=>SizedBox(), + routes: [ + GoRoute( + path: 'a', + builder: (context, state) => ColorPage(title: '第一工作区',), + ), + GoRoute( + path: 'b', + builder: (context, state) => ColorPage(title: '第二工作区',), + ), + GoRoute( + path: 'c', + builder: (context, state) => ColorPage(title: '第三工作区',), + ), + ] + ), + + ] + ), + + GoRoute( + path: '/cases', + builder: (_,__)=>SizedBox(), + routes: [ + GoRoute( + path: 'counter', + builder: (context, state) => CounterPage(), + ), + GoRoute( + path: 'guess', + builder: (context, state) => GuessPage(), + ), + GoRoute( + path: 'muyu', + builder: (context, state) => ColorPage(title: '第三工作区',), + ), GoRoute( + path: 'canvas', + builder: (context, state) => Paper(), + ), GoRoute( + path: 'muyu', + builder: (context, state) => ColorPage(title: '第三工作区',), + ), + ] + ), + GoRoute( + path: '/manager', builder: (_,__)=>SizedBox(), + routes: [ + GoRoute( + path: 'account', + builder: (context, state) => ColorPage(title: 'account',), + ), + GoRoute( + path: 'role', + builder: (context, state) => ColorPage(title: 'role',), + ), + ] + ), + ], +); + + +class RootContent extends StatelessWidget { + const RootContent({super.key}); + + @override + Widget build(BuildContext context) { + + + return Router.withConfig(config: goRouter); + } +} + +class RootContentDelegate extends RouterDelegate with ChangeNotifier,PopNavigatorRouterDelegateMixin{ + + + @override + final GlobalKey navigatorKey = GlobalKey(); + + + @override + Widget build(BuildContext context) { + return Navigator( + key: navigatorKey, + pages: [ + + ], + onPopPage: (route, result) { + // appState.selectedBook = null; + // notifyListeners(); + return route.didPop(result); + }, + ); + } + + + @override + Future setNewRoutePath(configuration) { + // TODO: implement setNewRoutePath + throw UnimplementedError(); + } + + + +} \ No newline at end of file diff --git a/lib/13/02_/pages/color_page.dart b/lib/13/02_/pages/color_page.dart new file mode 100644 index 0000000..08449bb --- /dev/null +++ b/lib/13/02_/pages/color_page.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class ColorPage extends StatelessWidget { + final String title; + const ColorPage({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Scaffold(body: Center(child: Text(title))); + } +} diff --git a/lib/13/02_/transition/fade_transition.dart b/lib/13/02_/transition/fade_transition.dart new file mode 100644 index 0000000..751b9bb --- /dev/null +++ b/lib/13/02_/transition/fade_transition.dart @@ -0,0 +1,58 @@ + + + + +import 'package:flutter/material.dart'; + + +class FadeTransitionsBuilder extends PageTransitionsBuilder { + /// Constructs a page transition animation that slides the page up. + const FadeTransitionsBuilder(); + + @override + Widget buildTransitions( + PageRoute? route, + BuildContext? context, + Animation animation, + Animation? secondaryAnimation, + Widget child, + ) { + return _FadeUpwardsPageTransition(routeAnimation: animation, child: child); + } +} + +class _FadeUpwardsPageTransition extends StatelessWidget { + _FadeUpwardsPageTransition({ + required Animation routeAnimation, // The route's linear 0.0 - 1.0 animation. + required this.child, + }) : _positionAnimation = routeAnimation.drive(_bottomUpTween.chain(_fastOutSlowInTween)), + _opacityAnimation = routeAnimation.drive(_easeInTween); + + // Fractional offset from 1/4 screen below the top to fully on screen. + static final Tween _bottomUpTween = Tween( + begin: const Offset(0.25,0), + end: Offset.zero, + ); + static final Animatable _fastOutSlowInTween = CurveTween(curve: Curves.fastOutSlowIn); + static final Animatable _easeInTween = CurveTween(curve: Curves.easeIn); + + final Animation _positionAnimation; + final Animation _opacityAnimation; + final Widget child; + + @override + Widget build(BuildContext context) { + // return FadeTransition( + // opacity: _opacityAnimation, + // child: child, + // ); + + return SlideTransition( + position: _positionAnimation, + child: FadeTransition( + opacity: _opacityAnimation, + child: child, + ), + ); + } +} \ No newline at end of file diff --git a/lib/13/03/main.dart b/lib/13/03/main.dart new file mode 100644 index 0000000..881b6ec --- /dev/null +++ b/lib/13/03/main.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +import 'pages/app/app.dart'; + + + +void main() { + runApp(const App()); +} + + + + diff --git a/lib/13/03/menus.dart b/lib/13/03/menus.dart new file mode 100644 index 0000000..1aa20cb --- /dev/null +++ b/lib/13/03/menus.dart @@ -0,0 +1,76 @@ +import 'package:toly_menu/toly_menu.dart'; + +List menus = [ + MenuData(path: '/dashboard', label: '总览面板', children: [ + MenuData(path: '/dashboard/data_analyse', label: '数据分析', deep: 1), + MenuData(path: '/dashboard/work_board', label: '工作台', deep: 1, children: [ + MenuData(path: '/dashboard/work_board/a', label: '第一工作区', deep: 2), + MenuData(path: '/dashboard/work_board/b', label: '第二工作区', deep: 2), + MenuData(path: '/dashboard/work_board/c', label: '第三工作区', deep: 2), + ]), + ]), + MenuData(path: '/manager', label: '系统管理', children: [ + MenuData(path: '/manager/account', label: '账号管理', deep: 1), + MenuData(path: '/manager/role', label: '角色管理', deep: 1), + MenuData(path: '/manager/menu', label: '菜单管理', deep: 1), + ]), + MenuData(path: '/knowledge', label: '知识库管理', children: [ + MenuData(path: '/knowledge/a', label: '语文知识', deep: 1, children: [ + MenuData( + path: '/knowledge/a/1', + label: '诗词歌赋', + deep: 2, + ), + MenuData( + path: '/knowledge/a/2', + label: '人物故事', + deep: 2, + ), + MenuData( + path: '/knowledge/a/3', + label: '名著推荐', + deep: 2, + ) + ]), + MenuData( + path: '/knowledge/b', + label: '数学知识', + children: [ + MenuData( + path: '/knowledge/b/1', + label: '人物故事', + deep: 2, + ), + MenuData( + path: '/knowledge/b/2', + label: '数学定理', + deep: 2, + ), + MenuData( + path: '/knowledge/b/3', + label: '几何知识', + deep: 2, + ), + MenuData( + path: '/knowledge/b/4', + label: '代数知识', + deep: 2, + ) + ], + deep: 1), + MenuData( + path: '/knowledge/c', + label: '英语知识', + deep: 1, + ), + ]), + MenuData(path: '/widgets', label: '组件集录', children: [ + MenuData(path: '/widgets/stateless', label: '无状态组件', deep: 1), + MenuData(path: '/widgets/stateful', label: '有状态组件', deep: 1), + MenuData(path: '/widgets/single_child', label: '单子渲染组件', deep: 1), + MenuData(path: '/widgets/mutli_child', label: '多子渲染组件', deep: 1), + MenuData(path: '/widgets/sliver', label: '滑片组件', deep: 1), + MenuData(path: '/widgets/proxy', label: '代理组件', deep: 1), + MenuData(path: '/widgets/other', label: '其他组件', deep: 1), + ]) +]; diff --git a/lib/13/03/pages/app/app.dart b/lib/13/03/pages/app/app.dart new file mode 100644 index 0000000..b6b3ac2 --- /dev/null +++ b/lib/13/03/pages/app/app.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:toly_menu/toly_menu.dart'; + +import '../../menus.dart'; + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + appBarTheme: const AppBarTheme( + elevation: 0, + iconTheme: IconThemeData(color: Colors.black), + titleTextStyle: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ))), + debugShowCheckedModeBanner: false, + home: AppLayout()); + } +} + +class AppLayout extends StatelessWidget { + const AppLayout({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + SizedBox( + width: 210, + child: MenuPanel(), + ), + // Container() + ], + ), + // appBar: AppToolBar(), + // body: AppNavigation(), + ); + } +} + +class MenuPanel extends StatefulWidget { + const MenuPanel({super.key}); + + @override + State createState() => _MenuPanelState(); +} + +class _MenuPanelState extends State { + MenuState state = MenuState(expandMenus: ['/dashboard'], activeMenu: '/dashboard/data_analyse', items: menus); + + @override + Widget build(BuildContext context) { + return TolyMenu(state: state, onSelect: _onSelect); + } + + void _onSelect(MenuData menu) { + if(menu.isLeaf){ + state = state.copyWith(activeMenu: menu.path); + }else{ + List menus = []; + String path = menu.path.substring(1); + List parts = path.split('/'); + + if(parts.isNotEmpty){ + String path = ''; + for (String part in parts) { + path+='/$part'; + menus.add(path); + } + } + + if(state.expandMenus.contains(menu.path)){ + menus.remove(menu.path); + } + + state = state.copyWith(expandMenus: menus); + + } + setState(() { + + }); + } +} diff --git a/lib/13/03/pages/app/root_content.dart b/lib/13/03/pages/app/root_content.dart new file mode 100644 index 0000000..36c46b5 --- /dev/null +++ b/lib/13/03/pages/app/root_content.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class RootContent extends StatelessWidget { + const RootContent({super.key}); + + @override + Widget build(BuildContext context) { + return Router(routerDelegate: RootContentDelegate()); + } +} + +class RootContentDelegate extends RouterDelegate with ChangeNotifier,PopNavigatorRouterDelegateMixin{ + + + @override + final GlobalKey navigatorKey = GlobalKey(); + + + @override + Widget build(BuildContext context) { + return Navigator( + key: navigatorKey, + pages: [ + + ], + onPopPage: (route, result) { + // appState.selectedBook = null; + // notifyListeners(); + return route.didPop(result); + }, + ); + } + + + @override + Future setNewRoutePath(configuration) { + // TODO: implement setNewRoutePath + throw UnimplementedError(); + } + + + +} \ No newline at end of file diff --git a/lib/13/go/async_redirection.dart b/lib/13/go/async_redirection.dart new file mode 100644 index 0000000..f84c14e --- /dev/null +++ b/lib/13/go/async_redirection.dart @@ -0,0 +1,244 @@ +// 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/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates how to use redirect to handle a asynchronous +// sign-in flow. +// +// The `StreamAuth` is a mock of google_sign_in. This example wraps it with an +// InheritedNotifier, StreamAuthScope, and relies on +// `dependOnInheritedWidgetOfExactType` to create a dependency between the +// notifier and go_router's parsing pipeline. When StreamAuth broadcasts new +// event, the dependency will cause the go_router to reparse the current url +// which will also trigger the redirect. + +void main() => runApp(StreamAuthScope(child: App())); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Redirection'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) => + const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (BuildContext context, GoRouterState state) async { + // Using `of` method creates a dependency of StreamAuthScope. It will + // cause go_router to reparse current route if StreamAuth has new sign-in + // information. + final bool loggedIn = await StreamAuthScope.of(context).isSignedIn(); + final bool loggingIn = state.matchedLocation == '/login'; + if (!loggedIn) { + return '/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; + }, + ); +} + +/// The login screen. +class LoginScreen extends StatefulWidget { + /// Creates a [LoginScreen]. + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State + with TickerProviderStateMixin { + bool loggingIn = false; + late final AnimationController controller; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + )..addListener(() { + setState(() {}); + }); + controller.repeat(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (loggingIn) CircularProgressIndicator(value: controller.value), + if (!loggingIn) + ElevatedButton( + onPressed: () { + StreamAuthScope.of(context).signIn('test-user'); + setState(() { + loggingIn = true; + }); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} + +/// The home screen. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final StreamAuth info = StreamAuthScope.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: () => info.signOut(), + tooltip: 'Logout: ${info.currentUser}', + icon: const Icon(Icons.logout), + ) + ], + ), + body: const Center( + child: Text('HomeScreen'), + ), + ); + } +} + +/// A scope that provides [StreamAuth] for the subtree. +class StreamAuthScope extends InheritedNotifier { + /// Creates a [StreamAuthScope] sign in scope. + StreamAuthScope({ + super.key, + required super.child, + }) : super( + notifier: StreamAuthNotifier(), + ); + + /// Gets the [StreamAuth]. + static StreamAuth of(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType()! + .notifier! + .streamAuth; + } +} + +/// A class that converts [StreamAuth] into a [ChangeNotifier]. +class StreamAuthNotifier extends ChangeNotifier { + /// Creates a [StreamAuthNotifier]. + StreamAuthNotifier() : streamAuth = StreamAuth() { + streamAuth.onCurrentUserChanged.listen((String? string) { + notifyListeners(); + }); + } + + /// The stream auth client. + final StreamAuth streamAuth; +} + +/// An asynchronous log in services mock with stream similar to google_sign_in. +/// +/// This class adds an artificial delay of 3 second when logging in an user, and +/// will automatically clear the login session after [refreshInterval]. +class StreamAuth { + /// Creates an [StreamAuth] that clear the current user session in + /// [refeshInterval] second. + StreamAuth({this.refreshInterval = 20}) + : _userStreamController = StreamController.broadcast() { + _userStreamController.stream.listen((String? currentUser) { + _currentUser = currentUser; + }); + } + + /// The current user. + String? get currentUser => _currentUser; + String? _currentUser; + + /// Checks whether current user is signed in with an artificial delay to mimic + /// async operation. + Future isSignedIn() async { + await Future.delayed(const Duration(seconds: 1)); + return _currentUser != null; + } + + /// A stream that notifies when current user has changed. + Stream get onCurrentUserChanged => _userStreamController.stream; + final StreamController _userStreamController; + + /// The interval that automatically signs out the user. + final int refreshInterval; + + Timer? _timer; + Timer _createRefreshTimer() { + return Timer(Duration(seconds: refreshInterval), () { + _userStreamController.add(null); + _timer = null; + }); + } + + /// Signs in a user with an artificial delay to mimic async operation. + Future signIn(String newUserName) async { + await Future.delayed(const Duration(seconds: 3)); + _userStreamController.add(newUserName); + _timer?.cancel(); + _timer = _createRefreshTimer(); + } + + /// Signs out the current user. + Future signOut() async { + _timer?.cancel(); + _timer = null; + _userStreamController.add(null); + } +} diff --git a/lib/13/go/books/main.dart b/lib/13/go/books/main.dart new file mode 100644 index 0000000..eb757e3 --- /dev/null +++ b/lib/13/go/books/main.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:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'src/auth.dart'; +import 'src/data/author.dart'; +import 'src/data/book.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()); + +/// The book store view. +class Bookstore extends StatelessWidget { + /// Creates a [Bookstore]. + Bookstore({super.key}); + + final ValueKey _scaffoldKey = const ValueKey('App scaffold'); + + @override + Widget build(BuildContext context) => BookstoreAuthScope( + notifier: _auth, + child: MaterialApp.router( + routerConfig: _router, + ), + ); + + final BookstoreAuth _auth = BookstoreAuth(); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_, __) => '/books', + ), + GoRoute( + path: '/signin', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: state.pageKey, + child: SignInScreen( + onSignIn: (Credentials credentials) { + BookstoreAuthScope.of(context) + .signIn(credentials.username, credentials.password); + }, + ), + ), + ), + GoRoute( + path: '/books', + redirect: (_, __) => '/books/popular', + ), + GoRoute( + path: '/book/:bookId', + redirect: (BuildContext context, GoRouterState state) => + '/books/all/${state.pathParameters['bookId']}', + ), + GoRoute( + path: '/books/:kind(new|all|popular)', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: _scaffoldKey, + child: BookstoreScaffold( + selectedTab: ScaffoldTab.books, + child: BooksScreen(state.pathParameters['kind']!), + ), + ), + routes: [ + GoRoute( + path: ':bookId', + builder: (BuildContext context, GoRouterState state) { + final String bookId = state.pathParameters['bookId']!; + final Book? selectedBook = libraryInstance.allBooks + .firstWhereOrNull((Book b) => b.id.toString() == bookId); + + return BookDetailsScreen(book: selectedBook); + }, + ), + ], + ), + GoRoute( + path: '/author/:authorId', + redirect: (BuildContext context, GoRouterState state) => + '/authors/${state.pathParameters['authorId']}', + ), + GoRoute( + path: '/authors', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: _scaffoldKey, + child: const BookstoreScaffold( + selectedTab: ScaffoldTab.authors, + child: AuthorsScreen(), + ), + ), + routes: [ + GoRoute( + path: ':authorId', + builder: (BuildContext context, GoRouterState state) { + final int authorId = int.parse(state.pathParameters['authorId']!); + final Author? selectedAuthor = libraryInstance.allAuthors + .firstWhereOrNull((Author a) => a.id == authorId); + + return AuthorDetailsScreen(author: selectedAuthor); + }, + ), + ], + ), + GoRoute( + path: '/settings', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: _scaffoldKey, + child: const BookstoreScaffold( + selectedTab: ScaffoldTab.settings, + child: SettingsScreen(), + ), + ), + ), + ], + redirect: _guard, + refreshListenable: _auth, + debugLogDiagnostics: true, + ); + + String? _guard(BuildContext context, GoRouterState state) { + final bool signedIn = _auth.signedIn; + final bool signingIn = state.matchedLocation == '/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; + } +} + +/// A page that fades in an out. +class FadeTransitionPage extends CustomTransitionPage { + /// Creates a [FadeTransitionPage]. + FadeTransitionPage({ + required LocalKey super.key, + required super.child, + }) : super( + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + FadeTransition( + opacity: animation.drive(_curveTween), + child: child, + )); + + static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn); +} diff --git a/lib/13/go/books/src/auth.dart b/lib/13/go/books/src/auth.dart new file mode 100644 index 0000000..b9c353c --- /dev/null +++ b/lib/13/go/books/src/auth.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/widgets.dart'; + +/// A mock authentication service. +class BookstoreAuth extends ChangeNotifier { + bool _signedIn = false; + + /// Whether user has signed in. + bool get signedIn => _signedIn; + + /// Signs out the current user. + Future signOut() async { + await Future.delayed(const Duration(milliseconds: 200)); + // Sign out. + _signedIn = false; + notifyListeners(); + } + + /// Signs in a user. + Future signIn(String username, String password) async { + await Future.delayed(const Duration(milliseconds: 200)); + + // Sign in. Allow any password. + _signedIn = true; + notifyListeners(); + return _signedIn; + } +} + +/// An inherited notifier to host [BookstoreAuth] for the subtree. +class BookstoreAuthScope extends InheritedNotifier { + /// Creates a [BookstoreAuthScope]. + const BookstoreAuthScope({ + required BookstoreAuth super.notifier, + required super.child, + super.key, + }); + + /// Gets the [BookstoreAuth] above the context. + static BookstoreAuth of(BuildContext context) => context + .dependOnInheritedWidgetOfExactType()! + .notifier!; +} diff --git a/lib/13/go/books/src/data.dart b/lib/13/go/books/src/data.dart new file mode 100644 index 0000000..109082e --- /dev/null +++ b/lib/13/go/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/lib/13/go/books/src/data/author.dart b/lib/13/go/books/src/data/author.dart new file mode 100644 index 0000000..b58db11 --- /dev/null +++ b/lib/13/go/books/src/data/author.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 'book.dart'; + +/// Author data class. +class Author { + /// Creates an author data object. + Author({ + required this.id, + required this.name, + }); + + /// The id of the author. + final int id; + + /// The name of the author. + final String name; + + /// The books of the author. + final List books = []; +} diff --git a/lib/13/go/books/src/data/book.dart b/lib/13/go/books/src/data/book.dart new file mode 100644 index 0000000..cd2c94f --- /dev/null +++ b/lib/13/go/books/src/data/book.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 'author.dart'; + +/// Book data class. +class Book { + /// Creates a book data object. + Book({ + required this.id, + required this.title, + required this.isPopular, + required this.isNew, + required this.author, + }); + + /// The id of the book. + final int id; + + /// The title of the book. + final String title; + + /// The author of the book. + final Author author; + + /// Whether the book is popular. + final bool isPopular; + + /// Whether the book is new. + final bool isNew; +} diff --git a/lib/13/go/books/src/data/library.dart b/lib/13/go/books/src/data/library.dart new file mode 100644 index 0000000..075b825 --- /dev/null +++ b/lib/13/go/books/src/data/library.dart @@ -0,0 +1,76 @@ +// 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'; + +/// Library data mock. +final Library 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); + +/// A library that contains books and authors. +class Library { + /// The books in the library. + final List allBooks = []; + + /// The authors in the library. + final List allAuthors = []; + + /// Adds a book into the library. + void addBook({ + required String title, + required String authorName, + required bool isPopular, + required bool isNew, + }) { + final Author author = allAuthors.firstWhere( + (Author author) => author.name == authorName, + orElse: () { + final Author value = Author(id: allAuthors.length, name: authorName); + allAuthors.add(value); + return value; + }, + ); + + final Book book = Book( + id: allBooks.length, + title: title, + isPopular: isPopular, + isNew: isNew, + author: author, + ); + + author.books.add(book); + allBooks.add(book); + } + + /// The list of popular books in the library. + List get popularBooks => [ + ...allBooks.where((Book book) => book.isPopular), + ]; + + /// The list of new books in the library. + List get newBooks => [ + ...allBooks.where((Book book) => book.isNew), + ]; +} diff --git a/lib/13/go/books/src/screens/author_details.dart b/lib/13/go/books/src/screens/author_details.dart new file mode 100644 index 0000000..3aff898 --- /dev/null +++ b/lib/13/go/books/src/screens/author_details.dart @@ -0,0 +1,49 @@ +// 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'; + +/// The author detail screen. +class AuthorDetailsScreen extends StatelessWidget { + /// Creates an author detail screen. + const AuthorDetailsScreen({ + required this.author, + super.key, + }); + + /// The author to be displayed. + 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 book) => context.go('/book/${book.id}'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/13/go/books/src/screens/authors.dart b/lib/13/go/books/src/screens/authors.dart new file mode 100644 index 0000000..0eeb1c3 --- /dev/null +++ b/lib/13/go/books/src/screens/authors.dart @@ -0,0 +1,31 @@ +// 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/author_list.dart'; + +/// A screen that displays a list of authors. +class AuthorsScreen extends StatelessWidget { + /// Creates an [AuthorsScreen]. + const AuthorsScreen({super.key}); + + /// The title of the screen. + static const String title = 'Authors'; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text(title), + ), + body: AuthorList( + authors: libraryInstance.allAuthors, + onTap: (Author author) { + context.go('/author/${author.id}'); + }, + ), + ); +} diff --git a/lib/13/go/books/src/screens/book_details.dart b/lib/13/go/books/src/screens/book_details.dart new file mode 100644 index 0000000..9a51a83 --- /dev/null +++ b/lib/13/go/books/src/screens/book_details.dart @@ -0,0 +1,77 @@ +// 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'; + +/// A screen to display book details. +class BookDetailsScreen extends StatelessWidget { + /// Creates a [BookDetailsScreen]. + const BookDetailsScreen({ + super.key, + this.book, + }); + + /// The book to be displayed. + 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.headlineMedium, + ), + Text( + book!.author.name, + style: Theme.of(context).textTheme.titleMedium, + ), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + AuthorDetailsScreen(author: book!.author), + ), + ); + }, + child: const Text('View author (navigator.push)'), + ), + Link( + uri: Uri.parse('/author/${book!.author.id}'), + builder: (BuildContext context, FollowLink? 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/lib/13/go/books/src/screens/books.dart b/lib/13/go/books/src/screens/books.dart new file mode 100644 index 0000000..b0e7ca2 --- /dev/null +++ b/lib/13/go/books/src/screens/books.dart @@ -0,0 +1,118 @@ +// 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'; + +/// A screen that displays a list of books. +class BooksScreen extends StatefulWidget { + /// Creates a [BooksScreen]. + const BooksScreen(this.kind, {super.key}); + + /// Which tab to display. + final String kind; + + @override + State 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/lib/13/go/books/src/screens/scaffold.dart b/lib/13/go/books/src/screens/scaffold.dart new file mode 100644 index 0000000..2ffaf9a --- /dev/null +++ b/lib/13/go/books/src/screens/scaffold.dart @@ -0,0 +1,71 @@ +// 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'; + +/// The enum for scaffold tab. +enum ScaffoldTab { + /// The books tab. + books, + + /// The authors tab. + authors, + + /// The settings tab. + settings +} + +/// The scaffold for the book store. +class BookstoreScaffold extends StatelessWidget { + /// Creates a [BookstoreScaffold]. + const BookstoreScaffold({ + required this.selectedTab, + required this.child, + super.key, + }); + + /// Which tab of the scaffold to display. + final ScaffoldTab selectedTab; + + /// The scaffold body. + final Widget child; + + @override + Widget build(BuildContext context) => Scaffold( + body: AdaptiveNavigationScaffold( + selectedIndex: selectedTab.index, + body: child, + onDestinationSelected: (int 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/lib/13/go/books/src/screens/settings.dart b/lib/13/go/books/src/screens/settings.dart new file mode 100644 index 0000000..a098e30 --- /dev/null +++ b/lib/13/go/books/src/screens/settings.dart @@ -0,0 +1,101 @@ +// 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'; + +/// The settings screen. +class SettingsScreen extends StatefulWidget { + /// Creates a [SettingsScreen]. + const SettingsScreen({super.key}); + + @override + State 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(), + ), + ), + ), + ), + ), + ), + ); +} + +/// The content of a [SettingsScreen]. +class SettingsContent extends StatelessWidget { + /// Creates a [SettingsContent]. + const SettingsContent({ + super.key, + }); + + @override + Widget build(BuildContext context) => Column( + children: [ + ...[ + Text( + 'Settings', + style: Theme.of(context).textTheme.headlineMedium, + ), + ElevatedButton( + onPressed: () { + BookstoreAuthScope.of(context).signOut(); + }, + child: const Text('Sign out'), + ), + Link( + uri: Uri.parse('/book/0'), + builder: (BuildContext context, FollowLink? 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((Widget w) => + Padding(padding: const EdgeInsets.all(8), child: w)), + TextButton( + onPressed: () => showDialog( + context: context, + builder: (BuildContext 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/lib/13/go/books/src/screens/sign_in.dart b/lib/13/go/books/src/screens/sign_in.dart new file mode 100644 index 0000000..e02c870 --- /dev/null +++ b/lib/13/go/books/src/screens/sign_in.dart @@ -0,0 +1,77 @@ +// 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'; + +/// Credential data class. +class Credentials { + /// Creates a credential data object. + Credentials(this.username, this.password); + + /// The username of the credentials. + final String username; + + /// The password of the credentials. + final String password; +} + +/// The sign-in screen. +class SignInScreen extends StatefulWidget { + /// Creates a sign-in screen. + const SignInScreen({ + required this.onSignIn, + super.key, + }); + + /// Called when users sign in with [Credentials]. + final ValueChanged onSignIn; + + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _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.headlineMedium), + 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/lib/13/go/books/src/widgets/author_list.dart b/lib/13/go/books/src/widgets/author_list.dart new file mode 100644 index 0000000..371e30a --- /dev/null +++ b/lib/13/go/books/src/widgets/author_list.dart @@ -0,0 +1,37 @@ +// 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'; + +/// The author list view. +class AuthorList extends StatelessWidget { + /// Creates an [AuthorList]. + const AuthorList({ + required this.authors, + this.onTap, + super.key, + }); + + /// The list of authors to be shown. + final List authors; + + /// Called when the user taps an author. + final ValueChanged? onTap; + + @override + Widget build(BuildContext context) => ListView.builder( + itemCount: authors.length, + itemBuilder: (BuildContext context, int index) => ListTile( + title: Text( + authors[index].name, + ), + subtitle: Text( + '${authors[index].books.length} books', + ), + onTap: onTap != null ? () => onTap!(authors[index]) : null, + ), + ); +} diff --git a/lib/13/go/books/src/widgets/book_list.dart b/lib/13/go/books/src/widgets/book_list.dart new file mode 100644 index 0000000..3e2761f --- /dev/null +++ b/lib/13/go/books/src/widgets/book_list.dart @@ -0,0 +1,37 @@ +// 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'; + +/// The book list view. +class BookList extends StatelessWidget { + /// Creates an [BookList]. + const BookList({ + required this.books, + this.onTap, + super.key, + }); + + /// The list of books to be displayed. + final List books; + + /// Called when the user taps a book. + final ValueChanged? onTap; + + @override + Widget build(BuildContext context) => ListView.builder( + itemCount: books.length, + itemBuilder: (BuildContext context, int index) => ListTile( + title: Text( + books[index].title, + ), + subtitle: Text( + books[index].author.name, + ), + onTap: onTap != null ? () => onTap!(books[index]) : null, + ), + ); +} diff --git a/lib/13/go/exception_handling.dart b/lib/13/go/exception_handling.dart new file mode 100644 index 0000000..82c10e3 --- /dev/null +++ b/lib/13/go/exception_handling.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'; + +/// This sample app shows how to use `GoRouter.onException` to redirect on +/// exception. +/// +/// The first route '/' is mapped to [HomeScreen], and the second route +/// '/404' is mapped to [NotFoundScreen]. +/// +/// Any other unknown route or exception is redirected to `/404`. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + onException: (_, GoRouterState state, GoRouter router) { + router.go('/404', extra: state.uri.toString()); + }, + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + ), + GoRoute( + path: '/404', + builder: (BuildContext context, GoRouterState state) { + return NotFoundScreen(uri: state.extra as String? ?? ''); + }, + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.go('/some-unknown-route'), + child: const Text('Simulates user entering unknown url'), + ), + ), + ); + } +} + +/// The not found screen +class NotFoundScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const NotFoundScreen({super.key, required this.uri}); + + /// The uri that can not be found. + final String uri; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Page Not Found')), + body: Center( + child: Text("Can't find a page for: $uri"), + ), + ); + } +} diff --git a/lib/13/go/main.dart b/lib/13/go/main.dart new file mode 100644 index 0000000..d159648 --- /dev/null +++ b/lib/13/go/main.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'; + +/// This sample app shows an app with two screens. +/// +/// The first route '/' is mapped to [HomeScreen], and the second route +/// '/details' is mapped to [DetailsScreen]. +/// +/// The buttons use context.go() to navigate to each destination. On mobile +/// devices, each destination is deep-linkable and on the web, can be navigated +/// to using the address bar. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(); + }, + ), + ], + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.go('/details'), + child: const Text('Go to the Details screen'), + ), + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details Screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go back to the Home screen'), + ), + ), + ); + } +} diff --git a/lib/13/go/named_routes.dart b/lib/13/go/named_routes.dart new file mode 100644 index 0000000..9685fc4 --- /dev/null +++ b/lib/13/go/named_routes.dart @@ -0,0 +1,181 @@ +// 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'; + +// This scenario demonstrates how to navigate using named locations instead of +// URLs. +// +// Instead of hardcoding the URI locations , you can also use the named +// locations. To use this API, give a unique name to each GoRoute. The name can +// then be used in context.namedLocation to be translate back to the actual URL +// location. + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map people; +} + +/// Person data class. +class Person { + /// Creates a person. + const Person({required this.name, required this.age}); + + /// The first name of the person. + final String name; + + /// The age of the person. + final int age; +} + +const Map _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane', age: 23), + 'p2': Person(name: 'John', age: 6), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + 'p1': Person(name: 'June', age: 51), + 'p2': Person(name: 'Xin', age: 44), + }, + ), +}; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Named Routes'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final GoRouter _router = GoRouter( + debugLogDiagnostics: true, + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(fid: state.pathParameters['fid']!), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (BuildContext context, GoRouterState state) { + return PersonScreen( + fid: state.pathParameters['fid']!, + pid: state.pathParameters['pid']!); + }, + ), + ], + ), + ], + ), + ], + ); +} + +/// The home screen that shows a list of families. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + ), + body: ListView( + children: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go(context.namedLocation('family', + pathParameters: {'fid': entry.key})), + ) + ], + ), + ); + } +} + +/// The screen that shows a list of persons in a family. +class FamilyScreen extends StatelessWidget { + /// Creates a [FamilyScreen]. + const FamilyScreen({required this.fid, super.key}); + + /// The id family to display. + final String fid; + + @override + Widget build(BuildContext context) { + final Map people = _families[fid]!.people; + return Scaffold( + appBar: AppBar(title: Text(_families[fid]!.name)), + body: ListView( + children: [ + for (final MapEntry entry in people.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go(context.namedLocation( + 'person', + pathParameters: {'fid': fid, 'pid': entry.key}, + queryParameters: {'qid': 'quid'}, + )), + ), + ], + ), + ); + } +} + +/// The person screen. +class PersonScreen extends StatelessWidget { + /// Creates a [PersonScreen]. + const PersonScreen({required this.fid, required this.pid, super.key}); + + /// The id of family this person belong to. + final String fid; + + /// The id of the person to be displayed. + final String pid; + + @override + Widget build(BuildContext context) { + final Family family = _families[fid]!; + final Person person = family.people[pid]!; + return Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); + } +} diff --git a/lib/13/go/others/custom_stateful_shell_route.dart b/lib/13/go/others/custom_stateful_shell_route.dart new file mode 100644 index 0000000..5fbe2b6 --- /dev/null +++ b/lib/13/go/others/custom_stateful_shell_route.dart @@ -0,0 +1,460 @@ +// 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'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _tabANavigatorKey = + GlobalKey(debugLabel: 'tabANav'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also +// enables deep linking into nested pages. +// +// This example also demonstrates how build a nested shell with a custom +// container for the branch Navigators (in this case a TabBarView). + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // This nested StatefulShellRoute demonstrates the use of a + // custom container for the branch Navigators. In this implementation, + // no customization is done in the builder function (navigationShell + // itself is simply used as the Widget for the route). Instead, the + // navigatorContainerBuilder function below is provided to + // customize the container for the branch Navigators. + return navigationShell; + }, + navigatorContainerBuilder: (BuildContext context, + StatefulNavigationShell navigationShell, List children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See ScaffoldWithNavBar for more details on how the children + // are managed (using AnimatedBranchContainer). + return ScaffoldWithNavBar( + navigationShell: navigationShell, children: children); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _tabANavigatorKey, + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreenA(), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ], + ), + + // The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + // StatefulShellBranch will automatically use the first descendant + // GoRoute as the initial location of the branch. If another route + // is desired, specify the location of it using the defaultLocation + // parameter. + // defaultLocation: '/c2', + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // Just like with the top level StatefulShellRoute, no + // customization is done in the builder function. + return navigationShell; + }, + navigatorContainerBuilder: (BuildContext context, + StatefulNavigationShell navigationShell, + List children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See TabbedRootScreen for more details on how the children + // are managed (in a TabBarView). + return TabbedRootScreen( + navigationShell: navigationShell, children: children); + }, + // This bottom tab uses a nested shell, wrapping sub routes in a + // top TabBar. + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/b1', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B1', detailsPath: '/b1/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B1', + withScaffold: false, + ), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b2', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B2', detailsPath: '/b2/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B2', + withScaffold: false, + ), + ), + ], + ), + ]), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + required this.children, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + /// The children (branch Navigators) to display in a custom container + /// ([AnimatedBranchContainer]). + final List children; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AnimatedBranchContainer( + currentIndex: navigationShell.currentIndex, + children: children, + ), + bottomNavigationBar: BottomNavigationBar( + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), + ), + ); + } + + /// Navigate to the current location of the branch at the provided index when + /// tapping an item in the BottomNavigationBar. + void _onTap(BuildContext context, int index) { + // When navigating to a new branch, it's recommended to use the goBranch + // method, as doing so makes sure the last navigation state of the + // Navigator for the branch is restored. + navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, + ); + } +} + +/// Custom branch Navigator container that provides animated transitions +/// when switching branches. +class AnimatedBranchContainer extends StatelessWidget { + /// Creates a AnimatedBranchContainer + const AnimatedBranchContainer( + {super.key, required this.currentIndex, required this.children}); + + /// The index (in [children]) of the branch Navigator to display. + final int currentIndex; + + /// The children (branch Navigators) to display in this container. + final List children; + + @override + Widget build(BuildContext context) { + return Stack( + children: children.mapIndexed( + (int index, Widget navigator) { + return AnimatedScale( + scale: index == currentIndex ? 1 : 1.5, + duration: const Duration(milliseconds: 400), + child: AnimatedOpacity( + opacity: index == currentIndex ? 1 : 0, + duration: const Duration(milliseconds: 400), + child: _branchNavigatorWrapper(index, navigator), + ), + ); + }, + ).toList()); + } + + Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer( + ignoring: index != currentIndex, + child: TickerMode( + enabled: index == currentIndex, + child: navigator, + ), + ); +} + +/// Widget for the root page for the first section of the bottom navigation bar. +class RootScreenA extends StatelessWidget { + /// Creates a RootScreenA + const RootScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Root of section A'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen A', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go('/a/details'); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} + +/// Builds a nested shell using a [TabBar] and [TabBarView]. +class TabbedRootScreen extends StatefulWidget { + /// Constructs a TabbedRootScreen + const TabbedRootScreen( + {required this.navigationShell, required this.children, super.key}); + + /// The current state of the parent StatefulShellRoute. + final StatefulNavigationShell navigationShell; + + /// The children (branch Navigators) to display in the [TabBarView]. + final List children; + + @override + State createState() => _TabbedRootScreenState(); +} + +class _TabbedRootScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController = TabController( + length: widget.children.length, + vsync: this, + initialIndex: widget.navigationShell.currentIndex); + + @override + void didUpdateWidget(covariant TabbedRootScreen oldWidget) { + super.didUpdateWidget(oldWidget); + _tabController.index = widget.navigationShell.currentIndex; + } + + @override + Widget build(BuildContext context) { + final List tabs = widget.children + .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) + .toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('Root of Section B (nested TabBar shell)'), + bottom: TabBar( + controller: _tabController, + tabs: tabs, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), + )), + body: TabBarView( + controller: _tabController, + children: widget.children, + ), + ); + } + + void _onTabTap(BuildContext context, int index) { + widget.navigationShell.goBranch(index); + } +} + +/// Widget for the pages in the top tab bar. +class TabScreen extends StatelessWidget { + /// Creates a RootScreen + const TabScreen({required this.label, required this.detailsPath, super.key}); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ); + } +} diff --git a/lib/13/go/others/error_screen.dart b/lib/13/go/others/error_screen.dart new file mode 100644 index 0000000..5c071fc --- /dev/null +++ b/lib/13/go/others/error_screen.dart @@ -0,0 +1,110 @@ +// 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()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Custom Error Screen'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + ], + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.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'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.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'), + ), + ], + ), + ), + ); +} + +/// The screen of the error page. +class ErrorScreen extends StatelessWidget { + /// Creates an [ErrorScreen]. + const ErrorScreen(this.error, {super.key}); + + /// The error to display. + 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/lib/13/go/others/extra_param.dart b/lib/13/go/others/extra_param.dart new file mode 100644 index 0000000..eaef755 --- /dev/null +++ b/lib/13/go/others/extra_param.dart @@ -0,0 +1,133 @@ +// 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'; + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map people; +} + +/// Person data class. +class Person { + /// Creates a person. + const Person({required this.name, required this.age}); + + /// The first name of the person. + final String name; + + /// The age of the person. + final int age; +} + +const Map _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane', age: 23), + 'p2': Person(name: 'John', age: 6), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + 'p1': Person(name: 'June', age: 51), + 'p2': Person(name: 'Xin', age: 44), + }, + ), +}; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Extra Parameter'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family', + builder: (BuildContext context, GoRouterState state) { + final Map params = + state.extra! as Map; + final String fid = params['fid']! as String; + return FamilyScreen(fid: fid); + }, + ), + ], + ), + ], + ); +} + +/// The home screen that shows a list of families. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: ListView( + children: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.goNamed('family', + extra: {'fid': entry.key}), + ) + ], + ), + ); +} + +/// The screen that shows a list of persons in a family. +class FamilyScreen extends StatelessWidget { + /// Creates a [FamilyScreen]. + const FamilyScreen({required this.fid, super.key}); + + /// The family to display. + final String fid; + + @override + Widget build(BuildContext context) { + final Map people = _families[fid]!.people; + return Scaffold( + appBar: AppBar(title: Text(_families[fid]!.name)), + body: ListView( + children: [ + for (final Person p in people.values) + ListTile( + title: Text(p.name), + ), + ], + ), + ); + } +} diff --git a/lib/13/go/others/init_loc.dart b/lib/13/go/others/init_loc.dart new file mode 100644 index 0000000..4f61b00 --- /dev/null +++ b/lib/13/go/others/init_loc.dart @@ -0,0 +1,110 @@ +// 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()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Initial Location'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + initialLocation: '/page3', + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + GoRoute( + path: '/page3', + builder: (BuildContext context, GoRouterState state) => + const Page3Screen(), + ), + ], + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.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'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.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'), + ), + ], + ), + ), + ); +} + +/// The screen of the third page. +class Page3Screen extends StatelessWidget { + /// Creates a [Page3Screen]. + const Page3Screen({super.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/lib/13/go/others/nav_observer.dart b/lib/13/go/others/nav_observer.dart new file mode 100644 index 0000000..038d337 --- /dev/null +++ b/lib/13/go/others/nav_observer.dart @@ -0,0 +1,167 @@ +// 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()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Navigator Observer'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + observers: [MyNavObserver()], + routes: [ + GoRoute( + // if there's no name, path will be used as name for observers + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + routes: [ + GoRoute( + name: 'page2', + path: 'page2/:p1', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + routes: [ + GoRoute( + name: 'page3', + path: 'page3', + builder: (BuildContext context, GoRouterState state) => + const Page3Screen(), + ), + ], + ), + ], + ), + ], + ); +} + +/// The Navigator observer. +class MyNavObserver extends NavigatorObserver { + /// Creates a [MyNavObserver]. + MyNavObserver() { + log.onRecord.listen((LogRecord e) => debugPrint('$e')); + } + + /// The logged message. + final Logger 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})'; +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.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', + pathParameters: {'p1': 'pv1'}, + queryParameters: {'q1': 'qv1'}, + ), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.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', + pathParameters: {'p1': 'pv2'}, + ), + child: const Text('Go to page 3'), + ), + ], + ), + ), + ); +} + +/// The screen of the third page. +class Page3Screen extends StatelessWidget { + /// Creates a [Page3Screen]. + const Page3Screen({super.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/lib/13/go/others/push.dart b/lib/13/go/others/push.dart new file mode 100644 index 0000000..0cea894 --- /dev/null +++ b/lib/13/go/others/push.dart @@ -0,0 +1,101 @@ +// 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()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Push'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1ScreenWithPush(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + Page2ScreenWithPush( + int.parse(state.uri.queryParameters['push-count']!), + ), + ), + ], + ); +} + +/// The screen of the first page. +class Page1ScreenWithPush extends StatelessWidget { + /// Creates a [Page1ScreenWithPush]. + const Page1ScreenWithPush({super.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'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2ScreenWithPush extends StatelessWidget { + /// Creates a [Page2ScreenWithPush]. + const Page2ScreenWithPush(this.pushCount, {super.key}); + + /// The push count. + 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/lib/13/go/others/router_neglect.dart b/lib/13/go/others/router_neglect.dart new file mode 100644 index 0000000..75686a1 --- /dev/null +++ b/lib/13/go/others/router_neglect.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'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Router neglect'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + // turn off history tracking in the browser for this navigation + routerNeglect: true, + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + ], + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.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'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.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/lib/13/go/others/state_restoration.dart b/lib/13/go/others/state_restoration.dart new file mode 100644 index 0000000..93e8aca --- /dev/null +++ b/lib/13/go/others/state_restoration.dart @@ -0,0 +1,102 @@ +// 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()), + ); + +/// The main app. +class App extends StatefulWidget { + /// Creates an [App]. + const App({super.key}); + + /// The title of the app. + static const String 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) { + // Implement restoreState for your app + } + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: App.title, + restorationScopeId: 'app', + ); + + final GoRouter _router = GoRouter( + routes: [ + // restorationId set for the route automatically + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + + // restorationId set for the route automatically + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + ], + restorationScopeId: 'router', + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.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'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.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/lib/13/go/others/stateful_shell_state_restoration.dart b/lib/13/go/others/stateful_shell_state_restoration.dart new file mode 100644 index 0000000..aeecd11 --- /dev/null +++ b/lib/13/go/others/stateful_shell_state_restoration.dart @@ -0,0 +1,236 @@ +// 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(RestorableStatefulShellRouteExampleApp()); + +/// An example demonstrating how to use StatefulShellRoute with state +/// restoration. +class RestorableStatefulShellRouteExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + RestorableStatefulShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + initialLocation: '/a', + restorationScopeId: 'router', + routes: [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'shell1', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + return MaterialPage( + restorationId: 'shellWidget1', + child: ScaffoldWithNavBar(navigationShell: navigationShell)); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenA', + child: + RootScreen(label: 'A', detailsPath: '/a/details')), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenADetail', + child: DetailsScreen(label: 'A')), + ), + ], + ), + ], + ), + // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + GoRoute( + // The screen to display as the root in the second tab of the + // bottom navigation bar. + path: '/b', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenB', + child: + RootScreen(label: 'B', detailsPath: '/b/details')), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenBDetail', + child: DetailsScreen(label: 'B')), + ), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + restorationScopeId: 'app', + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), + ), + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + required this.detailsPath, + super.key, + }); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Root of section $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State with RestorationMixin { + final RestorableInt _counter = RestorableInt(0); + + @override + String? get restorationId => 'DetailsScreen-${widget.label}'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_counter, 'counter'); + } + + @override + void dispose() { + super.dispose(); + _counter.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: ${_counter.value}', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter.value++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + ], + ), + ); + } +} diff --git a/lib/13/go/others/transitions.dart b/lib/13/go/others/transitions.dart new file mode 100644 index 0000000..e4b9a3e --- /dev/null +++ b/lib/13/go/others/transitions.dart @@ -0,0 +1,163 @@ +// 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()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Custom Transitions'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_, __) => '/none', + ), + GoRoute( + path: '/fade', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'fade', + color: Colors.red, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + FadeTransition(opacity: animation, child: child), + ), + ), + GoRoute( + path: '/scale', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'scale', + color: Colors.green, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + ScaleTransition(scale: animation, child: child), + ), + ), + GoRoute( + path: '/slide', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'slide', + color: Colors.yellow, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget 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: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'rotation', + color: Colors.purple, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + RotationTransition(turns: animation, child: child), + ), + ), + GoRoute( + path: '/none', + pageBuilder: (BuildContext context, GoRouterState state) => + NoTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'none', + color: Colors.white, + ), + ), + ), + ], + ); +} + +/// An Example transitions screen. +class ExampleTransitionsScreen extends StatelessWidget { + /// Creates an [ExampleTransitionsScreen]. + const ExampleTransitionsScreen({ + required this.color, + required this.kind, + super.key, + }); + + /// The available transition kinds. + static final List kinds = [ + 'fade', + 'scale', + 'slide', + 'rotation', + 'none' + ]; + + /// The color of the container. + final Color color; + + /// The transition kind. + 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 String kind in kinds) + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.go('/$kind'), + child: Text('$kind transition'), + ), + ) + ], + ), + ), + ), + ); +} diff --git a/lib/13/go/path_and_query_parameters.dart b/lib/13/go/path_and_query_parameters.dart new file mode 100644 index 0000000..3a8f289 --- /dev/null +++ b/lib/13/go/path_and_query_parameters.dart @@ -0,0 +1,169 @@ +// 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'; + +// This scenario demonstrates how to use path parameters and query parameters. +// +// The route segments that start with ':' are treated as path parameters when +// defining GoRoute[s]. The parameter values can be accessed through +// GoRouterState.pathParameters. +// +// The query parameters are automatically stored in GoRouterState.queryParameters. + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map people; +} + +/// Person data class. +class Person { + /// Creates a person. + const Person({required this.name, required this.age}); + + /// The first name of the person. + final String name; + + /// The age of the person. + final int age; +} + +const Map _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane', age: 23), + 'p2': Person(name: 'John', age: 6), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + 'p1': Person(name: 'June', age: 51), + 'p2': Person(name: 'Xin', age: 44), + }, + ), +}; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String 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) => MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) { + return FamilyScreen( + fid: state.pathParameters['fid']!, + asc: state.uri.queryParameters['sort'] == 'asc', + ); + }), + ], + ), + ], + ); +} + +/// The home screen that shows a list of families. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + ), + body: ListView( + children: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go('/family/${entry.key}'), + ) + ], + ), + ); + } +} + +/// The screen that shows a list of persons in a family. +class FamilyScreen extends StatelessWidget { + /// Creates a [FamilyScreen]. + const FamilyScreen({required this.fid, required this.asc, super.key}); + + /// The family to display. + final String fid; + + /// Whether to sort the name in ascending order. + final bool asc; + + @override + Widget build(BuildContext context) { + final Map newQueries; + final List names = _families[fid]! + .people + .values + .map((Person p) => p.name) + .toList(); + names.sort(); + if (asc) { + newQueries = const {'sort': 'desc'}; + } else { + newQueries = const {'sort': 'asc'}; + } + return Scaffold( + appBar: AppBar( + title: Text(_families[fid]!.name), + actions: [ + IconButton( + onPressed: () => context.goNamed('family', + pathParameters: {'fid': fid}, + queryParameters: newQueries), + tooltip: 'sort ascending or descending', + icon: const Icon(Icons.sort), + ) + ], + ), + body: ListView( + children: [ + for (final String name in asc ? names : names.reversed) + ListTile( + title: Text(name), + ), + ], + ), + ); + } +} diff --git a/lib/13/go/raw_book/main.dart b/lib/13/go/raw_book/main.dart new file mode 100644 index 0000000..7c97b30 --- /dev/null +++ b/lib/13/go/raw_book/main.dart @@ -0,0 +1,27 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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:io' show Platform; + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:url_strategy/url_strategy.dart'; +// import 'package:window_size/window_size.dart'; + +import 'src/app.dart'; + +void main() { + // Use package:url_strategy until this pull request is released: + // https://github.com/flutter/flutter/pull/77103 + + // Use to setHashUrlStrategy() to use "/#/" in the address bar (default). Use + // setPathUrlStrategy() to use the path. You may need to configure your web + // server to redirect all paths to index.html. + // + // On mobile platforms, both functions are no-ops. + setHashUrlStrategy(); + // setPathUrlStrategy(); + runApp(const Bookstore()); +} + diff --git a/lib/13/go/raw_book/src/app.dart b/lib/13/go/raw_book/src/app.dart new file mode 100644 index 0000000..8cb7ba8 --- /dev/null +++ b/lib/13/go/raw_book/src/app.dart @@ -0,0 +1,81 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 'auth.dart'; +import 'routing.dart'; +import 'screens/navigator.dart'; + +class Bookstore extends StatefulWidget { + const Bookstore({super.key}); + + @override + State createState() => _BookstoreState(); +} + +class _BookstoreState extends State { + late final RouteState _routeState; + late final SimpleRouterDelegate _routerDelegate; + late final TemplateRouteParser _routeParser; + + @override + void initState() { + /// Configure the parser with all of the app's allowed path templates. + _routeParser = TemplateRouteParser( + allowedPaths: [ + '/signin', + '/authors', + '/settings', + '/books/new', + '/books/all', + '/books/popular', + '/book/:bookId', + '/author/:authorId', + ], + initialRoute: '/signin', + ); + + _routeState = RouteState(_routeParser); + + _routerDelegate = SimpleRouterDelegate( + routeState: _routeState, + ); + + + super.initState(); + } + + @override + Widget build(BuildContext context) => RouteStateScope( + notifier: _routeState, + child: MaterialApp.router( + routerDelegate: _routerDelegate, + routeInformationParser: _routeParser, + // Revert back to pre-Flutter-2.5 transition behavior: + // https://github.com/flutter/flutter/issues/82053 + theme: ThemeData( + useMaterial3: true, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(), + TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), + }, + ), + ), + + ), + ); + + + @override + void dispose() { + _routeState.dispose(); + _routerDelegate.dispose(); + super.dispose(); + } +} diff --git a/lib/13/go/raw_book/src/auth.dart b/lib/13/go/raw_book/src/auth.dart new file mode 100644 index 0000000..1e9157f --- /dev/null +++ b/lib/13/go/raw_book/src/auth.dart @@ -0,0 +1,47 @@ +// // Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// // for details. 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; +// } +// +// @override +// bool operator ==(Object other) => +// other is BookstoreAuth && other._signedIn == _signedIn; +// +// @override +// int get hashCode => _signedIn.hashCode; +// } +// +// class BookstoreAuthScope extends InheritedNotifier { +// const BookstoreAuthScope({ +// required super.notifier, +// required super.child, +// super.key, +// }); +// +// static BookstoreAuth of(BuildContext context) => context +// .dependOnInheritedWidgetOfExactType()! +// .notifier!; +// } diff --git a/lib/13/go/raw_book/src/data.dart b/lib/13/go/raw_book/src/data.dart new file mode 100644 index 0000000..f82a31d --- /dev/null +++ b/lib/13/go/raw_book/src/data.dart @@ -0,0 +1,7 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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/lib/13/go/raw_book/src/data/author.dart b/lib/13/go/raw_book/src/data/author.dart new file mode 100644 index 0000000..affd9a2 --- /dev/null +++ b/lib/13/go/raw_book/src/data/author.dart @@ -0,0 +1,13 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 { + final int id; + final String name; + final books = []; + + Author(this.id, this.name); +} diff --git a/lib/13/go/raw_book/src/data/book.dart b/lib/13/go/raw_book/src/data/book.dart new file mode 100644 index 0000000..f91f038 --- /dev/null +++ b/lib/13/go/raw_book/src/data/book.dart @@ -0,0 +1,15 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 { + final int id; + final String title; + final Author author; + final bool isPopular; + final bool isNew; + + Book(this.id, this.title, this.isPopular, this.isNew, this.author); +} diff --git a/lib/13/go/raw_book/src/data/library.dart b/lib/13/go/raw_book/src/data/library.dart new file mode 100644 index 0000000..14fd496 --- /dev/null +++ b/lib/13/go/raw_book/src/data/library.dart @@ -0,0 +1,61 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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, + }) { + var author = allAuthors.firstWhere( + (author) => author.name == authorName, + orElse: () { + final value = Author(allAuthors.length, authorName); + allAuthors.add(value); + return value; + }, + ); + var book = Book(allBooks.length, title, isPopular, isNew, 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/lib/13/go/raw_book/src/routing.dart b/lib/13/go/raw_book/src/routing.dart new file mode 100644 index 0000000..cf0be2c --- /dev/null +++ b/lib/13/go/raw_book/src/routing.dart @@ -0,0 +1,8 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'routing/delegate.dart'; +export 'routing/parsed_route.dart'; +export 'routing/parser.dart'; +export 'routing/route_state.dart'; diff --git a/lib/13/go/raw_book/src/routing/delegate.dart b/lib/13/go/raw_book/src/routing/delegate.dart new file mode 100644 index 0000000..533e659 --- /dev/null +++ b/lib/13/go/raw_book/src/routing/delegate.dart @@ -0,0 +1,47 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 '../screens/navigator.dart'; +import 'parsed_route.dart'; +import 'route_state.dart'; + +class SimpleRouterDelegate extends RouterDelegate + with ChangeNotifier, PopNavigatorRouterDelegateMixin { + final RouteState routeState; + + @override + final GlobalKey navigatorKey = GlobalKey(); + + SimpleRouterDelegate({ + required this.routeState, + }) { + routeState.addListener(notifyListeners); + } + + @override + Widget build(BuildContext context) => BookstoreNavigator( + navigatorKey: navigatorKey, + ); + + @override + Future setNewRoutePath(ParsedRoute configuration) async { + routeState.route = configuration; + return SynchronousFuture(null); + } + + @override + ParsedRoute get currentConfiguration => routeState.route; + + @override + void dispose() { + routeState.removeListener(notifyListeners); + routeState.dispose(); + super.dispose(); + } +} diff --git a/lib/13/go/raw_book/src/routing/parsed_route.dart b/lib/13/go/raw_book/src/routing/parsed_route.dart new file mode 100644 index 0000000..9a40ac7 --- /dev/null +++ b/lib/13/go/raw_book/src/routing/parsed_route.dart @@ -0,0 +1,51 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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:quiver/core.dart'; + +import 'parser.dart'; + +/// A route path that has been parsed by [TemplateRouteParser]. +class ParsedRoute { + /// The current path location without query parameters. (/book/123) + final String path; + + /// The path template (/book/:id) + final String pathTemplate; + + /// The path parameters ({id: 123}) + final Map parameters; + + /// The query parameters ({search: abc}) + final Map queryParameters; + + static const _mapEquality = MapEquality(); + + ParsedRoute( + this.path, this.pathTemplate, this.parameters, this.queryParameters); + + @override + bool operator ==(Object other) => + other is ParsedRoute && + other.pathTemplate == pathTemplate && + other.path == path && + _mapEquality.equals(parameters, other.parameters) && + _mapEquality.equals(queryParameters, other.queryParameters); + + @override + int get hashCode => hash4( + path, + pathTemplate, + _mapEquality.hash(parameters), + _mapEquality.hash(queryParameters), + ); + + @override + String toString() => ''; +} diff --git a/lib/13/go/raw_book/src/routing/parser.dart b/lib/13/go/raw_book/src/routing/parser.dart new file mode 100644 index 0000000..fb25607 --- /dev/null +++ b/lib/13/go/raw_book/src/routing/parser.dart @@ -0,0 +1,62 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 'package:path_to_regexp/path_to_regexp.dart'; + +import 'parsed_route.dart'; + +/// Used by [TemplateRouteParser] to guard access to routes. +typedef RouteGuard = Future Function(T from); + +/// Parses the URI path into a [ParsedRoute]. +class TemplateRouteParser extends RouteInformationParser { + final List _pathTemplates; + final ParsedRoute initialRoute; + + TemplateRouteParser({ + /// The list of allowed path templates (['/', '/users/:id']) + required List allowedPaths, + + /// The initial route + String initialRoute = '/', + + }) : initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {}), + _pathTemplates = [ + ...allowedPaths, + ], + assert(allowedPaths.contains(initialRoute)); + + @override + Future parseRouteInformation( + RouteInformation routeInformation, + ) async { + print("=======parseRouteInformation:${routeInformation.uri.path}==================="); + + final uri = routeInformation.uri; + final path = uri.toString(); + final queryParams = uri.queryParameters; + var parsedRoute = initialRoute; + + for (var pathTemplate in _pathTemplates) { + final parameters = []; + var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters); + if (pathRegExp.hasMatch(path)) { + final match = pathRegExp.matchAsPrefix(path); + if (match == null) continue; + final params = extract(parameters, match); + parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams); + } + } + + return parsedRoute; + } + + @override + RouteInformation restoreRouteInformation(ParsedRoute configuration) { + print("=======restoreRouteInformation:${configuration}==================="); + + return RouteInformation(uri: Uri.parse(configuration.path)); + } +} diff --git a/lib/13/go/raw_book/src/routing/route_state.dart b/lib/13/go/raw_book/src/routing/route_state.dart new file mode 100644 index 0000000..aa7e1ad --- /dev/null +++ b/lib/13/go/raw_book/src/routing/route_state.dart @@ -0,0 +1,48 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 'parsed_route.dart'; +import 'parser.dart'; + +/// The current route state. To change the current route, call obtain the state +/// using `RouteStateScope.of(context)` and call `go()`: +/// +/// ``` +/// RouteStateScope.of(context).go('/book/2'); +/// ``` +class RouteState extends ChangeNotifier { + final TemplateRouteParser _parser; + ParsedRoute _route; + + RouteState(this._parser) : _route = _parser.initialRoute; + + ParsedRoute get route => _route; + + set route(ParsedRoute route) { + // Don't notify listeners if the path hasn't changed. + if (_route == route) return; + + _route = route; + notifyListeners(); + } + + Future go(String route) async { + this.route = await _parser + .parseRouteInformation(RouteInformation(uri: Uri.parse(route))); + } +} + +/// Provides the current [RouteState] to descendant widgets in the tree. +class RouteStateScope extends InheritedNotifier { + const RouteStateScope({ + required super.notifier, + required super.child, + super.key, + }); + + static RouteState of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.notifier!; +} diff --git a/lib/13/go/raw_book/src/screens/author_details.dart b/lib/13/go/raw_book/src/screens/author_details.dart new file mode 100644 index 0000000..d45f861 --- /dev/null +++ b/lib/13/go/raw_book/src/screens/author_details.dart @@ -0,0 +1,39 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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'; +import '../routing.dart'; +import '../widgets/book_list.dart'; + +class AuthorDetailsScreen extends StatelessWidget { + final Author author; + + const AuthorDetailsScreen({ + super.key, + required this.author, + }); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(author.name), + ), + body: Center( + child: Column( + children: [ + Expanded( + child: BookList( + books: author.books, + onTap: (book) { + RouteStateScope.of(context).go('/book/${book.id}'); + }, + ), + ), + ], + ), + ), + ); +} diff --git a/lib/13/go/raw_book/src/screens/authors.dart b/lib/13/go/raw_book/src/screens/authors.dart new file mode 100644 index 0000000..8965f91 --- /dev/null +++ b/lib/13/go/raw_book/src/screens/authors.dart @@ -0,0 +1,28 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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/library.dart'; +import '../routing.dart'; +import '../widgets/author_list.dart'; + +class AuthorsScreen extends StatelessWidget { + final String title = 'Authors'; + + const AuthorsScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: AuthorList( + authors: libraryInstance.allAuthors, + onTap: (author) { + RouteStateScope.of(context).go('/author/${author.id}'); + }, + ), + ); +} diff --git a/lib/13/go/raw_book/src/screens/book_details.dart b/lib/13/go/raw_book/src/screens/book_details.dart new file mode 100644 index 0000000..72a9346 --- /dev/null +++ b/lib/13/go/raw_book/src/screens/book_details.dart @@ -0,0 +1,66 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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:url_launcher/link.dart'; + +import '../data.dart'; +import 'author_details.dart'; + +class BookDetailsScreen extends StatelessWidget { + final Book? book; + + const BookDetailsScreen({ + super.key, + this.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.headlineMedium, + ), + Text( + book!.author.name, + style: Theme.of(context).textTheme.titleMedium, + ), + TextButton( + child: const Text('View author (Push)'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + AuthorDetailsScreen(author: book!.author), + ), + ); + }, + ), + Link( + uri: Uri.parse('/author/${book!.author.id}'), + builder: (context, followLink) => TextButton( + onPressed: followLink, + child: const Text('View author (Link)'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/13/go/raw_book/src/screens/books.dart b/lib/13/go/raw_book/src/screens/books.dart new file mode 100644 index 0000000..2646f1a --- /dev/null +++ b/lib/13/go/raw_book/src/screens/books.dart @@ -0,0 +1,111 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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'; +import '../routing.dart'; +import '../widgets/book_list.dart'; + +class BooksScreen extends StatefulWidget { + const BooksScreen({ + super.key, + }); + + @override + State createState() => _BooksScreenState(); +} + +class _BooksScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this) + ..addListener(_handleTabIndexChanged); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final newPath = _routeState.route.pathTemplate; + if (newPath.startsWith('/books/popular')) { + _tabController.index = 0; + } else if (newPath.startsWith('/books/new')) { + _tabController.index = 1; + } else if (newPath == '/books/all') { + _tabController.index = 2; + } + } + + @override + void dispose() { + _tabController.removeListener(_handleTabIndexChanged); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Books'), + elevation: 8, + bottom: TabBar( + controller: _tabController, + 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, + ), + ], + ), + ); + + RouteState get _routeState => RouteStateScope.of(context); + + void _handleBookTapped(Book book) { + _routeState.go('/book/${book.id}'); + } + + void _handleTabIndexChanged() { + switch (_tabController.index) { + case 1: + _routeState.go('/books/new'); + case 2: + _routeState.go('/books/all'); + case 0: + default: + _routeState.go('/books/popular'); + break; + } + } +} diff --git a/lib/13/go/raw_book/src/screens/navigator.dart b/lib/13/go/raw_book/src/screens/navigator.dart new file mode 100644 index 0000000..cfa8e68 --- /dev/null +++ b/lib/13/go/raw_book/src/screens/navigator.dart @@ -0,0 +1,108 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 '../auth.dart'; +import '../data.dart'; +import '../routing.dart'; +import '../screens/sign_in.dart'; +import '../widgets/fade_transition_page.dart'; +import 'author_details.dart'; +import 'book_details.dart'; +import 'scaffold.dart'; + +/// Builds the top-level navigator for the app. The pages to display are based +/// on the `routeState` that was parsed by the TemplateRouteParser. +class BookstoreNavigator extends StatefulWidget { + final GlobalKey navigatorKey; + + const BookstoreNavigator({ + required this.navigatorKey, + super.key, + }); + + @override + State createState() => _BookstoreNavigatorState(); +} + +class _BookstoreNavigatorState extends State { + final _signInKey = const ValueKey('Sign in'); + final _scaffoldKey = const ValueKey('App scaffold'); + final _bookDetailsKey = const ValueKey('Book details screen'); + final _authorDetailsKey = const ValueKey('Author details screen'); + + @override + Widget build(BuildContext context) { + final routeState = RouteStateScope.of(context); + final pathTemplate = routeState.route.pathTemplate; + + Book? selectedBook; + if (pathTemplate == '/book/:bookId') { + selectedBook = libraryInstance.allBooks.firstWhereOrNull( + (b) => b.id.toString() == routeState.route.parameters['bookId']); + } + + Author? selectedAuthor; + if (pathTemplate == '/author/:authorId') { + selectedAuthor = libraryInstance.allAuthors.firstWhereOrNull( + (b) => b.id.toString() == routeState.route.parameters['authorId']); + } + + return Navigator( + key: widget.navigatorKey, + onPopPage: (route, dynamic result) { + // When a page that is stacked on top of the scaffold is popped, display + // the /books or /authors tab in BookstoreScaffold. + if (route.settings is Page && + (route.settings as Page).key == _bookDetailsKey) { + routeState.go('/books/popular'); + } + + if (route.settings is Page && + (route.settings as Page).key == _authorDetailsKey) { + routeState.go('/authors'); + } + + return route.didPop(result); + }, + pages: [ + if (routeState.route.pathTemplate == '/signin') + // Display the sign in screen. + FadeTransitionPage( + key: _signInKey, + child: SignInScreen( + onSignIn: (credentials) async { + await routeState.go('/books/popular'); + }, + ), + ) + else ...[ + // Display the app + FadeTransitionPage( + key: _scaffoldKey, + child: const BookstoreScaffold(), + ), + // Add an additional page to the stack if the user is viewing a book + // or an author + if (selectedBook != null) + MaterialPage( + key: _bookDetailsKey, + child: BookDetailsScreen( + book: selectedBook, + ), + ) + else if (selectedAuthor != null) + MaterialPage( + key: _authorDetailsKey, + child: AuthorDetailsScreen( + author: selectedAuthor, + ), + ), + ], + ], + ); + } +} diff --git a/lib/13/go/raw_book/src/screens/scaffold.dart b/lib/13/go/raw_book/src/screens/scaffold.dart new file mode 100644 index 0000000..4949191 --- /dev/null +++ b/lib/13/go/raw_book/src/screens/scaffold.dart @@ -0,0 +1,54 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 '../routing.dart'; +import 'scaffold_body.dart'; + +class BookstoreScaffold extends StatelessWidget { + const BookstoreScaffold({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final routeState = RouteStateScope.of(context); + final selectedIndex = _getSelectedIndex(routeState.route.pathTemplate); + + return Scaffold( + body: AdaptiveNavigationScaffold( + selectedIndex: selectedIndex, + body: const BookstoreScaffoldBody(), + onDestinationSelected: (idx) { + if (idx == 0) routeState.go('/books/popular'); + if (idx == 1) routeState.go('/authors'); + if (idx == 2) routeState.go('/settings'); + }, + destinations: const [ + AdaptiveScaffoldDestination( + title: 'Books', + icon: Icons.book, + ), + AdaptiveScaffoldDestination( + title: 'Authors', + icon: Icons.person, + ), + AdaptiveScaffoldDestination( + title: 'Settings', + icon: Icons.settings, + ), + ], + ), + ); + } + + int _getSelectedIndex(String pathTemplate) { + if (pathTemplate.startsWith('/books')) return 0; + if (pathTemplate == '/authors') return 1; + if (pathTemplate == '/settings') return 2; + return 0; + } +} diff --git a/lib/13/go/raw_book/src/screens/scaffold_body.dart b/lib/13/go/raw_book/src/screens/scaffold_body.dart new file mode 100644 index 0000000..868e32e --- /dev/null +++ b/lib/13/go/raw_book/src/screens/scaffold_body.dart @@ -0,0 +1,63 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 '../routing.dart'; +import '../screens/settings.dart'; +import '../widgets/fade_transition_page.dart'; +import 'authors.dart'; +import 'books.dart'; +import 'scaffold.dart'; + +/// Displays the contents of the body of [BookstoreScaffold] +class BookstoreScaffoldBody extends StatelessWidget { + static GlobalKey navigatorKey = GlobalKey(); + + const BookstoreScaffoldBody({ + super.key, + }); + + @override + Widget build(BuildContext context) { + var currentRoute = RouteStateScope.of(context).route; + + // A nested Router isn't necessary because the back button behavior doesn't + // need to be customized. + return Navigator( + key: navigatorKey, + onPopPage: (route, dynamic result) => route.didPop(result), + pages: [ + if (currentRoute.pathTemplate.startsWith('/authors')) + const FadeTransitionPage( + key: ValueKey('authors'), + child: AuthorsScreen(), + ) + else if (currentRoute.pathTemplate.startsWith('/settings')) + const FadeTransitionPage( + key: ValueKey('settings'), + child: SettingsScreen(), + ) + else if (currentRoute.pathTemplate.startsWith('/books') || + currentRoute.pathTemplate == '/') + const FadeTransitionPage( + key: ValueKey('books'), + child: BooksScreen(), + ) + + // Avoid building a Navigator with an empty `pages` list when the + // RouteState is set to an unexpected path, such as /signin. + // + // Since RouteStateScope is an InheritedNotifier, any change to the + // route will result in a call to this build method, even though this + // widget isn't built when those routes are active. + else + FadeTransitionPage( + key: const ValueKey('empty'), + child: Container(), + ), + ], + ); + } +} diff --git a/lib/13/go/raw_book/src/screens/settings.dart b/lib/13/go/raw_book/src/screens/settings.dart new file mode 100644 index 0000000..cefd834 --- /dev/null +++ b/lib/13/go/raw_book/src/screens/settings.dart @@ -0,0 +1,95 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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:url_launcher/link.dart'; + +import '../auth.dart'; +import '../routing.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State 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({ + super.key, + }); + + @override + Widget build(BuildContext context) => Column( + children: [ + ...[ + Text( + 'Settings', + style: Theme.of(context).textTheme.headlineMedium, + ), + FilledButton( + 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( + child: const Text('Go directly to /book/0 (RouteState)'), + onPressed: () { + RouteStateScope.of(context).go('/book/0'); + }, + ), + ].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/lib/13/go/raw_book/src/screens/sign_in.dart b/lib/13/go/raw_book/src/screens/sign_in.dart new file mode 100644 index 0000000..84f915f --- /dev/null +++ b/lib/13/go/raw_book/src/screens/sign_in.dart @@ -0,0 +1,69 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 { + final String username; + final String password; + + Credentials(this.username, this.password); +} + +class SignInScreen extends StatefulWidget { + final ValueChanged onSignIn; + + const SignInScreen({ + required this.onSignIn, + super.key, + }); + + @override + State 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.headlineMedium), + 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/lib/13/go/raw_book/src/widgets/author_list.dart b/lib/13/go/raw_book/src/widgets/author_list.dart new file mode 100644 index 0000000..8890696 --- /dev/null +++ b/lib/13/go/raw_book/src/widgets/author_list.dart @@ -0,0 +1,32 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 { + final List authors; + final ValueChanged? onTap; + + const AuthorList({ + required this.authors, + this.onTap, + super.key, + }); + + @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/lib/13/go/raw_book/src/widgets/book_list.dart b/lib/13/go/raw_book/src/widgets/book_list.dart new file mode 100644 index 0000000..b3c6370 --- /dev/null +++ b/lib/13/go/raw_book/src/widgets/book_list.dart @@ -0,0 +1,32 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 { + final List books; + final ValueChanged? onTap; + + const BookList({ + required this.books, + this.onTap, + super.key, + }); + + @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/lib/13/go/raw_book/src/widgets/fade_transition_page.dart b/lib/13/go/raw_book/src/widgets/fade_transition_page.dart new file mode 100644 index 0000000..552171b --- /dev/null +++ b/lib/13/go/raw_book/src/widgets/fade_transition_page.dart @@ -0,0 +1,53 @@ +// Copyright 2021, the Flutter project authors. Please see the AUTHORS file +// for details. 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 FadeTransitionPage extends Page { + final Widget child; + final Duration duration; + + const FadeTransitionPage({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 300), + }); + + @override + Route createRoute(BuildContext context) => + PageBasedFadeTransitionRoute(this); +} + +class PageBasedFadeTransitionRoute extends PageRoute { + final FadeTransitionPage _page; + + PageBasedFadeTransitionRoute(this._page) : super(settings: _page); + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; + + @override + Duration get transitionDuration => _page.duration; + + @override + bool get maintainState => true; + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + var curveTween = CurveTween(curve: Curves.easeIn); + return FadeTransition( + opacity: animation.drive(curveTween), + child: (settings as FadeTransitionPage).child, + ); + } + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) => + child; +} diff --git a/lib/13/go/redirection.dart b/lib/13/go/redirection.dart new file mode 100644 index 0000000..aded4b7 --- /dev/null +++ b/lib/13/go/redirection.dart @@ -0,0 +1,146 @@ +// 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'; + +// This scenario demonstrates how to use redirect to handle a sign-in flow. +// +// The GoRouter.redirect method is called before the app is navigate to a +// new page. You can choose to redirect to a different page by returning a +// non-null URL string. + +/// The login information. +class LoginInfo extends ChangeNotifier { + /// The username of login. + String get userName => _userName; + String _userName = ''; + + /// Whether a user has logged in. + bool get loggedIn => _userName.isNotEmpty; + + /// Logs in a user. + void login(String userName) { + _userName = userName; + notifyListeners(); + } + + /// Logs out the current user. + void logout() { + _userName = ''; + notifyListeners(); + } +} + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + final LoginInfo _loginInfo = LoginInfo(); + + /// The title of the app. + static const String 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( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ), + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) => + const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (BuildContext context, GoRouterState state) { + // if the user is not logged in, they need to login + final bool loggedIn = _loginInfo.loggedIn; + final bool loggingIn = state.matchedLocation == '/login'; + if (!loggedIn) { + return '/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, + ); +} + +/// The login screen. +class LoginScreen extends StatelessWidget { + /// Creates a [LoginScreen]. + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: 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 + }, + child: const Text('Login'), + ), + ), + ); +} + +/// The home screen. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final LoginInfo 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: const Center( + child: Text('HomeScreen'), + ), + ); + } +} diff --git a/lib/13/go/shell_route.dart b/lib/13/go/shell_route.dart new file mode 100644 index 0000000..70878b8 --- /dev/null +++ b/lib/13/go/shell_route.dart @@ -0,0 +1,289 @@ +// 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'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _shellNavigatorKey = + GlobalKey(debugLabel: 'shell'); + +// This scenario demonstrates how to set up nested navigation using ShellRoute, +// which is a pattern where an additional Navigator is placed in the widget tree +// to be used instead of the root navigator. This allows deep-links to display +// pages along with other UI components such as a BottomNavigationBar. +// +// This example demonstrates how to display a route within a ShellRoute and also +// push a screen using a different navigator (such as the root Navigator) by +// providing a `parentNavigatorKey`. + +void main() { + runApp(ShellRouteExampleApp()); +} + +/// An example demonstrating how to use [ShellRoute] +class ShellRouteExampleApp extends StatelessWidget { + /// Creates a [ShellRouteExampleApp] + ShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + debugLogDiagnostics: true, + routes: [ + /// Application shell + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (BuildContext context, GoRouterState state, Widget child) { + return ScaffoldWithNavBar(child: child); + }, + routes: [ + /// The first screen to display in the bottom navigation bar. + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return const ScreenA(); + }, + routes: [ + // The details screen to display stacked on the inner Navigator. + // This will cover screen A but not the application shell. + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'A'); + }, + ), + ], + ), + + /// Displayed when the second item in the the bottom navigation bar is + /// selected. + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) { + return const ScreenB(); + }, + routes: [ + /// Same as "/a/details", but displayed on the root Navigator by + /// specifying [parentNavigatorKey]. This will cover both screen B + /// and the application shell. + GoRoute( + path: 'details', + parentNavigatorKey: _rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'B'); + }, + ), + ], + ), + + /// The third screen to display in the bottom navigation bar. + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) { + return const ScreenC(); + }, + routes: [ + // The details screen to display stacked on the inner Navigator. + // This will cover screen A but not the application shell. + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'C'); + }, + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.child, + super.key, + }); + + /// The widget to display in the body of the Scaffold. + /// In this sample, it is a Navigator. + final Widget child; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: child, + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'A Screen', + ), + BottomNavigationBarItem( + icon: Icon(Icons.business), + label: 'B Screen', + ), + BottomNavigationBarItem( + icon: Icon(Icons.notification_important_rounded), + label: 'C Screen', + ), + ], + currentIndex: _calculateSelectedIndex(context), + onTap: (int idx) => _onItemTapped(idx, context), + ), + ); + } + + static int _calculateSelectedIndex(BuildContext context) { + final String location = GoRouterState.of(context).uri.toString(); + if (location.startsWith('/a')) { + return 0; + } + if (location.startsWith('/b')) { + return 1; + } + if (location.startsWith('/c')) { + return 2; + } + return 0; + } + + void _onItemTapped(int index, BuildContext context) { + switch (index) { + case 0: + GoRouter.of(context).go('/a'); + break; + case 1: + GoRouter.of(context).go('/b'); + break; + case 2: + GoRouter.of(context).go('/c'); + break; + } + } +} + +/// The first screen in the bottom navigation bar. +class ScreenA extends StatelessWidget { + /// Constructs a [ScreenA] widget. + const ScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Screen A'), + TextButton( + onPressed: () { + GoRouter.of(context).go('/a/details'); + }, + child: const Text('View A details'), + ), + ], + ), + ), + ); + } +} + +/// The second screen in the bottom navigation bar. +class ScreenB extends StatelessWidget { + /// Constructs a [ScreenB] widget. + const ScreenB({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Screen B'), + TextButton( + onPressed: () { + GoRouter.of(context).go('/b/details'); + }, + child: const Text('View B details'), + ), + ], + ), + ), + ); + } +} + +/// The third screen in the bottom navigation bar. +class ScreenC extends StatelessWidget { + /// Constructs a [ScreenC] widget. + const ScreenC({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Screen C'), + TextButton( + onPressed: () { + GoRouter.of(context).go('/c/details'); + }, + child: const Text('View C details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A, B or C screen. +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Details Screen'), + ), + body: Center( + child: Text( + 'Details for $label', + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } +} diff --git a/lib/13/go/stateful_shell_route.dart b/lib/13/go/stateful_shell_route.dart new file mode 100644 index 0000000..eb0a67b --- /dev/null +++ b/lib/13/go/stateful_shell_route.dart @@ -0,0 +1,324 @@ +// 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'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _sectionANavigatorKey = + GlobalKey(debugLabel: 'sectionANav'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also +// enables deep linking into nested pages. + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // Return the widget that implements the custom shell (in this case + // using a BottomNavigationBar). The StatefulNavigationShell is passed + // to be able access the state of the shell and to navigate to other + // branches in a stateful way. + return ScaffoldWithNavBar(navigationShell: navigationShell); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _sectionANavigatorKey, + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ], + ), + + // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + // It's not necessary to provide a navigatorKey if it isn't also + // needed elsewhere. If not provided, a default key will be used. + routes: [ + GoRoute( + // The screen to display as the root in the second tab of the + // bottom navigation bar. + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'B', + detailsPath: '/b/details/1', + secondDetailsPath: '/b/details/2', + ), + routes: [ + GoRoute( + path: 'details/:param', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'B', + param: state.pathParameters['param'], + ), + ), + ], + ), + ], + ), + + // The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + routes: [ + GoRoute( + // The screen to display as the root in the third tab of the + // bottom navigation bar. + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'C', + detailsPath: '/c/details', + ), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C', + extra: state.extra, + ), + ), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: BottomNavigationBar( + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), + ), + ); + } + + /// Navigate to the current location of the branch at the provided index when + /// tapping an item in the BottomNavigationBar. + void _onTap(BuildContext context, int index) { + // When navigating to a new branch, it's recommended to use the goBranch + // method, as doing so makes sure the last navigation state of the + // Navigator for the branch is restored. + navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + required this.detailsPath, + this.secondDetailsPath, + super.key, + }); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + /// The path to another detail page + final String? secondDetailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Root of section $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); + }, + child: const Text('View details'), + ), + const Padding(padding: EdgeInsets.all(4)), + if (secondDetailsPath != null) + TextButton( + onPressed: () { + GoRouter.of(context).go(secondDetailsPath!); + }, + child: const Text('View more details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.extra, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Optional extra object + final Object? extra; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (widget.extra != null) + Text('Extra: ${widget.extra!}', + style: Theme.of(context).textTheme.titleMedium), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} diff --git a/lib/13/go/transition_animations.dart b/lib/13/go/transition_animations.dart new file mode 100644 index 0000000..f245719 --- /dev/null +++ b/lib/13/go/transition_animations.dart @@ -0,0 +1,175 @@ +// 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'; + +/// To use a custom transition animation, provide a `pageBuilder` with a +/// CustomTransitionPage. +/// +/// To learn more about animation in Flutter, check out the [Introduction to +/// animations](https://docs.flutter.dev/development/ui/animations) page on +/// flutter.dev. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: const DetailsScreen(), + transitionDuration: const Duration(milliseconds: 150), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) { + // Change the opacity of the screen using a Curve based on the the animation's + // value + return FadeTransition( + opacity: + CurveTween(curve: Curves.easeInOut).animate(animation), + child: child, + ); + }, + ); + }, + ), + GoRoute( + path: 'dismissible-details', + pageBuilder: (BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: const DismissibleDetails(), + barrierDismissible: true, + barrierColor: Colors.black38, + opaque: false, + transitionDuration: Duration.zero, + transitionsBuilder: (_, __, ___, Widget child) => child, + ); + }, + ), + GoRoute( + path: 'custom-reverse-transition-duration', + pageBuilder: (BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: const DetailsScreen(), + barrierDismissible: true, + barrierColor: Colors.black38, + opaque: false, + transitionDuration: const Duration(milliseconds: 500), + reverseTransitionDuration: const Duration(milliseconds: 200), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ); + }, + ), + ], + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/details'), + child: const Text('Go to the Details screen'), + ), + const SizedBox(height: 48), + ElevatedButton( + onPressed: () => context.go('/dismissible-details'), + child: const Text('Go to the Dismissible Details screen'), + ), + const SizedBox(height: 48), + ElevatedButton( + onPressed: () => + context.go('/custom-reverse-transition-duration'), + child: const Text( + 'Go to the Custom Reverse Transition Duration Screen', + ), + ), + ], + ), + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go back to the Home screen'), + ), + ], + ), + ), + ); + } +} + +/// The dismissible details screen +class DismissibleDetails extends StatelessWidget { + /// Constructs a [DismissibleDetails] + const DismissibleDetails({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(48), + child: ColoredBox(color: Colors.red), + ); + } +} diff --git a/lib/cases/cases.dart b/lib/cases/cases.dart new file mode 100644 index 0000000..3f5d5e3 --- /dev/null +++ b/lib/cases/cases.dart @@ -0,0 +1,3 @@ +export 'counter/counter_page.dart'; +export 'guess/guess_page.dart'; +export 'paper/paper.dart'; \ No newline at end of file diff --git a/lib/cases/counter/counter_page.dart b/lib/cases/counter/counter_page.dart new file mode 100644 index 0000000..039b571 --- /dev/null +++ b/lib/cases/counter/counter_page.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class CounterPage extends StatefulWidget { + const CounterPage({super.key}); + + + + @override + State createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} \ No newline at end of file diff --git a/lib/cases/guess/guess_app_bar.dart b/lib/cases/guess/guess_app_bar.dart new file mode 100644 index 0000000..ee4bf94 --- /dev/null +++ b/lib/cases/guess/guess_app_bar.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class GuessAppBar extends StatelessWidget implements PreferredSizeWidget { + final VoidCallback onCheck; + final TextEditingController controller; + + const GuessAppBar({ + Key? key, + required this.onCheck, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + leading: Icon(Icons.menu, color: Colors.black), + actions: [ + IconButton( + splashRadius: 20, + onPressed: onCheck, + icon: Icon( + Icons.run_circle_outlined, + color: Colors.blue, + )) + ], + title: TextField( + controller: controller, + keyboardType: TextInputType.number, + decoration: InputDecoration( + filled: true, + fillColor: Color(0xffF3F6F9), + constraints: BoxConstraints(maxHeight: 34), //约束信息 + contentPadding: EdgeInsets.only(top: -14,left: 10), + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + hintText: "输入 0~99 数字", + hintStyle: TextStyle(fontSize: 14)), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/lib/cases/guess/guess_page.dart b/lib/cases/guess/guess_page.dart new file mode 100644 index 0000000..2d0b994 --- /dev/null +++ b/lib/cases/guess/guess_page.dart @@ -0,0 +1,125 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'guess_app_bar.dart'; +import 'result_notice.dart'; + +class GuessPage extends StatefulWidget { + const GuessPage({super.key}); + + @override + State createState() => _GuessPageState(); +} + +class _GuessPageState extends State with SingleTickerProviderStateMixin,AutomaticKeepAliveClientMixin{ + + late AnimationController controller; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + } + + + int _value = 0; + + final Random _random = Random(); + bool _guessing = false; + bool? _isBig; + + + @override + void dispose() { + _guessCtrl.dispose(); + controller.dispose(); + super.dispose(); + } + + void _generateRandomValue() { + setState(() { + _guessing = true; + _value = _random.nextInt(100); + print(_value); + }); + } + + TextEditingController _guessCtrl = TextEditingController(); + + void _onCheck() { + print("=====Check:目标数值:$_value=====${_guessCtrl.text}============"); + int? guessValue = int.tryParse(_guessCtrl.text); + // 游戏未开始,或者输入非整数,无视 + if (!_guessing || guessValue == null) return; + controller.forward(from: 0); + //猜对了 + if (guessValue == _value) { + setState(() { + _isBig = null; + _guessing = false; + }); + return; + } + // 猜错了 + setState(() { + _isBig = guessValue > _value; + }); + _guessCtrl.clear(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: GuessAppBar( + controller: _guessCtrl, + onCheck: _onCheck, + ), + body: Stack( + children: [ + if(_isBig!=null) + Column( + children: [ + if(_isBig!) + ResultNotice(color:Colors.redAccent,info:'大了',controller: controller,), + Spacer(), + if(!_isBig!) + ResultNotice(color:Colors.blueAccent,info:'小了',controller: controller,), + ], + ), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!_guessing) + const Text( + '点击生成随机数值', + ), + Text( + _guessing ? '**' : '$_value', + style: const TextStyle( + fontSize: 68, fontWeight: FontWeight.bold), + ), + ], + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _guessing ? null : _generateRandomValue, + backgroundColor: _guessing ? Colors.grey : Colors.blue, + tooltip: 'Increment', + child: const Icon(Icons.generating_tokens_outlined), + ), + ); + } + + @override + // TODO: implement wantKeepAlive + bool get wantKeepAlive => true; + + +} diff --git a/lib/cases/guess/result_notice.dart b/lib/cases/guess/result_notice.dart new file mode 100644 index 0000000..b239d69 --- /dev/null +++ b/lib/cases/guess/result_notice.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class ResultNotice extends StatelessWidget { + final Color color; + final String info; + final AnimationController controller; + + const ResultNotice({ + Key? key, + required this.color, + required this.info, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + alignment: Alignment.center, + color: color, + child: AnimatedBuilder( + animation: controller, + builder: (_, child) => Text( + info, + style: TextStyle( + fontSize: 54 * (controller.value), + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} diff --git a/lib/cases/paper/color_selector.dart b/lib/cases/paper/color_selector.dart new file mode 100644 index 0000000..85fa6f4 --- /dev/null +++ b/lib/cases/paper/color_selector.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class ColorSelector extends StatelessWidget { + final List supportColors; + final ValueChanged onSelect; + final int activeIndex; + + const ColorSelector({ + Key? key, + required this.supportColors, + required this.activeIndex, + required this.onSelect, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8), + child: Wrap( + // crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate( + supportColors.length, + _buildByIndex, + )), + ); + } + + Widget _buildByIndex(int index) { + bool select = index == activeIndex; + return GestureDetector( + onTap: () => onSelect(index), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(2), + width: 24, + height: 24, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + // borderRadius: BorderRadius.circular(8), + border: select ? Border.all(color: Colors.blue) : null + ), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: supportColors[index], + ), + ), + ), + ); + } +} diff --git a/lib/cases/paper/conform_dialog.dart b/lib/cases/paper/conform_dialog.dart new file mode 100644 index 0000000..fcfee66 --- /dev/null +++ b/lib/cases/paper/conform_dialog.dart @@ -0,0 +1,102 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// create by 张风捷特烈 on 2020-04-23 +/// contact me by email 1981462002@qq.com +/// 说明: + +class ConformDialog extends StatelessWidget { + final String title; + final String msg; + final String conformText; + final VoidCallback onConform; + + ConformDialog({ + Key? key, + required this.title, + required this.msg, + required this.onConform, + this.conformText = '删除', + }) : super(key: key); + + final ButtonStyle conformStyle = ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + foregroundColor: Colors.white, + elevation: 0, + minimumSize: const Size(70, 35), + padding: EdgeInsets.zero, + shape: const StadiumBorder(), + ); + + final ButtonStyle cancelStyle = OutlinedButton.styleFrom( + minimumSize: const Size(70, 35), + elevation: 0, + padding: EdgeInsets.zero, + shape: const StadiumBorder(), + ); + + @override + Widget build(BuildContext context) { + return Dialog( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildTitle(), + _buildMessage(), + _buildButtons(context), + ], + ), + ), + ); + } + + Widget _buildTitle()=>Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.warning_amber_rounded, color: Colors.orange), + const SizedBox( + width: 10, + ), + Text( + title, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ); + + Widget _buildMessage()=>Padding( + padding: const EdgeInsets.only( + top: 15, + bottom: 15, + ), + child: Text( + msg, + style: const TextStyle(fontSize: 14), + ), + ); + + Widget _buildButtons(BuildContext context) => Row( + children: [ + const Spacer(), + OutlinedButton( + onPressed: Navigator.of(context).pop, + style: cancelStyle, + child: const Text( + '取消', + style: TextStyle(fontSize: 12, color: Colors.grey), + )), + const SizedBox(width: 10), + ElevatedButton( + onPressed: onConform, + style: conformStyle, + child: Text(conformText), + ), + ], + ); +} diff --git a/lib/cases/paper/model.dart b/lib/cases/paper/model.dart new file mode 100644 index 0000000..f65bbca --- /dev/null +++ b/lib/cases/paper/model.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class Line { + List points; + Color color; + double strokeWidth; + + Line({ + required this.points, + this.color = Colors.black, + this.strokeWidth = 1, + }); + +} \ No newline at end of file diff --git a/lib/cases/paper/paper.dart b/lib/cases/paper/paper.dart new file mode 100644 index 0000000..6c2d533 --- /dev/null +++ b/lib/cases/paper/paper.dart @@ -0,0 +1,194 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'color_selector.dart'; +import 'conform_dialog.dart'; + +import 'model.dart'; +import 'paper_app_bar.dart'; +import 'stork_width_selector.dart'; + +class Paper extends StatefulWidget { + const Paper({Key? key}) : super(key: key); + + @override + State createState() => _PaperState(); +} + +class _PaperState extends State with AutomaticKeepAliveClientMixin { + + @override + bool get wantKeepAlive => true; + + + List _lines = []; // 线列表 + + int _activeColorIndex = 0; // 颜色激活索引 + int _activeStorkWidthIndex = 0; // 线宽激活索引 + + // 支持的颜色 + final List supportColors = [ + Colors.black, + Colors.red, + Colors.orange, + Colors.yellow, + Colors.green, + Colors.blue, + Colors.indigo, + Colors.purple, + Colors.pink, + Colors.grey, + Colors.redAccent, + Colors.orangeAccent, + Colors.yellowAccent, + Colors.greenAccent, + Colors.blueAccent, + Colors.indigoAccent, + Colors.purpleAccent, + Colors.pinkAccent, + ]; + + // 支持的线粗 + final List supportStorkWidths = [1, 2, 4, 6, 8, 10]; + + List _historyLines = []; + + void _back() { + Line line = _lines.removeLast(); + _historyLines.add(line); + setState(() {}); + } + + void _revocation() { + Line line = _historyLines.removeLast(); + _lines.add(line); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PaperAppBar( + onClear: _showClearDialog, + onBack: _lines.isEmpty ? null : _back, + onRevocation: _historyLines.isEmpty ? null : _revocation, + ), + body: Stack( + children: [ + GestureDetector( + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + child: CustomPaint( + painter: PaperPainter(lines: _lines), + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ), + ), + Positioned( + bottom: 0, + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Expanded( + child: ColorSelector( + supportColors: supportColors, + activeIndex: _activeColorIndex, + onSelect: _onSelectColor, + ), + ), + StorkWidthSelector( + supportStorkWidths: supportStorkWidths, + color: supportColors[_activeColorIndex], + activeIndex: _activeStorkWidthIndex, + onSelect: _onSelectStorkWidth, + ), + ], + ), + ), + ], + ), + ); + } + + void _showClearDialog() { + String msg = "您的当前操作会清空绘制内容,是否确定删除!"; + showDialog( + context: context, + builder: (ctx) => ConformDialog( + title: '清空提示', + conformText: '确定', + msg: msg, + onConform: _clear, + )); + } + + void _clear() { + _lines.clear(); + Navigator.of(context).pop(); + setState(() {}); + } + + void _onPanStart(DragStartDetails details) { + _lines.add(Line( + points: [details.localPosition], + strokeWidth: supportStorkWidths[_activeStorkWidthIndex], + color: supportColors[_activeColorIndex], + )); + } + + void _onPanUpdate(DragUpdateDetails details) { + Offset point = details.localPosition; + double distance = (_lines.last.points.last - point).distance; + if (distance > 5) { + _lines.last.points.add(details.localPosition); + setState(() {}); + } + } + + void _onSelectStorkWidth(int index) { + if (index != _activeStorkWidthIndex) { + setState(() { + _activeStorkWidthIndex = index; + }); + } + } + + void _onSelectColor(int index) { + if (index != _activeColorIndex) { + setState(() { + _activeColorIndex = index; + }); + } + } + +} + +class PaperPainter extends CustomPainter { + PaperPainter({ + required this.lines, + }) { + _paint = Paint() + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + } + + late Paint _paint; + final List lines; + + @override + void paint(Canvas canvas, Size size) { + for (int i = 0; i < lines.length; i++) { + drawLine(canvas, lines[i]); + } + } + + ///根据点位绘制线 + void drawLine(Canvas canvas, Line line) { + _paint.color = line.color; + _paint.strokeWidth = line.strokeWidth; + canvas.drawPoints(PointMode.polygon, line.points, _paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => true; +} diff --git a/lib/cases/paper/paper_app_bar.dart b/lib/cases/paper/paper_app_bar.dart new file mode 100644 index 0000000..e7c136e --- /dev/null +++ b/lib/cases/paper/paper_app_bar.dart @@ -0,0 +1,76 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PaperAppBar extends StatelessWidget implements PreferredSizeWidget { + final VoidCallback onClear; + final VoidCallback? onBack; + final VoidCallback? onRevocation; + + const PaperAppBar({ + Key? key, + required this.onClear, + this.onBack, + this.onRevocation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + leading: BackUpButtons( + onBack: onBack, + onRevocation: onRevocation, + ), + leadingWidth: 100, + title: Text('画板绘制'), + actions: [ + IconButton( + splashRadius: 20, + onPressed: onClear, + icon: Icon(CupertinoIcons.delete, size: 20)) + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class BackUpButtons extends StatelessWidget { + final VoidCallback? onBack; + final VoidCallback? onRevocation; + + const BackUpButtons({ + Key? key, + required this.onBack, + required this.onRevocation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + const BoxConstraints cts = BoxConstraints(minHeight: 32, minWidth: 32); + Color backColor = onBack ==null?Colors.grey:Colors.black; + Color revocationColor = onRevocation ==null?Colors.grey:Colors.black; + return Center( + child: Wrap( + children: [ + Transform.scale( + scaleX: -1, + child: IconButton( + splashRadius: 20, + constraints: cts, + onPressed: onBack, + icon: Icon(Icons.next_plan_outlined,color: backColor), + ), + ), + IconButton( + splashRadius: 20, + onPressed: onRevocation, + constraints: cts, + icon: Icon(Icons.next_plan_outlined, color: revocationColor), + ) + ], + ), + ); + } +} diff --git a/lib/cases/paper/stork_width_selector.dart b/lib/cases/paper/stork_width_selector.dart new file mode 100644 index 0000000..d399176 --- /dev/null +++ b/lib/cases/paper/stork_width_selector.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class StorkWidthSelector extends StatelessWidget { + final List supportStorkWidths; + final ValueChanged onSelect; + final int activeIndex; + final Color color; + + const StorkWidthSelector({ + Key? key, + required this.supportStorkWidths, + required this.activeIndex, + required this.onSelect, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate( + supportStorkWidths.length, + _buildByIndex, + )), + ); + } + + Widget _buildByIndex(int index) { + bool select = index == activeIndex; + return GestureDetector( + onTap: () => onSelect(index), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 2), + width: 70, + height: 18, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: select ? Border.all(color: Colors.blue) : null), + child: Container( + width: 50, + color: color, + height: supportStorkWidths[index], + ), + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..8236f57 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 4f612fd..8fe732b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + adaptive_breakpoints: + dependency: transitive + description: + name: adaptive_breakpoints + sha256: "3aa6ef09074fe7824dfdc9a6d9f955c8a3bae0fb71c495cbd72f471a87699b4f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.6" + adaptive_navigation: + dependency: "direct main" + description: + name: adaptive_navigation + sha256: "271d3e54147ce37b389c3d7d19777b4340e4ccd440d0349457c8b8db197b1c41" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.9" async: dependency: transitive description: @@ -49,6 +65,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -128,6 +152,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -136,6 +168,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.8.3" + path_to_regexp: + dependency: "direct main" + description: + name: path_to_regexp + sha256: "169d78fbd55e61ea8873bcca545979f559d22238f66facdd7ef30870c7f53327" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.6" + provider: + dependency: "direct main" + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.5" + quiver: + dependency: "direct main" + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" sky_engine: dependency: transitive description: flutter @@ -189,6 +253,85 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.6.0" + toly_menu: + dependency: "direct main" + description: + path: "E:\\Projects\\Flutter\\packages\\toly_menu" + relative: false + source: path + version: "0.0.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.14" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.5" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.20" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.8" + url_strategy: + dependency: "direct main" + description: + name: url_strategy + sha256: "42b68b42a9864c4d710401add17ad06e28f1c1d5500c93b98c431f6b0ea4ab87" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" vector_math: dependency: transitive description: @@ -206,5 +349,5 @@ packages: source: hosted version: "0.1.4-beta" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.1.1 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index e157af2..160ec64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,11 +30,19 @@ environment: dependencies: flutter: sdk: flutter - + adaptive_navigation: ^0.0.4 go_router: ^10.1.0 + provider: 6.0.5 + url_launcher: ^6.0.7 + equatable: ^2.0.5 + url_strategy: ^0.2.0 + quiver: ^3.1.0 + path_to_regexp: ^0.4.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + toly_menu: + path: E:\Projects\Flutter\packages\toly_menu dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 9392b36..c92ca90 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -26,7 +26,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); + Win32Window::Size size(640, 360); if (!window.Create(L"iroute", origin, size)) { return EXIT_FAILURE; }