保留Flutter中的子选项卡中的历史导航,使用NavBar和go路由。

huangapple go评论104阅读模式
英文:

Keep history navigation in child tab with flutter and NavBar and go router

问题

以下是您提供的内容的翻译:

我想知道如何在底部导航栏的子标签页中保留导航历史记录。

目前,我遵循这个帖子来创建一个带有导航栏的导航。

我想在标签页中导航时保留历史记录(从主页到FeedPeople再到FeedPeople再到FeedPeople...):

  • 主页(通用动态源)(从链接访问FeedPeople)
    • FeedPeople => FeedPeople => FeedPeople...
  • 发现
  • 商店
  • 我的

问题是:当我在主页上点击链接以推送FeedPeople并查看profil/:uuid,然后再次点击FeedPeople,再次点击FeedPeople,等等...当我从导航栏中的标签,例如商店,单击并再次单击主页时,我看不到上一个FeedPeople(上一个profil),而是直接回到主屏幕。

main.dart

  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:my_app/feed.dart';
  4. import 'package:my_app/feed_people.dart';
  5. import 'package:my_app/route_names.dart';
  6. final _rootNavigatorKey = GlobalKey<NavigatorState>();
  7. final _shellNavigatorKey = GlobalKey<NavigatorState>();
  8. void main() {
  9. runApp(MyApp());
  10. }
  11. class MyApp extends StatelessWidget {
  12. MyApp({super.key});
  13. @override
  14. Widget build(BuildContext context) {
  15. return MaterialApp.router(
  16. title: 'Flutter Demo',
  17. routerConfig: router,
  18. );
  19. }
  20. final router = GoRouter(
  21. initialLocation: '/',
  22. navigatorKey: _rootNavigatorKey,
  23. routes: [
  24. ShellRoute(
  25. navigatorKey: _shellNavigatorKey,
  26. pageBuilder: (context, state, child) {
  27. print(state.location);
  28. return NoTransitionPage(
  29. child: ScaffoldWithNavBar(
  30. location: state.location,
  31. child: child,
  32. ));
  33. },
  34. routes: [
  35. GoRoute(
  36. path: '/',
  37. parentNavigatorKey: _shellNavigatorKey,
  38. pageBuilder: (context, state) {
  39. return const NoTransitionPage(
  40. child: Feed(uuid: 'a78987-hiIh!ç767897'),
  41. );
  42. },
  43. routes: [
  44. GoRoute(
  45. name: RoutesNames.feedPeople,
  46. path: 'profil/:uuid',
  47. builder: (context, state) =>
  48. FeedPeople(uuid: state.pathParameters['uuid']!))
  49. ]),
  50. GoRoute(
  51. path: '/discover',
  52. parentNavigatorKey: _shellNavigatorKey,
  53. pageBuilder: (context, state) {
  54. return const NoTransitionPage(
  55. child: Scaffold(
  56. body: Center(child: Text("Discover")),
  57. ),
  58. );
  59. },
  60. ),
  61. GoRoute(
  62. parentNavigatorKey: _shellNavigatorKey,
  63. path: '/shop',
  64. pageBuilder: (context, state) {
  65. return const NoTransitionPage(
  66. child: Scaffold(
  67. body: Center(child: Text("Shop")),
  68. ),
  69. );
  70. }),
  71. ],
  72. ),
  73. GoRoute(
  74. parentNavigatorKey: _rootNavigatorKey,
  75. path: '/login',
  76. pageBuilder: (context, state) {
  77. return NoTransitionPage(
  78. key: UniqueKey(),
  79. child: Scaffold(
  80. appBar: AppBar(),
  81. body: const Center(
  82. child: Text("Login"),
  83. ),
  84. ),
  85. );
  86. },
  87. ),
  88. ],
  89. );
  90. }
  91. class ScaffoldWithNavBar extends StatefulWidget {
  92. String location;
  93. ScaffoldWithNavBar({super.key, required this.child, required this.location});
  94. final Widget child;
  95. @override
  96. State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
  97. }
  98. class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
  99. int _currentIndex = 0;
  100. static const List<MyCustomBottomNavBarItem> tabs = [
  101. MyCustomBottomNavBarItem(
  102. icon: Icon(Icons.home),
  103. activeIcon: Icon(Icons.home),
  104. label: 'HOME',
  105. initialLocation: '/',
  106. ),
  107. MyCustomBottomNavBarItem(
  108. icon: Icon(Icons.explore_outlined),
  109. activeIcon: Icon(Icons.explore),
  110. label: 'DISCOVER',
  111. initialLocation: '/discover',
  112. ),
  113. MyCustomBottomNavBarItem(
  114. icon: Icon(Icons.storefront_outlined),
  115. activeIcon: Icon(Icons.storefront),
  116. label: 'SHOP',
  117. initialLocation: '/shop',
  118. ),
  119. MyCustomBottomNavBarItem(
  120. icon: Icon(Icons.account_circle_outlined),
  121. activeIcon: Icon(Icons.account_circle),
  122. label: 'MY',
  123. initialLocation: '/login',
  124. ),
  125. ];
  126. @override
  127. Widget build(BuildContext context) {
  128. const labelStyle = TextStyle(fontFamily: 'Roboto');
  129. return Scaffold(
  130. body: SafeArea(child: widget.child),
  131. bottomNavigationBar: BottomNavigationBar(
  132. selectedLabelStyle: labelStyle,
  133. unselectedLabelStyle: labelStyle,
  134. selectedItemColor: const Color(0xFF434343),
  135. selectedFontSize: 12,
  136. unselectedItemColor: const Color(0xFF838383),
  137. showUnselectedLabels: true,
  138. type: BottomNavigationBarType.fixed,
  139. onTap: (int index) {
  140. _goOtherTab(context, index);
  141. },
  142. currentIndex: widget.location == '/'
  143. ? 0
  144. : widget.location == '/discover'
  145. ? 1
  146. : widget.location == '/shop'
  147. ? 2
  148. : 3,
  149. items: tabs,
  150. ),
  151. );
  152. }
  153. void _goOtherTab(BuildContext context, int index) {
  154. if (index == _currentIndex) return;
  155. GoRouter router = GoRouter.of(context);
  156. String location = tabs[index].initialLocation;
  157. setState(() {
  158. _currentIndex = index;
  159. });
  160. if (index == 3) {
  161. context.push('/login');
  162. } else {
  163. router.go(location);
  164. }
  165. }
  166. }
  167. class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
  168. final String initialLocation;
  169. const MyCustomBottomNavBarItem(
  170. {required this.initialLocation,
  171. required Widget icon,
  172. String? label,
  173. Widget? activeIcon})
  174. : super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
  175. }

