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

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

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

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:my_app/feed.dart';
import 'package:my_app/feed_people.dart';
import 'package:my_app/route_names.dart';

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routerConfig: router,
    );
  }

  final router = GoRouter(
    initialLocation: '/',
    navigatorKey: _rootNavigatorKey,
    routes: [
      ShellRoute(
        navigatorKey: _shellNavigatorKey,
        pageBuilder: (context, state, child) {
          print(state.location);
          return NoTransitionPage(
              child: ScaffoldWithNavBar(
            location: state.location,
            child: child,
          ));
        },
        routes: [
          GoRoute(
              path: '/',
              parentNavigatorKey: _shellNavigatorKey,
              pageBuilder: (context, state) {
                return const NoTransitionPage(
                  child: Feed(uuid: 'a78987-hiIh!ç767897'),
                );
              },
              routes: [
                GoRoute(
                    name: RoutesNames.feedPeople,
                    path: 'profil/:uuid',
                    builder: (context, state) =>
                        FeedPeople(uuid: state.pathParameters['uuid']!))
              ]),
          GoRoute(
            path: '/discover',
            parentNavigatorKey: _shellNavigatorKey,
            pageBuilder: (context, state) {
              return const NoTransitionPage(
                child: Scaffold(
                  body: Center(child: Text("Discover")),
                ),
              );
            },
          ),
          GoRoute(
              parentNavigatorKey: _shellNavigatorKey,
              path: '/shop',
              pageBuilder: (context, state) {
                return const NoTransitionPage(
                  child: Scaffold(
                    body: Center(child: Text("Shop")),
                  ),
                );
              }),
        ],
      ),
      GoRoute(
        parentNavigatorKey: _rootNavigatorKey,
        path: '/login',
        pageBuilder: (context, state) {
          return NoTransitionPage(
            key: UniqueKey(),
            child: Scaffold(
              appBar: AppBar(),
              body: const Center(
                child: Text("Login"),
              ),
            ),
          );
        },
      ),
    ],
  );
}

class ScaffoldWithNavBar extends StatefulWidget {
  String location;
  ScaffoldWithNavBar({super.key, required this.child, required this.location});

  final Widget child;

  @override
  State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
}

class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
  int _currentIndex = 0;

  static const List<MyCustomBottomNavBarItem> tabs = [
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.home),
      activeIcon: Icon(Icons.home),
      label: 'HOME',
      initialLocation: '/',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.explore_outlined),
      activeIcon: Icon(Icons.explore),
      label: 'DISCOVER',
      initialLocation: '/discover',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.storefront_outlined),
      activeIcon: Icon(Icons.storefront),
      label: 'SHOP',
      initialLocation: '/shop',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.account_circle_outlined),
      activeIcon: Icon(Icons.account_circle),
      label: 'MY',
      initialLocation: '/login',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    const labelStyle = TextStyle(fontFamily: 'Roboto');
    return Scaffold(
      body: SafeArea(child: widget.child),
      bottomNavigationBar: BottomNavigationBar(
        selectedLabelStyle: labelStyle,
        unselectedLabelStyle: labelStyle,
        selectedItemColor: const Color(0xFF434343),
        selectedFontSize: 12,
        unselectedItemColor: const Color(0xFF838383),
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
        onTap: (int index) {
          _goOtherTab(context, index);
        },
        currentIndex: widget.location == '/'
            ? 0
            : widget.location == '/discover'
                ? 1
                : widget.location == '/shop'
                    ? 2
                    : 3,
        items: tabs,
      ),
    );
  }

  void _goOtherTab(BuildContext context, int index) {
    if (index == _currentIndex) return;
    GoRouter router = GoRouter.of(context);
    String location = tabs[index].initialLocation;

    setState(() {
      _currentIndex = index;
    });
    if (index == 3) {
      context.push('/login');
    } else {
      router.go(location);
    }
  }
}

class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
  final String initialLocation;

  const MyCustomBottomNavBarItem(
      {required this.initialLocation,
      required Widget icon,
      String? label,
      Widget? activeIcon})
      : super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
}

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

import &#39;package:flutter/material.dart&#39;;
import &#39;package:go_router/go_router.dart&#39;;
import &#39;package:my_app/feed.dart&#39;;
import &#39;package:my_app/feed_people.dart&#39;;
import &#39;package:my_app/route_names.dart&#39;;

