Flutter full app 4. Add Provider and move hardcoded data
In the previous article, we did not add any new functionality to the code. We upgraded Flutter to version 2 and migrated to sound null safety.
In this article, we are going to add Provider to the app and move the hardcoded data in a class that will be used until we implement a way to get data from a real source. We will also add a new screen to the app.
Using Provider
Provider is one of my favorite packages for Flutter. It makes InheritedWidget
s very convenient to use.
You use one of the providers made available by the package to inject an object in your widget tree, and then you either use Provider.of
or Consumer
to get that object lower in the tree.
When you use Provider.of
or Consumer
, you specify the type of the object you want to use, the class name, and you receive the object that is closer to where you are in the tree, injected with Provider
.
Start with adding the dependency to pubspec.yaml
:
# State management helper
provider: ^5.0.0
and then run flutter pub get
in to download the package and add it to your project.
Create a service that exposes playlists
The idea is to use Provider
to inject an object that exposes one or more playlists. We do not have yet a class that stores playlists, we do not even have a formal way to define the items of a playlist. We need to create a PlaylistItem
class and an Author
class that represents the author of a playlist item (the host of a podcast, or the artist of a song).
// author.dart
class Author {
final String name;
final Uri? image;
Author(this.name, this.image);
}
// playlist_item.dart
import 'package:music_player/domain/playlists/author.dart';
class PlaylistItem {
final Author author;
final String title;
final Uri? artworkUri;
final Uri itemLocation;
PlaylistItem(
this.author,
this.title,
this.artworkUri,
this.itemLocation,
);
}
and now we can create a class that exposes lists of PlaylistItem
s.
// playlist_service.dart
import 'package:music_player/domain/playlists/author.dart';
import 'package:music_player/domain/playlists/playlist_item.dart';
abstract class PlaylistsService {
List<PlaylistItem> get allItems;
Map<Author, List<PlaylistItem>> get itemsByAuthor;
}
This class is abstract, so that we can write different kind of playlist providers. Until we decide how we are going to get data in the app, we use a hardcoded list of items:
// hardcoded_playlists_service.dart
import 'package:music_player/domain/playlists/author.dart';
import 'package:music_player/domain/playlists/playlist_item.dart';
import 'package:music_player/services/playlists/playlists_service.dart';
class HardcodedPlaylistsService implements PlaylistsService {
final _gameSongs = [
PlaylistItem(
Author("Blizzard North", null),
"Tristram",
Uri.parse(
"https://upload.wikimedia.org/wikipedia/en/3/3a/Diablo_Coverart.png"),
Uri.parse(
"https://archive.org/download/IGM-V7/IGM%20-%20Vol.%207/25%20Diablo%20-%20Tristram%20%28Blizzard%29.mp3")),
PlaylistItem(
Author("Game Freak", null),
"Cerulean City",
Uri.parse(
"https://upload.wikimedia.org/wikipedia/en/f/f1/Bulbasaur_pokemon_red.png"),
Uri.parse(
"https://archive.org/download/igm-v8_202101/IGM%20-%20Vol.%208/15%20Pokemon%20Red%20-%20Cerulean%20City%20%28Game%20Freak%29.mp3")),
PlaylistItem(
Author("Lucasfilm Games", null),
"The secret of Monkey Island - Introduction",
Uri.parse(
"https://upload.wikimedia.org/wikipedia/en/a/a8/The_Secret_of_Monkey_Island_artwork.jpg"),
Uri.parse(
"https://scummbar.com/mi2/MI1-CD/01%20-%20Opening%20Themes%20-%20Introduction.mp3")),
];
@override
List<PlaylistItem> get allItems {
return _gameSongs;
}
@override
// TODO: implement itemsByAuthor
Map<Author, List<PlaylistItem>> get itemsByAuthor =>
throw UnimplementedError();
}
Inject the playlist service
Now we can inject an object of this class in the widget tree, in main.dart
.
home: Provider<PlaylistsService>(
create: (_) {
return HardcodedPlaylistsService();
},
child: Player()),
We can use it in player.dart
.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SafeArea(
child: Consumer<PlaylistsService>(
builder: (__, value, _) {
_loadAudioSources(value.allItems);
return Column(
children: [
Expanded(child: Playlist(_audioPlayer)),
PlayerButtons(_audioPlayer),
],
);
},
),
),
),
);
where _loadAudioSources(value.allItems)
converts PlaylistItem
s into AudioSource
s, needed by just_audio
, and loads them in the audio player.
void _loadAudioSources(List<PlaylistItem> playlist) {
_audioPlayer
.setAudioSource(
ConcatenatingAudioSource(
children: playlist
.map(
(item) => AudioSource.uri(
item.itemLocation,
tag: AudioMetadata(
title: item.title, artwork: item.artworkUri.toString()),
),
)
.toList(),
),
)
.catchError((error) {
// catch load errors: 404, invalid url ...
print("An error occured $error");
});
}
These changes can be found at this GitHub commit.
Adding playlist selection
Next up is a change to the UI. We want to be able to select the list of audio we play, and, in the future, add more playlists.
We create a new Widget
, CategorySelector
, and we take the state-related code from Player
. This widget will show a list of playlists, and when you tap one, the app navigates to the Player
widget.
/// category_selector.dart
class CategorySelector extends StatefulWidget {
@override
_CategorySelectorState createState() => _CategorySelectorState();
}
class _CategorySelectorState extends State<CategorySelector> {
late AudioPlayer _audioPlayer;
@override
void initState() {
super.initState();
_audioPlayer = AudioPlayer();
}
@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: SafeArea(
child: Consumer<PlaylistsService>(
builder: (__, value, _) {
return Column(
children: [
Expanded(
child: ListView(
children: [
ListTile(
title: Text("All items"),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
Player(_audioPlayer, value.allItems),
),
);
},
),
],
),
),
StreamBuilder<bool>(
stream: _audioPlayer.playingStream,
builder: (context, snapshot) {
// If we are not playing, do not show the player buttons
if (snapshot.hasData && (snapshot.data ?? false))
return PlayerButtons(_audioPlayer);
else
return Container();
}),
],
);
},
),
),
),
);
}
}
There is only one list of audio items, for now, so we only have one ListTile
. On tapping it, we navigate to the Player
widget where we can start playing the audio. We also add an AppBar
to make navigation easier.
Player
will need to be changed. We instantiate the audio player in CategorySelector
, so we can pass it to Player
, which now can be refactored into a StatelessWidget
.
// player.dart
class Player extends StatelessWidget {
final AudioPlayer _audioPlayer;
final List<PlaylistItem> _playlist;
Player(this._audioPlayer, this._playlist, {Key? key}) : super(key: key) {
if (!_audioPlayer.playing) _loadAudioSources(_playlist);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: SafeArea(
child: Column(
children: [
Expanded(child: Playlist(_audioPlayer)),
PlayerButtons(_audioPlayer),
],
),
),
),
);
}
On constructing Player
, we load the audio only if it is not playing. This is correct for the first playlist we play, but if we have more playlists, we cannot switch between them unless we stop the audio first. This is not a good user experience, and we will fix it in the future.
The app starts to look a little bit more interesting. In the next article, we will fix the user experience problem described above and we will add more playlists to the mix.
The full code for this article can be found on GitHub.
Leave a comment