我尝试在主页路由中直接设置路由,但是出现了相同的问题。历史记录没有保留,当我从导航栏导航时,我直接看到了“第一个”屏幕(主页),而不是上一个FeedPeople。

英文:

I would like to know how can I keep history navigation in child on tab from NavBar.

Currently I follow this post to create a navigation with NavBar.

I would like to keep the history when I navigate in the tab (Home => FeedPeople => FeedPeople => FeedPeople...) :

  • Home (General Feed) (access to FeedPeople from link)
    • FeedPeople => FeedPeople => FeedPeople...
  • Discover
  • Shop
  • My

The problem: when I click on link on Home to push FeedPeople and see the profil/:uuid, and again FeedPeople, and again FeedPeople, etc... when I click on tab from NavBar, shop for example, and click again on Home, I don't see the last FeedPeople (last profil) but directly Home screen.

main.dart

  1. import &#39;package:flutter/material.dart&#39;;
  2. import &#39;package:go_router/go_router.dart&#39;;
  3. import &#39;package:my_app/feed.dart&#39;;
  4. import &#39;package:my_app/feed_people.dart&#39;;
  5. import &#39;package:my_app/route_names.dart&#39;;
  6. final _rootNavigatorKey = GlobalKey&lt;NavigatorState&gt;();
  7. final _shellNavigatorKey = GlobalKey&lt;NavigatorState&gt;();
  8. void main() {
  9. runApp(MyApp());
  10. }
  11. class MyApp extends StatelessWidget {
  12. MyApp({super.key});
  13. // This widget is the root of your application.
  14. @override
  15. Widget build(BuildContext context) {
  16. return MaterialApp.router(
  17. title: &#39;Flutter Demo&#39;,
  18. routerConfig: router,
  19. );
  20. }
  21. final router = GoRouter(
  22. initialLocation: &#39;/&#39;,
  23. navigatorKey: _rootNavigatorKey,
  24. routes: [
  25. ShellRoute(
  26. navigatorKey: _shellNavigatorKey,
  27. pageBuilder: (context, state, child) {
  28. print(state.location);
  29. return NoTransitionPage(
  30. child: ScaffoldWithNavBar(
  31. location: state.location,
  32. child: child,
  33. ));
  34. },
  35. routes: [
  36. GoRoute(
  37. path: &#39;/&#39;,
  38. parentNavigatorKey: _shellNavigatorKey,
  39. pageBuilder: (context, state) {
  40. return const NoTransitionPage(
  41. child: Feed(uuid: &#39;a78987-hiIh!&#231;767897&#39;),
  42. );
  43. },
  44. routes: [
  45. GoRoute(
  46. name: RoutesNames.feedPeople,
  47. path: &#39;profil/:uuid&#39;,
  48. builder: (context, state) =&gt;
  49. FeedPeople(uuid: state.pathParameters[&#39;uuid&#39;]!))
  50. ]),
  51. GoRoute(
  52. path: &#39;/discover&#39;,
  53. parentNavigatorKey: _shellNavigatorKey,
  54. pageBuilder: (context, state) {
  55. return const NoTransitionPage(
  56. child: Scaffold(
  57. body: Center(child: Text(&quot;Discover&quot;)),
  58. ),
  59. );
  60. },
  61. ),
  62. GoRoute(
  63. parentNavigatorKey: _shellNavigatorKey,
  64. path: &#39;/shop&#39;,
  65. pageBuilder: (context, state) {
  66. return const NoTransitionPage(
  67. child: Scaffold(
  68. body: Center(child: Text(&quot;Shop&quot;)),
  69. ),
  70. );
  71. }),
  72. ],
  73. ),
  74. GoRoute(
  75. parentNavigatorKey: _rootNavigatorKey,
  76. path: &#39;/login&#39;,
  77. pageBuilder: (context, state) {
  78. return NoTransitionPage(
  79. key: UniqueKey(),
  80. child: Scaffold(
  81. appBar: AppBar(),
  82. body: const Center(
  83. child: Text(&quot;Login&quot;),
  84. ),
  85. ),
  86. );
  87. },
  88. ),
  89. ],
  90. );
  91. }
  92. // ignore: must_be_immutable
  93. class ScaffoldWithNavBar extends StatefulWidget {
  94. String location;
  95. ScaffoldWithNavBar({super.key, required this.child, required this.location});
  96. final Widget child;
  97. @override
  98. State&lt;ScaffoldWithNavBar&gt; createState() =&gt; _ScaffoldWithNavBarState();
  99. }
  100. class _ScaffoldWithNavBarState extends State&lt;ScaffoldWithNavBar&gt; {
  101. int _currentIndex = 0;
  102. static const List&lt;MyCustomBottomNavBarItem&gt; tabs = [
  103. MyCustomBottomNavBarItem(
  104. icon: Icon(Icons.home),
  105. activeIcon: Icon(Icons.home),
  106. label: &#39;HOME&#39;,
  107. initialLocation: &#39;/&#39;,
  108. ),
  109. MyCustomBottomNavBarItem(
  110. icon: Icon(Icons.explore_outlined),
  111. activeIcon: Icon(Icons.explore),
  112. label: &#39;DISCOVER&#39;,
  113. initialLocation: &#39;/discover&#39;,
  114. ),
  115. MyCustomBottomNavBarItem(
  116. icon: Icon(Icons.storefront_outlined),
  117. activeIcon: Icon(Icons.storefront),
  118. label: &#39;SHOP&#39;,
  119. initialLocation: &#39;/shop&#39;,
  120. ),
  121. MyCustomBottomNavBarItem(
  122. icon: Icon(Icons.account_circle_outlined),
  123. activeIcon: Icon(Icons.account_circle),
  124. label: &#39;MY&#39;,
  125. initialLocation: &#39;/login&#39;,
  126. ),
  127. ];
  128. @override
  129. Widget build(BuildContext context) {
  130. const labelStyle = TextStyle(fontFamily: &#39;Roboto&#39;);
  131. return Scaffold(
  132. body: SafeArea(child: widget.child),
  133. bottomNavigationBar: BottomNavigationBar(
  134. selectedLabelStyle: labelStyle,
  135. unselectedLabelStyle: labelStyle,
  136. selectedItemColor: const Color(0xFF434343),
  137. selectedFontSize: 12,
  138. unselectedItemColor: const Color(0xFF838383),
  139. showUnselectedLabels: true,
  140. type: BottomNavigationBarType.fixed,
  141. onTap: (int index) {
  142. _goOtherTab(context, index);
  143. },
  144. currentIndex: widget.location == &#39;/&#39;
  145. ? 0
  146. : widget.location == &#39;/discover&#39;
  147. ? 1
  148. : widget.location == &#39;/shop&#39;
  149. ? 2
  150. : 3,
  151. items: tabs,
  152. ),
  153. );
  154. }
  155. void _goOtherTab(BuildContext context, int index) {
  156. if (index == _currentIndex) return;
  157. GoRouter router = GoRouter.of(context);
  158. String location = tabs[index].initialLocation;
  159. setState(() {
  160. _currentIndex = index;
  161. });
  162. if (index == 3) {
  163. context.push(&#39;/login&#39;);
  164. } else {
  165. router.go(location);
  166. }
  167. }
  168. }
  169. class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
  170. final String initialLocation;
  171. const MyCustomBottomNavBarItem(
  172. {required this.initialLocation,
  173. required Widget icon,
  174. String? label,
  175. Widget? activeIcon})
  176. : super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
  177. }

I tried to set the route directly in Home routes, but same problem. The history isn't keep, and when I navigate from NavBar, I see directly the "first" screen (Home and not the last FeedPeople).

答案1

得分: 1

使用go_router时,使用StatefulShellRoute来获取选项卡的持久状态。以下是修改后的dartpad go_router示例,但使用StatefulShellRoute和NavigationBar(而不是BottomNavigationBar)以获得所需的输出。

您可以在dartpad中使用以下代码进行测试此处

  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:english_words/english_words.dart';
  4. import 'dart:math' as math;
  5. void main() {
  6. runApp(MusicAppDemo());
  7. }
  8. class MusicAppDemo extends StatelessWidget {
  9. MusicAppDemo({Key? key}) : super(key: key);
  10. final MusicDatabase database = MusicDatabase.mock();
  11. final GoRouter _router = GoRouter(
  12. initialLocation: '/login',
  13. routes: <RouteBase>[
  14. GoRoute(
  15. path: '/login',
  16. builder: (BuildContext context, GoRouterState state) {
  17. return const LoginScreen();
  18. },
  19. ),
  20. StatefulShellRoute.indexedStack(
  21. builder: (BuildContext context, GoRouterState state,
  22. StatefulNavigationShell navigationShell) {
  23. return MusicAppShell(
  24. navigationShell: navigationShell,
  25. );
  26. },
  27. branches: <StatefulShellBranch>[
  28. StatefulShellBranch(
  29. routes: <RouteBase>[
  30. GoRoute(
  31. path: '/library',
  32. pageBuilder: (context, state) {
  33. return FadeTransitionPage(
  34. child: const LibraryScreen(),
  35. key: state.pageKey,
  36. );
  37. },
  38. routes: <RouteBase>[
  39. GoRoute(
  40. path: 'album/:albumId',
  41. builder: (BuildContext context, GoRouterState state) {
  42. return AlbumScreen(
  43. albumId: state.pathParameters['albumId'],
  44. );
  45. },
  46. routes: [
  47. GoRoute(
  48. path: 'song/:songId',
  49. // Display on the root Navigator
  50. builder: (BuildContext context, GoRouterState state) {
  51. return SongScreen(
  52. songId: state.pathParameters['songId']!,
  53. );
  54. },
  55. ),
  56. ],
  57. ),
  58. ],
  59. ),
  60. ],
  61. ),
  62. StatefulShellBranch(
  63. routes: <RouteBase>[
  64. GoRoute(
  65. path: '/recents',
  66. pageBuilder: (context, state) {
  67. return FadeTransitionPage(
  68. child: const RecentlyPlayedScreen(),
  69. key: state.pageKey,
  70. );
  71. },
  72. routes: <RouteBase>[
  73. GoRoute(
  74. path: 'song/:songId',
  75. // Display on the root Navigator
  76. builder: (BuildContext context, GoRouterState state) {
  77. return SongScreen(
  78. songId: state.pathParameters['songId']!,
  79. );
  80. },
  81. ),
  82. ],
  83. ),
  84. ],
  85. ),
  86. StatefulShellBranch(
  87. routes: <RouteBase>[
  88. GoRoute(
  89. path: '/search',
  90. pageBuilder: (context, state) {
  91. final query = state.queryParameters['q'] ?? '';
  92. return FadeTransitionPage(
  93. child: SearchScreen(
  94. query: query,
  95. ),
  96. key: state.pageKey,
  97. );
  98. },
  99. ),
  100. ],
  101. ),
  102. ],
  103. ),
  104. ],
  105. );
  106. @override
  107. Widget build(BuildContext context) {
  108. return MaterialApp.router(
  109. title: 'Music app',
  110. theme: ThemeData(primarySwatch: Colors.pink),
  111. routerConfig: _router,
  112. builder: (context, child) {
  113. return MusicDatabaseScope(
  114. state: database,
  115. child: child!,
  116. );
  117. },
  118. );
  119. }
  120. }
  121. // ...(以下为其他类和部分代码,请参考原始内容)
英文:

With go_router, use StatefulShellRoute to obtain the persistent state for the tabs. Below is a modified version of the dartpad go_router example but uses StatefulShellRoute and NavigationBar (instead of BottomNavigationBar) to get the desired output.

you can play with the below code in dartpart here

  1. import &#39;package:flutter/material.dart&#39;;
  2. import &#39;package:go_router/go_router.dart&#39;;
  3. import &#39;package:english_words/english_words.dart&#39;;
  4. import &#39;dart:math&#39; as math;
  5. void main() {
  6. runApp(MusicAppDemo());
  7. }
  8. class MusicAppDemo extends StatelessWidget {
  9. MusicAppDemo({Key? key}) : super(key: key);
  10. final MusicDatabase database = MusicDatabase.mock();
  11. final GoRouter _router = GoRouter(
  12. initialLocation: &#39;/login&#39;,
  13. routes: &lt;RouteBase&gt;[
  14. GoRoute(
  15. path: &#39;/login&#39;,
  16. builder: (BuildContext context, GoRouterState state) {
  17. return const LoginScreen();
  18. },
  19. ),
  20. StatefulShellRoute.indexedStack(
  21. builder: (BuildContext context, GoRouterState state,
  22. StatefulNavigationShell navigationShell) {
  23. return MusicAppShell(
  24. navigationShell: navigationShell,
  25. );
  26. },
  27. branches: &lt;StatefulShellBranch&gt;[
  28. StatefulShellBranch(
  29. routes: &lt;RouteBase&gt;[
  30. GoRoute(
  31. path: &#39;/library&#39;,
  32. pageBuilder: (context, state) {
  33. return FadeTransitionPage(
  34. child: const LibraryScreen(),
  35. key: state.pageKey,
  36. );
  37. },
  38. routes: &lt;RouteBase&gt;[
  39. GoRoute(
  40. path: &#39;album/:albumId&#39;,
  41. builder: (BuildContext context, GoRouterState state) {
  42. return AlbumScreen(
  43. albumId: state.pathParameters[&#39;albumId&#39;],
  44. );
  45. },
  46. routes: [
  47. GoRoute(
  48. path: &#39;song/:songId&#39;,
  49. // Display on the root Navigator
  50. builder: (BuildContext context, GoRouterState state) {
  51. return SongScreen(
  52. songId: state.pathParameters[&#39;songId&#39;]!,
  53. );
  54. },
  55. ),
  56. ],
  57. ),
  58. ],
  59. ),
  60. ],
  61. ),
  62. StatefulShellBranch(
  63. routes: &lt;RouteBase&gt;[
  64. GoRoute(
  65. path: &#39;/recents&#39;,
  66. pageBuilder: (context, state) {
  67. return FadeTransitionPage(
  68. child: const RecentlyPlayedScreen(),
  69. key: state.pageKey,
  70. );
  71. },
  72. routes: &lt;RouteBase&gt;[
  73. GoRoute(
  74. path: &#39;song/:songId&#39;,
  75. // Display on the root Navigator
  76. builder: (BuildContext context, GoRouterState state) {
  77. return SongScreen(
  78. songId: state.pathParameters[&#39;songId&#39;]!,
  79. );
  80. },
  81. ),
  82. ],
  83. ),
  84. ],
  85. ),
  86. StatefulShellBranch(
  87. routes: &lt;RouteBase&gt;[
  88. GoRoute(
  89. path: &#39;/search&#39;,
  90. pageBuilder: (context, state) {
  91. final query = state.queryParameters[&#39;q&#39;] ?? &#39;&#39;;
  92. return FadeTransitionPage(
  93. child: SearchScreen(
  94. query: query,
  95. ),
  96. key: state.pageKey,
  97. );
  98. },
  99. ),
  100. ],
  101. ),
  102. ],
  103. ),
  104. ],
  105. );
  106. @override
  107. Widget build(BuildContext context) {
  108. return MaterialApp.router(
  109. title: &#39;Music app&#39;,
  110. theme: ThemeData(primarySwatch: Colors.pink),
  111. routerConfig: _router,
  112. builder: (context, child) {
  113. return MusicDatabaseScope(
  114. state: database,
  115. child: child!,
  116. );
  117. },
  118. );
  119. }
  120. }
  121. class LoginScreen extends StatelessWidget {
  122. const LoginScreen({super.key});
  123. @override
  124. Widget build(BuildContext context) {
  125. return Scaffold(
  126. appBar: AppBar(title: const Center(child: Text(&#39;Welcome!&#39;))),
  127. body: Center(
  128. child: Column(
  129. mainAxisAlignment: MainAxisAlignment.center,
  130. children: &lt;Widget&gt;[
  131. const FlutterLogo(
  132. size: 150,
  133. ),
  134. const SizedBox(
  135. height: 50,
  136. ),
  137. ElevatedButton(
  138. child: const Text(&#39;Login&#39;),
  139. onPressed: () {
  140. context.go(&#39;/library&#39;);
  141. },
  142. ),
  143. ],
  144. ),
  145. ),
  146. );
  147. }
  148. }
  149. class MusicAppShell extends StatelessWidget {
  150. final StatefulNavigationShell navigationShell;
  151. const MusicAppShell({
  152. Key? key,
  153. required this.navigationShell,
  154. }) : super(key: key ?? const ValueKey&lt;String&gt;(&#39;MusicAppShell&#39;));
  155. @override
  156. Widget build(BuildContext context) {
  157. return Scaffold(
  158. body: navigationShell,
  159. bottomNavigationBar: NavigationBar(
  160. destinations: const [
  161. NavigationDestination(
  162. icon: Icon(Icons.my_library_music_rounded),
  163. label: &#39;Library&#39;,
  164. ),
  165. NavigationDestination(
  166. icon: Icon(Icons.timelapse),
  167. label: &#39;Recently Played&#39;,
  168. ),
  169. NavigationDestination(
  170. icon: Icon(Icons.search),
  171. label: &#39;Search&#39;,
  172. ),
  173. ],
  174. selectedIndex: navigationShell.currentIndex,
  175. onDestinationSelected: (int index) {
  176. navigationShell.goBranch(index);
  177. },
  178. ),
  179. );
  180. }
  181. }
  182. class LibraryScreen extends StatelessWidget {
  183. const LibraryScreen({Key? key}) : super(key: key);
  184. @override
  185. Widget build(BuildContext context) {
  186. final database = MusicDatabase.of(context);
  187. return Scaffold(
  188. appBar: const CustomAppBar(title: &#39;Library&#39;),
  189. body: ListView.builder(
  190. itemBuilder: (context, albumId) {
  191. final album = database.albums[albumId];
  192. return AlbumTile(
  193. album: album,
  194. onTap: () {
  195. GoRouter.of(context).go(&#39;/library/album/$albumId&#39;);
  196. },
  197. );
  198. },
  199. itemCount: database.albums.length,
  200. ),
  201. );
  202. }
  203. }
  204. class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
  205. final String title;
  206. final double height;
  207. const CustomAppBar({
  208. super.key,
  209. required this.title,
  210. this.height = kToolbarHeight,
  211. });
  212. @override
  213. Size get preferredSize =&gt; Size.fromHeight(height);
  214. @override
  215. Widget build(BuildContext context) {
  216. return AppBar(
  217. title: Text(title),
  218. actions: &lt;Widget&gt;[
  219. PopupMenuButton(
  220. icon: const Icon(Icons.settings),
  221. itemBuilder: (context) {
  222. return [
  223. const PopupMenuItem&lt;int&gt;(
  224. value: 0,
  225. child: Text(&quot;Settings&quot;),
  226. ),
  227. const PopupMenuItem&lt;int&gt;(
  228. value: 1,
  229. child: Text(&quot;Logout&quot;),
  230. ),
  231. ];
  232. },
  233. onSelected: (value) {
  234. if (value == 1) {
  235. context.go(&#39;/login&#39;);
  236. }
  237. }),
  238. ],
  239. );
  240. }
  241. }
  242. class RecentlyPlayedScreen extends StatelessWidget {
  243. const RecentlyPlayedScreen({Key? key}) : super(key: key);
  244. @override
  245. Widget build(BuildContext context) {
  246. final database = MusicDatabase.of(context);
  247. final songs = database.recentlyPlayed;
  248. return Scaffold(
  249. appBar: const CustomAppBar(title: &#39;Recently Played&#39;),
  250. body: ListView.builder(
  251. itemBuilder: (context, index) {
  252. final song = songs[index];
  253. final albumIdInt = int.tryParse(song.albumId)!;
  254. final album = database.albums[albumIdInt];
  255. return SongTile(
  256. album: album,
  257. song: song,
  258. onTap: () {
  259. GoRouter.of(context).go(&#39;/recents/song/${song.fullId}&#39;);
  260. },
  261. );
  262. },
  263. itemCount: songs.length,
  264. ),
  265. );
  266. }
  267. }
  268. class SearchScreen extends StatefulWidget {
  269. final String query;
  270. const SearchScreen({Key? key, required this.query}) : super(key: key);
  271. @override
  272. State&lt;SearchScreen&gt; createState() =&gt; _SearchScreenState();
  273. }
  274. class _SearchScreenState extends State&lt;SearchScreen&gt; {
  275. String? _currentQuery;
  276. @override
  277. Widget build(BuildContext context) {
  278. final database = MusicDatabase.of(context);
  279. final songs = database.search(widget.query);
  280. return Scaffold(
  281. appBar: const CustomAppBar(title: &#39;Search&#39;),
  282. body: Column(
  283. children: [
  284. Padding(
  285. padding: const EdgeInsets.all(12.0),
  286. child: TextField(
  287. decoration: const InputDecoration(
  288. hintText: &#39;Search...&#39;,
  289. border: OutlineInputBorder(),
  290. ),
  291. onChanged: (String? newSearch) {
  292. _currentQuery = newSearch;
  293. },
  294. onEditingComplete: () {
  295. GoRouter.of(context).go(
  296. &#39;/search?q=$_currentQuery&#39;,
  297. );
  298. },
  299. ),
  300. ),
  301. Expanded(
  302. child: ListView.builder(
  303. itemBuilder: (context, index) {
  304. final song = songs[index];
  305. return SongTile(
  306. album: database.albums[int.tryParse(song.albumId)!],
  307. song: song,
  308. onTap: () {
  309. GoRouter.of(context).go(
  310. &#39;/library/album/${song.albumId}/song/${song.fullId}&#39;);
  311. },
  312. );
  313. },
  314. itemCount: songs.length,
  315. ),
  316. ),
  317. ],
  318. ),
  319. );
  320. }
  321. }
  322. class AlbumScreen extends StatelessWidget {
  323. final String? albumId;
  324. const AlbumScreen({
  325. required this.albumId,
  326. Key? key,
  327. }) : super(key: key);
  328. @override
  329. Widget build(BuildContext context) {
  330. final database = MusicDatabase.of(context);
  331. final albumIdInt = int.tryParse(albumId ?? &#39;&#39;);
  332. final album = database.albums[albumIdInt!];
  333. return Scaffold(
  334. appBar: CustomAppBar(title: &#39;Album - ${album.title}&#39;),
  335. body: Center(
  336. child: Column(
  337. children: [
  338. Row(
  339. children: [
  340. SizedBox(
  341. width: 200,
  342. height: 200,
  343. child: Container(
  344. color: album.color,
  345. margin: const EdgeInsets.all(8),
  346. ),
  347. ),
  348. Column(
  349. crossAxisAlignment: CrossAxisAlignment.start,
  350. children: [
  351. Text(
  352. album.title,
  353. style: Theme.of(context).textTheme.headlineMedium,
  354. ),
  355. Text(
  356. album.artist,
  357. style: Theme.of(context).textTheme.titleMedium,
  358. ),
  359. ],
  360. ),
  361. ],
  362. ),
  363. Expanded(
  364. child: ListView.builder(
  365. itemBuilder: (context, index) {
  366. final song = album.songs[index];
  367. return ListTile(
  368. title: Text(song.title),
  369. leading: SizedBox(
  370. width: 50,
  371. height: 50,
  372. child: Container(
  373. color: album.color,
  374. margin: const EdgeInsets.all(8),
  375. ),
  376. ),
  377. trailing: SongDuration(
  378. duration: song.duration,
  379. ),
  380. onTap: () {
  381. GoRouter.of(context)
  382. .go(&#39;/library/album/$albumId/song/${song.fullId}&#39;);
  383. },
  384. );
  385. },
  386. itemCount: album.songs.length,
  387. ),
  388. ),
  389. ],
  390. ),
  391. ),
  392. );
  393. }
  394. }
  395. class SongScreen extends StatelessWidget {
  396. final String songId;
  397. const SongScreen({
  398. Key? key,
  399. required this.songId,
  400. }) : super(key: key);
  401. @override
  402. Widget build(BuildContext context) {
  403. final database = MusicDatabase.of(context);
  404. final song = database.getSongById(songId);
  405. final albumIdInt = int.tryParse(song.albumId);
  406. final album = database.albums[albumIdInt!];
  407. return Scaffold(
  408. appBar: CustomAppBar(title: &#39;Song - ${song.title}&#39;),
  409. body: Column(
  410. children: [
  411. Row(
  412. children: [
  413. SizedBox(
  414. width: 300,
  415. height: 300,
  416. child: Container(
  417. color: album.color,
  418. margin: const EdgeInsets.all(8),
  419. ),
  420. ),
  421. Padding(
  422. padding: const EdgeInsets.all(16.0),
  423. child: Column(
  424. crossAxisAlignment: CrossAxisAlignment.start,
  425. children: [
  426. Text(
  427. song.title,
  428. style: Theme.of(context).textTheme.displayMedium,
  429. ),
  430. Text(
  431. album.title,
  432. style: Theme.of(context).textTheme.titleMedium,
  433. ),
  434. ],
  435. ),
  436. )
  437. ],
  438. )
  439. ],
  440. ),
  441. );
  442. }
  443. }
  444. class MusicDatabase {
  445. final List&lt;Album&gt; albums;
  446. final List&lt;Song&gt; recentlyPlayed;
  447. final Map&lt;String, Song&gt; _allSongs = {};
  448. MusicDatabase(this.albums, this.recentlyPlayed) {
  449. _populateAllSongs();
  450. }
  451. factory MusicDatabase.mock() {
  452. final albums = _mockAlbums().toList();
  453. final recentlyPlayed = _mockRecentlyPlayed(albums).toList();
  454. return MusicDatabase(albums, recentlyPlayed);
  455. }
  456. Song getSongById(String songId) {
  457. if (_allSongs.containsKey(songId)) {
  458. return _allSongs[songId]!;
  459. }
  460. throw (&#39;No song with ID $songId found.&#39;);
  461. }
  462. List&lt;Song&gt; search(String searchString) {
  463. final songs = &lt;Song&gt;[];
  464. for (var song in _allSongs.values) {
  465. final album = albums[int.tryParse(song.albumId)!];
  466. if (song.title.contains(searchString) ||
  467. album.title.contains(searchString)) {
  468. songs.add(song);
  469. }
  470. }
  471. return songs;
  472. }
  473. void _populateAllSongs() {
  474. for (var album in albums) {
  475. for (var song in album.songs) {
  476. _allSongs[song.fullId] = song;
  477. }
  478. }
  479. }
  480. static MusicDatabase of(BuildContext context) {
  481. final routeStateScope =
  482. context.dependOnInheritedWidgetOfExactType&lt;MusicDatabaseScope&gt;();
  483. if (routeStateScope == null) throw (&#39;No RouteState in scope!&#39;);
  484. return routeStateScope.state;
  485. }
  486. static Iterable&lt;Album&gt; _mockAlbums() sync* {
  487. for (var i = 0; i &lt; Colors.primaries.length; i++) {
  488. final color = Colors.primaries[i];
  489. final title = WordPair.random().toString();
  490. final artist = WordPair.random().toString();
  491. final songs = &lt;Song&gt;[];
  492. for (var j = 0; j &lt; 12; j++) {
  493. final minutes = math.Random().nextInt(3) + 3;
  494. final seconds = math.Random().nextInt(60);
  495. final title = WordPair.random();
  496. final duration = Duration(minutes: minutes, seconds: seconds);
  497. final song = Song(&#39;$j&#39;, &#39;$i&#39;, &#39;$title&#39;, duration);
  498. songs.add(song);
  499. }
  500. yield Album(&#39;$i&#39;, title, artist, color, songs);
  501. }
  502. }
  503. static Iterable&lt;Song&gt; _mockRecentlyPlayed(List&lt;Album&gt; albums) sync* {
  504. for (var album in albums) {
  505. final songIndex = math.Random().nextInt(album.songs.length);
  506. yield album.songs[songIndex];
  507. }
  508. }
  509. }
  510. class MusicDatabaseScope extends InheritedWidget {
  511. final MusicDatabase state;
  512. const MusicDatabaseScope({
  513. required this.state,
  514. required Widget child,
  515. Key? key,
  516. }) : super(child: child, key: key);
  517. @override
  518. bool updateShouldNotify(covariant InheritedWidget oldWidget) {
  519. return oldWidget is MusicDatabaseScope &amp;&amp; state != oldWidget.state;
  520. }
  521. }
  522. class Album {
  523. final String id;
  524. final String title;
  525. final String artist;
  526. final Color color;
  527. final List&lt;Song&gt; songs;
  528. Album(this.id, this.title, this.artist, this.color, this.songs);
  529. }
  530. class Song {
  531. final String id;
  532. final String albumId;
  533. final String title;
  534. final Duration duration;
  535. Song(this.id, this.albumId, this.title, this.duration);
  536. String get fullId =&gt; &#39;$albumId-$id&#39;;
  537. }
  538. class AlbumTile extends StatelessWidget {
  539. final Album album;
  540. final VoidCallback? onTap;
  541. const AlbumTile({Key? key, required this.album, this.onTap})
  542. : super(key: key);
  543. @override
  544. Widget build(BuildContext context) {
  545. return ListTile(
  546. leading: SizedBox(
  547. width: 50,
  548. height: 50,
  549. child: Container(
  550. color: album.color,
  551. ),
  552. ),
  553. title: Text(album.title),
  554. subtitle: Text(album.artist),
  555. onTap: onTap,
  556. );
  557. }
  558. }
  559. class SongTile extends StatelessWidget {
  560. final Album album;
  561. final Song song;
  562. final VoidCallback? onTap;
  563. const SongTile(
  564. {Key? key, required this.album, required this.song, this.onTap})
  565. : super(key: key);
  566. @override
  567. Widget build(BuildContext context) {
  568. return ListTile(
  569. leading: SizedBox(
  570. width: 50,
  571. height: 50,
  572. child: Container(
  573. color: album.color,
  574. margin: const EdgeInsets.all(8),
  575. ),
  576. ),
  577. title: Text(song.title),
  578. trailing: SongDuration(
  579. duration: song.duration,
  580. ),
  581. onTap: onTap,
  582. );
  583. }
  584. }
  585. class SongDuration extends StatelessWidget {
  586. final Duration duration;
  587. const SongDuration({
  588. required this.duration,
  589. Key? key,
  590. }) : super(key: key);
  591. @override
  592. Widget build(BuildContext context) {
  593. return Text(
  594. &#39;${duration.inMinutes.toString().padLeft(2, &#39;0&#39;)}:${(duration.inSeconds % 60).toString().padLeft(2, &#39;0&#39;)}&#39;);
  595. }
  596. }
  597. /// A page that fades in an out.
  598. class FadeTransitionPage extends CustomTransitionPage&lt;void&gt; {
  599. /// Creates a [FadeTransitionPage].
  600. FadeTransitionPage({
  601. required LocalKey key,
  602. required Widget child,
  603. }) : super(
  604. key: key,
  605. transitionsBuilder: (BuildContext context,
  606. Animation&lt;double&gt; animation,
  607. Animation&lt;double&gt; secondaryAnimation,
  608. Widget child) =&gt;
  609. FadeTransition(
  610. opacity: animation.drive(_curveTween),
  611. child: child,
  612. ),
  613. child: child);
  614. static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
  615. }

huangapple
  • 本文由 发表于 2023年5月29日 19:55:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/76357147.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定