final _rootNavigatorKey = GlobalKey&lt;NavigatorState&gt;();
final _shellNavigatorKey = GlobalKey&lt;NavigatorState&gt;();

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: &#39;Flutter Demo&#39;,
      routerConfig: router,
    );
  }

  final router = GoRouter(
    initialLocation: &#39;/&#39;,
    navigatorKey: _rootNavigatorKey,
    routes: [
      ShellRoute(
        navigatorKey: _shellNavigatorKey,
        pageBuilder: (context, state, child) {
          print(state.location);
          return NoTransitionPage(
              child: ScaffoldWithNavBar(
            location: state.location,
            child: child,
          ));
        },
        routes: [
          GoRoute(
              path: &#39;/&#39;,
              parentNavigatorKey: _shellNavigatorKey,
              pageBuilder: (context, state) {
                return const NoTransitionPage(
                  child: Feed(uuid: &#39;a78987-hiIh!&#231;767897&#39;),
                );
              },
              routes: [
                GoRoute(
                    name: RoutesNames.feedPeople,
                    path: &#39;profil/:uuid&#39;,
                    builder: (context, state) =&gt;
                        FeedPeople(uuid: state.pathParameters[&#39;uuid&#39;]!))
              ]),
          GoRoute(
            path: &#39;/discover&#39;,
            parentNavigatorKey: _shellNavigatorKey,
            pageBuilder: (context, state) {
              return const NoTransitionPage(
                child: Scaffold(
                  body: Center(child: Text(&quot;Discover&quot;)),
                ),
              );
            },
          ),
          GoRoute(
              parentNavigatorKey: _shellNavigatorKey,
              path: &#39;/shop&#39;,
              pageBuilder: (context, state) {
                return const NoTransitionPage(
                  child: Scaffold(
                    body: Center(child: Text(&quot;Shop&quot;)),
                  ),
                );
              }),
        ],
      ),
      GoRoute(
        parentNavigatorKey: _rootNavigatorKey,
        path: &#39;/login&#39;,
        pageBuilder: (context, state) {
          return NoTransitionPage(
            key: UniqueKey(),
            child: Scaffold(
              appBar: AppBar(),
              body: const Center(
                child: Text(&quot;Login&quot;),
              ),
            ),
          );
        },
      ),
    ],
  );
}

// ignore: must_be_immutable
class ScaffoldWithNavBar extends StatefulWidget {
  String location;
  ScaffoldWithNavBar({super.key, required this.child, required this.location});

  final Widget child;

  @override
  State&lt;ScaffoldWithNavBar&gt; createState() =&gt; _ScaffoldWithNavBarState();
}

class _ScaffoldWithNavBarState extends State&lt;ScaffoldWithNavBar&gt; {
  int _currentIndex = 0;

  static const List&lt;MyCustomBottomNavBarItem&gt; tabs = [
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.home),
      activeIcon: Icon(Icons.home),
      label: &#39;HOME&#39;,
      initialLocation: &#39;/&#39;,
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.explore_outlined),
      activeIcon: Icon(Icons.explore),
      label: &#39;DISCOVER&#39;,
      initialLocation: &#39;/discover&#39;,
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.storefront_outlined),
      activeIcon: Icon(Icons.storefront),
      label: &#39;SHOP&#39;,
      initialLocation: &#39;/shop&#39;,
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.account_circle_outlined),
      activeIcon: Icon(Icons.account_circle),
      label: &#39;MY&#39;,
      initialLocation: &#39;/login&#39;,
    ),
  ];

  @override
  Widget build(BuildContext context) {
    const labelStyle = TextStyle(fontFamily: &#39;Roboto&#39;);
    return Scaffold(
      body: SafeArea(child: widget.child),
      bottomNavigationBar: BottomNavigationBar(
        selectedLabelStyle: labelStyle,
        unselectedLabelStyle: labelStyle,
        selectedItemColor: const Color(0xFF434343),
        selectedFontSize: 12,
        unselectedItemColor: const Color(0xFF838383),
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
        onTap: (int index) {
          _goOtherTab(context, index);
        },
        currentIndex: widget.location == &#39;/&#39;
            ? 0
            : widget.location == &#39;/discover&#39;
                ? 1
                : widget.location == &#39;/shop&#39;
                    ? 2
                    : 3,
        items: tabs,
      ),
    );
  }

  void _goOtherTab(BuildContext context, int index) {
    if (index == _currentIndex) return;
    GoRouter router = GoRouter.of(context);
    String location = tabs[index].initialLocation;

    setState(() {
      _currentIndex = index;
    });
    if (index == 3) {
      context.push(&#39;/login&#39;);
    } else {
      router.go(location);
    }
  }
}

class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
  final String initialLocation;

  const MyCustomBottomNavBarItem(
      {required this.initialLocation,
      required Widget icon,
      String? label,
      Widget? activeIcon})
      : super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
}

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中使用以下代码进行测试此处

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:english_words/english_words.dart';
import 'dart:math' as math;

