Created with <3 with dartpad.dev.
Last active
September 21, 2022 19:36
-
-
Save johnpryan/b44f0bb13ac8148407e2715763fb6a3a to your computer and use it in GitHub Desktop.
go_router 5 sample
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Copyright 2013 The Flutter Authors. All rights reserved. | |
Redistribution and use in source and binary forms, with or without modification, | |
are permitted provided that the following conditions are met: | |
* Redistributions of source code must retain the above copyright | |
notice, this list of conditions and the following disclaimer. | |
* Redistributions in binary form must reproduce the above | |
copyright notice, this list of conditions and the following | |
disclaimer in the documentation and/or other materials provided | |
with the distribution. | |
* Neither the name of Google Inc. nor the names of its | |
contributors may be used to endorse or promote products derived | |
from this software without specific prior written permission. | |
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | |
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2013 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
import 'dart:math' as math; | |
import 'package:flutter/material.dart'; | |
import 'package:go_router/go_router.dart'; | |
import 'package:english_words/english_words.dart'; | |
void main() { | |
runApp(MusicAppDemo()); | |
} | |
class MusicAppDemo extends StatelessWidget { | |
MusicAppDemo({Key? key}) : super(key: key); | |
final MusicDatabase database = MusicDatabase.mock(); | |
final GoRouter _router = GoRouter( | |
initialLocation: '/library', | |
routes: <RouteBase>[ | |
ShellRoute( | |
builder: (BuildContext context, GoRouterState state, Widget child) { | |
return MusicAppShell( | |
child: child, | |
); | |
}, | |
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.params['albumId'], | |
); | |
}, | |
routes: [ | |
GoRoute( | |
path: 'song/:songId', | |
// Display on the root Navigator | |
builder: (BuildContext context, GoRouterState state) { | |
return SongScreen( | |
songId: state.params['songId']!, | |
); | |
}, | |
), | |
], | |
), | |
], | |
), | |
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.params['songId']!, | |
); | |
}, | |
), | |
], | |
), | |
GoRoute( | |
path: '/search', | |
pageBuilder: (context, state) { | |
final query = state.queryParams['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 MusicAppShell extends StatelessWidget { | |
final Widget child; | |
const MusicAppShell({ | |
Key? key, | |
required this.child, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: child, | |
bottomNavigationBar: BottomNavigationBar( | |
items: const <BottomNavigationBarItem>[ | |
BottomNavigationBarItem( | |
icon: Icon(Icons.my_library_music_rounded), | |
label: 'Library', | |
), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.timelapse), | |
label: 'Recently Played', | |
), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.search), | |
label: 'Search', | |
), | |
], | |
currentIndex: _calculateSelectedIndex(context), | |
onTap: (int idx) => _onItemTapped(idx, context), | |
), | |
); | |
} | |
static int _calculateSelectedIndex(BuildContext context) { | |
final GoRouter route = GoRouter.of(context); | |
final String location = route.location; | |
if (location.startsWith('/recents')) { | |
return 1; | |
} else if (location.startsWith('/search')) { | |
return 2; | |
} else { | |
return 0; | |
} | |
} | |
void _onItemTapped(int index, BuildContext context) { | |
switch (index) { | |
case 1: | |
GoRouter.of(context).go('/recents'); | |
break; | |
case 2: | |
GoRouter.of(context).go('/search'); | |
break; | |
case 0: | |
default: | |
GoRouter.of(context).go('/library'); | |
break; | |
} | |
} | |
} | |
class LibraryScreen extends StatelessWidget { | |
const LibraryScreen({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
final database = MusicDatabase.of(context); | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('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 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: AppBar( | |
title: const Text('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: AppBar( | |
title: const Text('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: AppBar( | |
title: Text('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.subtitle1, | |
), | |
], | |
), | |
], | |
), | |
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: AppBar( | |
title: Text('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.subtitle1, | |
), | |
], | |
), | |
) | |
], | |
) | |
], | |
), | |
); | |
} | |
} | |
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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment