Michele Volpato

Michele Volpato

Flutter full app 4. Add Provider and move hardcoded data

Tutorials Flutter

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 InheritedWidgets 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 PlaylistItems.

// 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 PlaylistItems into AudioSources, 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.

Get a weekly email about Flutter

Subscribe to get a weekly curated list of articles and videos about Flutter and Dart.

    We respect your privacy. Unsubscribe at any time.

    Leave a comment