void main() {
  runApp(MusicAppDemo());
}

class MusicAppDemo extends StatelessWidget {
  MusicAppDemo({Key? key}) : super(key: key);

  final MusicDatabase database = MusicDatabase.mock();

  final GoRouter _router = GoRouter(
    initialLocation: '/login',
    routes: <RouteBase>[
      GoRoute(
        path: '/login',
        builder: (BuildContext context, GoRouterState state) {
          return const LoginScreen();
        },
      ),
      StatefulShellRoute.indexedStack(
        builder: (BuildContext context, GoRouterState state,
            StatefulNavigationShell navigationShell) {
          return MusicAppShell(
            navigationShell: navigationShell,
          );
        },
        branches: <StatefulShellBranch>[
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/library',
                pageBuilder: (context, state) {
                  return FadeTransitionPage(
                    child: const LibraryScreen(),
                    key: state.pageKey,
                  );
                },
                routes: <RouteBase>[
                  GoRoute(
                    path: 'album/:albumId',
                    builder: (BuildContext context, GoRouterState state) {
                      return AlbumScreen(
                        albumId: state.pathParameters['albumId'],
                      );
                    },
                    routes: [
                      GoRoute(
                        path: 'song/:songId',
                        // Display on the root Navigator
                        builder: (BuildContext context, GoRouterState state) {
                          return SongScreen(
                            songId: state.pathParameters['songId']!,
                          );
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/recents',
                pageBuilder: (context, state) {
                  return FadeTransitionPage(
                    child: const RecentlyPlayedScreen(),
                    key: state.pageKey,
                  );
                },
                routes: <RouteBase>[
                  GoRoute(
                    path: 'song/:songId',
                    // Display on the root Navigator
                    builder: (BuildContext context, GoRouterState state) {
                      return SongScreen(
                        songId: state.pathParameters['songId']!,
                      );
                    },
                  ),
                ],
              ),
            ],
          ),
          StatefulShellBranch(
            routes: <RouteBase>[
              GoRoute(
                path: '/search',
                pageBuilder: (context, state) {
                  final query = state.queryParameters['q'] ?? '';
                  return FadeTransitionPage(
                    child: SearchScreen(
                      query: query,
                    ),
                    key: state.pageKey,
                  );
                },
              ),
            ],
          ),
        ],
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Music app',
      theme: ThemeData(primarySwatch: Colors.pink),
      routerConfig: _router,
      builder: (context, child) {
        return MusicDatabaseScope(
          state: database,
          child: child!,
        );
      },
    );
  }
}

// ...(以下为其他类和部分代码,请参考原始内容)
英文:

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

import &#39;package:flutter/material.dart&#39;;
import &#39;package:go_router/go_router.dart&#39;;
import &#39;package:english_words/english_words.dart&#39;;
import &#39;dart:math&#39; as math;

void main() {
  runApp(MusicAppDemo());
}

class MusicAppDemo extends StatelessWidget {
  MusicAppDemo({Key? key}) : super(key: key);

  final MusicDatabase database = MusicDatabase.mock();

  final GoRouter _router = GoRouter(
    initialLocation: &#39;/login&#39;,
    routes: &lt;RouteBase&gt;[
      GoRoute(
        path: &#39;/login&#39;,
        builder: (BuildContext context, GoRouterState state) {
          return const LoginScreen();
        },
      ),
      StatefulShellRoute.indexedStack(
        builder: (BuildContext context, GoRouterState state,
            StatefulNavigationShell navigationShell) {
          return MusicAppShell(
            navigationShell: navigationShell,
          );
        },
        branches: &lt;StatefulShellBranch&gt;[
          StatefulShellBranch(
            routes: &lt;RouteBase&gt;[
              GoRoute(
                path: &#39;/library&#39;,
                pageBuilder: (context, state) {
                  return FadeTransitionPage(
                    child: const LibraryScreen(),
                    key: state.pageKey,
                  );
                },
                routes: &lt;RouteBase&gt;[
                  GoRoute(
                    path: &#39;album/:albumId&#39;,
                    builder: (BuildContext context, GoRouterState state) {
                      return AlbumScreen(
                        albumId: state.pathParameters[&#39;albumId&#39;],
                      );
                    },
                    routes: [
                      GoRoute(
                        path: &#39;song/:songId&#39;,
                        // Display on the root Navigator
                        builder: (BuildContext context, GoRouterState state) {
                          return SongScreen(
                            songId: state.pathParameters[&#39;songId&#39;]!,
                          );
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
          StatefulShellBranch(
            routes: &lt;RouteBase&gt;[
              GoRoute(
                path: &#39;/recents&#39;,
                pageBuilder: (context, state) {
                  return FadeTransitionPage(
                    child: const RecentlyPlayedScreen(),
                    key: state.pageKey,
                  );
                },
                routes: &lt;RouteBase&gt;[
                  GoRoute(
                    path: &#39;song/:songId&#39;,
                    // Display on the root Navigator
                    builder: (BuildContext context, GoRouterState state) {
                      return SongScreen(
                        songId: state.pathParameters[&#39;songId&#39;]!,
                      );
                    },
                  ),
                ],
              ),
            ],
          ),
          StatefulShellBranch(
            routes: &lt;RouteBase&gt;[
              GoRoute(
                path: &#39;/search&#39;,
                pageBuilder: (context, state) {
                  final query = state.queryParameters[&#39;q&#39;] ?? &#39;&#39;;
                  return FadeTransitionPage(
                    child: SearchScreen(
                      query: query,
                    ),
                    key: state.pageKey,
                  );
                },
              ),
            ],
          ),
        ],
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: &#39;Music app&#39;,
      theme: ThemeData(primarySwatch: Colors.pink),
      routerConfig: _router,
      builder: (context, child) {
        return MusicDatabaseScope(
          state: database,
          child: child!,
        );
      },
    );
  }
}

class LoginScreen extends StatelessWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Center(child: Text(&#39;Welcome!&#39;))),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            const FlutterLogo(
              size: 150,
            ),
            const SizedBox(
              height: 50,
            ),
            ElevatedButton(
              child: const Text(&#39;Login&#39;),
              onPressed: () {
                context.go(&#39;/library&#39;);
              },
            ),
          ],
        ),
      ),
    );
  }
}

class MusicAppShell extends StatelessWidget {
  final StatefulNavigationShell navigationShell;

  const MusicAppShell({
    Key? key,
    required this.navigationShell,
  }) : super(key: key ?? const ValueKey&lt;String&gt;(&#39;MusicAppShell&#39;));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: NavigationBar(
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.my_library_music_rounded),
            label: &#39;Library&#39;,
          ),
          NavigationDestination(
            icon: Icon(Icons.timelapse),
            label: &#39;Recently Played&#39;,
          ),
          NavigationDestination(
            icon: Icon(Icons.search),
            label: &#39;Search&#39;,
          ),
        ],
        selectedIndex: navigationShell.currentIndex,
        onDestinationSelected: (int index) {
          navigationShell.goBranch(index);
        },
      ),
    );
  }
}

class LibraryScreen extends StatelessWidget {
  const LibraryScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    return Scaffold(
      appBar: const CustomAppBar(title: &#39;Library&#39;),
      body: ListView.builder(
        itemBuilder: (context, albumId) {
          final album = database.albums[albumId];
          return AlbumTile(
            album: album,
            onTap: () {
              GoRouter.of(context).go(&#39;/library/album/$albumId&#39;);
            },
          );
        },
        itemCount: database.albums.length,
      ),
    );
  }
}

class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String title;
  final double height;

  const CustomAppBar({
    super.key,
    required this.title,
    this.height = kToolbarHeight,
  });

  @override
  Size get preferredSize =&gt; Size.fromHeight(height);

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text(title),
      actions: &lt;Widget&gt;[
        PopupMenuButton(
            icon: const Icon(Icons.settings),
            itemBuilder: (context) {
              return [
                const PopupMenuItem&lt;int&gt;(
                  value: 0,
                  child: Text(&quot;Settings&quot;),
                ),
                const PopupMenuItem&lt;int&gt;(
                  value: 1,
                  child: Text(&quot;Logout&quot;),
                ),
              ];
            },
            onSelected: (value) {
              if (value == 1) {
                context.go(&#39;/login&#39;);
              }
            }),
      ],
    );
  }
}

class RecentlyPlayedScreen extends StatelessWidget {
  const RecentlyPlayedScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final songs = database.recentlyPlayed;
    return Scaffold(
      appBar: const CustomAppBar(title: &#39;Recently Played&#39;),
      body: ListView.builder(
        itemBuilder: (context, index) {
          final song = songs[index];
          final albumIdInt = int.tryParse(song.albumId)!;
          final album = database.albums[albumIdInt];
          return SongTile(
            album: album,
            song: song,
            onTap: () {
              GoRouter.of(context).go(&#39;/recents/song/${song.fullId}&#39;);
            },
          );
        },
        itemCount: songs.length,
      ),
    );
  }
}

class SearchScreen extends StatefulWidget {
  final String query;

  const SearchScreen({Key? key, required this.query}) : super(key: key);

  @override
  State&lt;SearchScreen&gt; createState() =&gt; _SearchScreenState();
}

class _SearchScreenState extends State&lt;SearchScreen&gt; {
  String? _currentQuery;

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final songs = database.search(widget.query);
    return Scaffold(
      appBar: const CustomAppBar(title: &#39;Search&#39;),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12.0),
            child: TextField(
              decoration: const InputDecoration(
                hintText: &#39;Search...&#39;,
                border: OutlineInputBorder(),
              ),
              onChanged: (String? newSearch) {
                _currentQuery = newSearch;
              },
              onEditingComplete: () {
                GoRouter.of(context).go(
                  &#39;/search?q=$_currentQuery&#39;,
                );
              },
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemBuilder: (context, index) {
                final song = songs[index];
                return SongTile(
                  album: database.albums[int.tryParse(song.albumId)!],
                  song: song,
                  onTap: () {
                    GoRouter.of(context).go(
                        &#39;/library/album/${song.albumId}/song/${song.fullId}&#39;);
                  },
                );
              },
              itemCount: songs.length,
            ),
          ),
        ],
      ),
    );
  }
}

class AlbumScreen extends StatelessWidget {
  final String? albumId;

  const AlbumScreen({
    required this.albumId,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final albumIdInt = int.tryParse(albumId ?? &#39;&#39;);
    final album = database.albums[albumIdInt!];
    return Scaffold(
      appBar: CustomAppBar(title: &#39;Album - ${album.title}&#39;),
      body: Center(
        child: Column(
          children: [
            Row(
              children: [
                SizedBox(
                  width: 200,
                  height: 200,
                  child: Container(
                    color: album.color,
                    margin: const EdgeInsets.all(8),
                  ),
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      album.title,
                      style: Theme.of(context).textTheme.headlineMedium,
                    ),
                    Text(
                      album.artist,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                  ],
                ),
              ],
            ),
            Expanded(
              child: ListView.builder(
                itemBuilder: (context, index) {
                  final song = album.songs[index];
                  return ListTile(
                    title: Text(song.title),
                    leading: SizedBox(
                      width: 50,
                      height: 50,
                      child: Container(
                        color: album.color,
                        margin: const EdgeInsets.all(8),
                      ),
                    ),
                    trailing: SongDuration(
                      duration: song.duration,
                    ),
                    onTap: () {
                      GoRouter.of(context)
                          .go(&#39;/library/album/$albumId/song/${song.fullId}&#39;);
                    },
                  );
                },
                itemCount: album.songs.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SongScreen extends StatelessWidget {
  final String songId;

  const SongScreen({
    Key? key,
    required this.songId,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final database = MusicDatabase.of(context);
    final song = database.getSongById(songId);
    final albumIdInt = int.tryParse(song.albumId);
    final album = database.albums[albumIdInt!];

    return Scaffold(
      appBar: CustomAppBar(title: &#39;Song - ${song.title}&#39;),
      body: Column(
        children: [
          Row(
            children: [
              SizedBox(
                width: 300,
                height: 300,
                child: Container(
                  color: album.color,
                  margin: const EdgeInsets.all(8),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      song.title,
                      style: Theme.of(context).textTheme.displayMedium,
                    ),
                    Text(
                      album.title,
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                  ],
                ),
              )
            ],
          )
        ],
      ),
    );
  }
}

class MusicDatabase {
  final List&lt;Album&gt; albums;
  final List&lt;Song&gt; recentlyPlayed;
  final Map&lt;String, Song&gt; _allSongs = {};

  MusicDatabase(this.albums, this.recentlyPlayed) {
    _populateAllSongs();
  }

  factory MusicDatabase.mock() {
    final albums = _mockAlbums().toList();
    final recentlyPlayed = _mockRecentlyPlayed(albums).toList();
    return MusicDatabase(albums, recentlyPlayed);
  }

  Song getSongById(String songId) {
    if (_allSongs.containsKey(songId)) {
      return _allSongs[songId]!;
    }
    throw (&#39;No song with ID $songId found.&#39;);
  }

  List&lt;Song&gt; search(String searchString) {
    final songs = &lt;Song&gt;[];
    for (var song in _allSongs.values) {
      final album = albums[int.tryParse(song.albumId)!];
      if (song.title.contains(searchString) ||
          album.title.contains(searchString)) {
        songs.add(song);
      }
    }
    return songs;
  }

  void _populateAllSongs() {
    for (var album in albums) {
      for (var song in album.songs) {
        _allSongs[song.fullId] = song;
      }
    }
  }

  static MusicDatabase of(BuildContext context) {
    final routeStateScope =
        context.dependOnInheritedWidgetOfExactType&lt;MusicDatabaseScope&gt;();
    if (routeStateScope == null) throw (&#39;No RouteState in scope!&#39;);
    return routeStateScope.state;
  }

  static Iterable&lt;Album&gt; _mockAlbums() sync* {
    for (var i = 0; i &lt; Colors.primaries.length; i++) {
      final color = Colors.primaries[i];
      final title = WordPair.random().toString();
      final artist = WordPair.random().toString();
      final songs = &lt;Song&gt;[];
      for (var j = 0; j &lt; 12; j++) {
        final minutes = math.Random().nextInt(3) + 3;
        final seconds = math.Random().nextInt(60);
        final title = WordPair.random();
        final duration = Duration(minutes: minutes, seconds: seconds);
        final song = Song(&#39;$j&#39;, &#39;$i&#39;, &#39;$title&#39;, duration);

        songs.add(song);
      }
      yield Album(&#39;$i&#39;, title, artist, color, songs);
    }
  }

  static Iterable&lt;Song&gt; _mockRecentlyPlayed(List&lt;Album&gt; albums) sync* {
    for (var album in albums) {
      final songIndex = math.Random().nextInt(album.songs.length);
      yield album.songs[songIndex];
    }
  }
}

class MusicDatabaseScope extends InheritedWidget {
  final MusicDatabase state;

  const MusicDatabaseScope({
    required this.state,
    required Widget child,
    Key? key,
  }) : super(child: child, key: key);

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return oldWidget is MusicDatabaseScope &amp;&amp; state != oldWidget.state;
  }
}

class Album {
  final String id;
  final String title;
  final String artist;
  final Color color;
  final List&lt;Song&gt; songs;

  Album(this.id, this.title, this.artist, this.color, this.songs);
}

class Song {
  final String id;
  final String albumId;
  final String title;
  final Duration duration;

  Song(this.id, this.albumId, this.title, this.duration);

  String get fullId =&gt; &#39;$albumId-$id&#39;;
}

class AlbumTile extends StatelessWidget {
  final Album album;
  final VoidCallback? onTap;

  const AlbumTile({Key? key, required this.album, this.onTap})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: SizedBox(
        width: 50,
        height: 50,
        child: Container(
          color: album.color,
        ),
      ),
      title: Text(album.title),
      subtitle: Text(album.artist),
      onTap: onTap,
    );
  }
}

class SongTile extends StatelessWidget {
  final Album album;
  final Song song;
  final VoidCallback? onTap;

  const SongTile(
      {Key? key, required this.album, required this.song, this.onTap})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: SizedBox(
        width: 50,
        height: 50,
        child: Container(
          color: album.color,
          margin: const EdgeInsets.all(8),
        ),
      ),
      title: Text(song.title),
      trailing: SongDuration(
        duration: song.duration,
      ),
      onTap: onTap,
    );
  }
}

class SongDuration extends StatelessWidget {
  final Duration duration;

  const SongDuration({
    required this.duration,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
        &#39;${duration.inMinutes.toString().padLeft(2, &#39;0&#39;)}:${(duration.inSeconds % 60).toString().padLeft(2, &#39;0&#39;)}&#39;);
  }
}

/// A page that fades in an out.
class FadeTransitionPage extends CustomTransitionPage&lt;void&gt; {
  /// Creates a [FadeTransitionPage].
  FadeTransitionPage({
    required LocalKey key,
    required Widget child,
  }) : super(
            key: key,
            transitionsBuilder: (BuildContext context,
                    Animation&lt;double&gt; animation,
                    Animation&lt;double&gt; secondaryAnimation,
                    Widget child) =&gt;
                FadeTransition(
                  opacity: animation.drive(_curveTween),
                  child: child,
                ),
            child: child);

  static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
}

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:

确定