【Flutter】Riverpod+StateNotifier+freezedで作ろう

もくじ

こんにちは。
スマホアプリをメインに開発しているロッキーカナイです。

今日は「Riverpod+StateNotifier+freezed」について書いてみたいと思います。

ちなみにですが、これまでProvider系とか、StateNotifierProvider+freezedを用いたものはずっと触っているので良いのですが、Riverpodは個人でちょろっと触ったぐらいで次の業務でやる可能性が高いので、いい機会だと思い記事にしてみました。

最後にも感想書きますが「StateNotifierProvider+freezed」やってたら全然違和感なく使いこなせるかと思います。

今回は全体的にざっくりした概念の紹介で、初めての方もスムーズに導入してもらえるような内容にしました。

はじめに

先人達の参考URLを紹介します。

Flutter3系+RiverPod+StateNotifier+freezedで日々の記録管理アプリを作ろう!

Riverpod + StateNotifier + FreezedでState管理

Riverpod + freezedで状態管理

これらのサイトは大変勉強になりました。

今回は初歩的なざっくり内容をお届けするので、もっと詳しく知りたい方は一読ください。

やること

今回作るのは、SpotifyAPIからプレイリストを取得してリスト表示させるものを「Riverpod+StateNotifier+freezed」で作ります。

具体的には以下のような要件にしたいと思います。

  1. SpotifyAPIからプレイリスト一覧を取得しリスト表示する
  2. リストには、アルバムジャケット画像・曲名・収録アルバム名を表示させる
  3. リロードボタンを設置
  4. ロード中はインジケータ表示

登場人物

  1. Model(データの塊で、ListPageのステートデータとなる) : ListState
  2. StateNotifier(処理をしたり、Model(ステート)の更新をしたりするやつ):  ListPageNotifier
  3. CustomerWidget(俗にいうViewでレイアウト部分。Model(ステート)の内容に応じてレイアウト変更をしたりする): ListPage

大きく分けるとこんな感じで、StateNotifierでステート(Model)が更新されるとCustomerWidgetで更新が走るのでステートの状態に応じた表示が常にされるという流れです。

作ってみる

Flutterバージョン

Flutter (Channel stable, 3.7.9)

pubspec.yaml(抜粋)

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  flutter_riverpod: ^2.3.2 #追加
  freezed_annotation: #追加
  spotify: ^0.10.0#追加
  json_annotation: ^4.8.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0
  freezed: ^2.3.2 #追加
  build_runner: ^2.3.3 #追加
  json_serializable:

バージョンは使用している環境に合わせてください。

プラグインの取得は、お決まりの

flutter pub get

を行ってください。

ListState

まず始めにModel(ステートデータ)を作ってみます。

ローディングがあるのでフラグと、曲情報のリストデータを持つクラスにします。

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:spotify/spotify.dart'; // ---> ①

part 'list_state.freezed.dart';

@freezed
class ListState with _$ListState {
  const ListState._();
  const factory ListState({
    List<Track>? list, // ---> ②
    @Default(true) bool loading, // ---> ②
  }) = _ListState;
}
  1. spotify.dartをインポートしているのは曲情報のリストデータがspotify側で作られているデータクラスのTrackを読み込む為になります。
  2. ListStateは曲情報(Track)のリストデータのlistとローディングフラグ(初期値true)を持たせてます。

freezedを利用する為には、以下コマンドを実行する必要があります。(コマンドオプションはぐぐってください)

flutter pub run build_runner build --delete-conflicting-outputs

実行するとlist_state.freezed.dartが生成されればOKです。

ListPageNotifier

続いて、Model(ステートデータ)を更新したりするクラスを作ります。

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:test_flutter/models/list_state.dart';

export 'package:test_flutter/models/list_state.dart';

class ListPageNotifier extends StateNotifier<ListState> {
  ListPageNotifier() : super(const ListState()) {
    _load(); // ---> ①
  }

