英文:
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 '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});
// This widget is the root of your application.
@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"),
),
),
);
},
),
],
);
}
// ignore: must_be_immutable
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);
}
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 '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!,
);
},
);
}
}
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Center(child: Text('Welcome!'))),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const FlutterLogo(
size: 150,
),
const SizedBox(
height: 50,
),
ElevatedButton(
child: const Text('Login'),
onPressed: () {
context.go('/library');
},
),
],
),
),
);
}
}
class MusicAppShell extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const MusicAppShell({
Key? key,
required this.navigationShell,
}) : super(key: key ?? const ValueKey<String>('MusicAppShell'));
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
destinations: const [
NavigationDestination(
icon: Icon(Icons.my_library_music_rounded),
label: 'Library',
),
NavigationDestination(
icon: Icon(Icons.timelapse),
label: 'Recently Played',
),
NavigationDestination(
icon: Icon(Icons.search),
label: 'Search',
),
],
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: 'Library'),
body: ListView.builder(
itemBuilder: (context, albumId) {
final album = database.albums[albumId];
return AlbumTile(
album: album,
onTap: () {
GoRouter.of(context).go('/library/album/$albumId');
},
);
},
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 => Size.fromHeight(height);
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(title),
actions: <Widget>[
PopupMenuButton(
icon: const Icon(Icons.settings),
itemBuilder: (context) {
return [
const PopupMenuItem<int>(
value: 0,
child: Text("Settings"),
),
const PopupMenuItem<int>(
value: 1,
child: Text("Logout"),
),
];
},
onSelected: (value) {
if (value == 1) {
context.go('/login');
}
}),
],
);
}
}
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: 'Recently Played'),
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('/recents/song/${song.fullId}');
},
);
},
itemCount: songs.length,
),
);
}
}
class SearchScreen extends StatefulWidget {
final String query;
const SearchScreen({Key? key, required this.query}) : super(key: key);
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
String? _currentQuery;
@override
Widget build(BuildContext context) {
final database = MusicDatabase.of(context);
final songs = database.search(widget.query);
return Scaffold(
appBar: const CustomAppBar(title: 'Search'),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
decoration: const InputDecoration(
hintText: 'Search...',
border: OutlineInputBorder(),
),
onChanged: (String? newSearch) {
_currentQuery = newSearch;
},
onEditingComplete: () {
GoRouter.of(context).go(
'/search?q=$_currentQuery',
);
},
),
),
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(
'/library/album/${song.albumId}/song/${song.fullId}');
},
);
},
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 ?? '');
final album = database.albums[albumIdInt!];
return Scaffold(
appBar: CustomAppBar(title: 'Album - ${album.title}'),
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('/library/album/$albumId/song/${song.fullId}');
},
);
},
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: 'Song - ${song.title}'),
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<Album> albums;
final List<Song> recentlyPlayed;
final Map<String, Song> _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 ('No song with ID $songId found.');
}
List<Song> search(String searchString) {
final songs = <Song>[];
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<MusicDatabaseScope>();
if (routeStateScope == null) throw ('No RouteState in scope!');
return routeStateScope.state;
}
static Iterable<Album> _mockAlbums() sync* {
for (var i = 0; i < Colors.primaries.length; i++) {
final color = Colors.primaries[i];
final title = WordPair.random().toString();
final artist = WordPair.random().toString();
final songs = <Song>[];
for (var j = 0; j < 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('$j', '$i', '$title', duration);
songs.add(song);
}
yield Album('$i', title, artist, color, songs);
}
}
static Iterable<Song> _mockRecentlyPlayed(List<Album> 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 && state != oldWidget.state;
}
}
class Album {
final String id;
final String title;
final String artist;
final Color color;
final List<Song> 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 => '$albumId-$id';
}
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(
'${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}');
}
}
/// A page that fades in an out.
class FadeTransitionPage extends CustomTransitionPage<void> {
/// Creates a [FadeTransitionPage].
FadeTransitionPage({
required LocalKey key,
required Widget child,
}) : super(
key: key,
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) =>
FadeTransition(
opacity: animation.drive(_curveTween),
child: child,
),
child: child);
static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论