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

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

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:

确定