  void _load() async {
    final spotify = SpotifyApi(
      SpotifyApiCredentials(
        "********",
        "********",
      ),
    );
    final tracks = await spotify.artists.getTopTracks(
      '5yCWuaBlu42BKsnW89brND',
      "JP",
    );
    state = state.copyWith( // ---> ②
      list: tracks.toList(),
      loading: false,
    );
  }

  void reload() { // ---> ③
    state = state.copyWith(loading: true);
    _load();
  }
}
  1. ListPageNotifierが生成されたらSpotifyの曲情報のリストを取得します。
  2. state.copyWith()でModel(ListState)の更新を行います。これによりView側で再生成(リビルド)が発生します。
  3. リロードを行う為、ローディングフラグを立ててから_load()を読んでます。

ListPage

最後にView側のレイアウト実装です。

Model(ListState)であるステートがListPageNotifierによって更新されたらリビルトされるので、いい感じにしてみます。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:test_flutter/notifiers/list_page_notifier.dart'; // ---> ①

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ProviderScope( // ---> ②
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blueGrey,
        ),
        home: const ListPage(),
      ),
    );
  }
}

final listPageProvider = StateNotifierProvider<ListPageNotifier, ListState>(
    (ref) => ListPageNotifier()); // ---> ③

class ListPage extends ConsumerWidget {
  const ListPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final listPageState = ref.watch(listPageProvider); // ---> ④
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          '4s4ki',
          style: TextStyle(fontSize: 30),
        ),
      ),
      body: listPageState.loading // ---> ⑤
          ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: listPageState.list?.length ?? 0, // ---> ⑤
              itemBuilder: (BuildContext context, int index) {
                final current = listPageState.list![index];
                return Padding(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 2.0,
                    vertical: 1.0,
                  ),
                  child: Card(
                    child: ListTile(
                      leading:
                          Image.network(current.album?.images?.first.url ?? ""), // ---> ⑤
                      title: Text(
                        current.name ?? "", // ---> ⑤
                        style: const TextStyle(fontSize: 17),
                      ),
                      subtitle: Text(
                        current.album?.name ?? "", // ---> ⑤
                        style: const TextStyle(fontSize: 10),
                      ),
                    ),
                  ),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.blueGrey,
        child: const Icon(Icons.download),
        onPressed: () => ref.read(listPageProvider.notifier).reload(), // ---> ⑥
      ),
    );
  }
}
  1. ListPageNotifierの読み込みをしています。
  2. ProviderScopeでラップすることでProviderが利用できる様になります。
  3. StateNotifierProviderの宣言です。これはListPageNotifierとListStateが紐付けたStateNotifierProviderですって感じです。
  4. ListPageNotifierによってListStateが更新されたら、検知する為のお決まりの書き方です。今回変数名をlistPageStateとしてますが、命名通り中身はListStateクラスの変数です。
  5. ListState(ステート)のデータが更新されたらこちらがリビルド(再生成)されます。
  6. ref.read()はListPageNotifierメソッド呼び出しなど監視が必要でない場合に利用します。ここではリロードの関数を読んでます。

作ったもの

さいごに

「Riverpod+StateNotifier+freezed」は、「StateNotifierProvider+freezed」でのProviderスコープが限定的で使いにくかった部分の改良版だと感じましたし、めちゃめちゃ使いやすかったです。「StateNotifierProvider+freezed」を経験している方は全く問題なく「Riverpod+StateNotifier+freezed」へ移行できるかと思います。今回初心者の方でも、ざっくりこんな感じというイメージを持っていただけたなら嬉しいです。

あと作ってて気になった部分はSpotifyのTrackデータから使用するデータまで深いので(current.album?.images?.first.urlみたいな)ここで使用するデータモデルを作ってListPageNotifierでTrackデータを取ってきたら、それにつっこんであげる方がいいかとも思ったのですが諸事情で行いませんでした。

他の記事でも何度も書いてますがおすすめプレイリストはこちら

関連する記事

  • 映画公開予定通知botを作ってみた!

  • 【Flutter】通信ライブラリRetrofit(Dio)がいい感じ

  • 【Flutter】Riverpod+StateNotifier+freezedで作ろう

  • ChatGPTくんと仲良くなって手羽元の甘辛煮を作るまで

もっと見る