見出し画像

【神パッケージ】 Riverpod の使い方【Flutter】

YOTA

今回紹介する Riverpod は Flutter の状態管理パッケージです。Riverpod のおかげで僕の Flutter ライフは最高に快適になりました。紛れもない神パッケージです。今まで provider パッケージをフル活用してアプリを開発してきましたが、新しいアプリでは Riverpod パッケージを使っています。
Riverpod は Flutter でよく使われている provider パッケージを開発している人が、provider パッケージで問題のある部分を改良したものです。
2021年の11月に正式版(バージョン 1.0.0 )が公開されて、より安心して使えるようになりました。
Riverpod の実践的なアーキテクチャについては、こちらまで。

provider の問題点と Riverpod の解答

provider パッケージは充分に優れていて、正直そんなに困ることもありません。そんな中で Riverpod は provider の優れた特徴を全て備えつつも、さらに以下の特徴があります。
✅ complie-safe である(つまり、ランタイムエラーが起こらない)
provider を使っている人にはおなじみ(?)の ProviderNotFoundException がもう起こりません。開発時間に影響する重要な特徴です。
✅ より柔軟な構造が設計可能
・同じ型で、複数の Provider を使用可能
・使われなくなった状態を自動で破棄 (dispose)
・computed states を実装可能
・Provider を private スコープで宣言可能
✅ Flutter に依存しない(InheritedWidgets を使わない実装)

Riverpod は Dart 言語のみを使った実装になっているので例えばサーバーサイド Dart などでも使えます。
✅ Provider をグローバルに定義できる
依存関係を一箇所にまとめて定義したりする必要がなくどこからでも Provider を定義できます。これ、めちゃくちゃ便利です。

Riverpod のインストール

Riverpod を使うにはいくつかの選択肢があります。
公式にフローチャート図があるのでこれにそって選択すれば良いでしょう。ここでは、一般的だと思われる hooks_riverpod パッケージに絞って解説していきます(flutter_riverpod と機能面は完全に同じですが短く書ける)。
詳しくはこちらのリンクをご覧下さい:What package to install | Riverpod

画像1


flutter_hooks は React Hooks の Flutter での実装でこちらも Riverpod の作者が開発しています。Hooks についての解説は、こちらなどが参考になると思います(丸投げ🙄):フック早分かり | React

Providers とは?

ここでいう Provider は、provider パッケージのことではなくて Riverpod における Provider クラスです。provider パッケージで出てくるいろいろな Provider 群とほぼ同じです。違いは、同じ型で違う名前の Provider を定義できる所です。
Provider は 変数を保持しておく入れ物であり、Riverpod で提供される関数を通じてアクセスできるものを言います。この Provider クラス群が Riverpod で中心となっています。Riverpod では 以下のようにグローバルな final 値として定義することが一般的です。 Provider は完全に immutable なのでグローバルに定義しても問題は起こりません。ここで ref というオブジェクトは、他の Provider にアクセスしたり、破棄の時の処理をいれるために使えます。

final myProvider = Provider((ref) {
 return MyValue();
});

Provider は他に StateProvider / ChangeNotifierProvider / StreamProvider / StateNotifierProvider などの変種があります。
注意点として Provider を使用するには ProviderScope でくくる必要があります。

void main() {
 runApp(ProviderScope(child: MyApp()));
}

Provider 修飾子

Provider を宣言するときに、autoDispose / family という修飾子を使うことができます。現在はこの二つのみ提供されていますが、今後増える可能性がありそうです。使い方は名前付きコンストラクタに近いシンタックスとなっています。​​
👉 autoDispose
autoDispose をつけると、使われなくなった Provider を自動的に破棄することが可能です。使い方は簡単。 .autoDispose を追加するだけ。autoDispose がつくと Provider に maintainState という破棄するか否か指定できるフラグが追加されます。詳しくはこちらを参照。

// autoDispose を使用
final userProvider = FutureProvider.autoDispose<User>((ref) {
  ...

👉 family
family をつけると、外部の値を使って Provider を作成することができます。例えば、FutureProvider に family をつけると渡された値によって API を投げる Provider が作れます。渡せる値は、プリミティブ値かコンスタント値か、==hashCode をオーバーライドした immutable オブジェクトのみです。詳しくはこちらを参照。

// family を使用
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
 return dio.get("http://my_api.dev/messages/$id");
});

チェインして両方使うこともできます:

// autoDispose と family 両方を使用
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
 return fetchUser(userId);
});

Provider にアクセスする

Provider にアクセスするには、 ref.watch が使えます。watch の場合は、値が変更されたときに Provider が再生成されますが read の場合はされません。read の使い方は少し癖がありますので、この後説明します。

// 都道府県を提供する Provider を定義する
final prefectureProvider = Provider((ref) => '東京');

// weatherProvider は、prefectureProvider に依存する
final weatherProvider = FutureProvider((ref) async {
 // `ref.watch` によって他の Provider を監視できる
 // prefecture が変わった場合、もう一度作られる
 final city = ref.watch(prefectureProvider);

 // prefectureProvider の値によって天気を取得する
 return fetchWeather(prefecture: prefecture);
});

ref.read を使う
他の Provider から得られる値がほとんど変わらないようなときには、ref.read を使うと Provider の再生成が行われずに実装することができます。
ただし Provider の中で ref.read を呼ぶのではなくて ref.read を渡した先で使います。ref.read を Provider 本体の中で呼ぶことは推奨されていません。
Repository にトークンを渡すような場面では以下の疑似コードのようになります。provider パッケージでは依存関係を解決するのにコンストラクタを使っていましたが、Riverpod では使いたい時に好きな Provider にアクセスできるため非常に便利です。

// Token Provider を定義
final userTokenProvider = StateProvider<String>((ref) => '');

// Repository Provider を定義(ref.readRepository に渡す)
final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
 Repository(this.read);

 // `ref.read` 関数 == Reader クラス
 final Reader read;

 Future<Catalog> fetchCatalog() async {
   String token = read(userTokenProvider);

   final response = await dio.get('/path', queryParameters: {
     'token': token,
   });

   return Catalog.fromJson(response.data);
 }
}

最も簡単な実装例(カウンターアプリを使って)

最後に、簡単な実装例を使って実例をみていきます。
flutter プロジェクトを作ったときのカウンターアプリ改良してみていきます。
👉 準備: flutter_hooks と hooks_riverpod をインストール
まずは、パッケージをインストールしましょう。hooks_riverpod を使うには flutter_hooks のインストールが必要です。

dependencies:
 flutter:
   sdk: flutter

 flutter_hooks: ^0.18.0
 hooks_riverpod: ^1.0.0

👉 1:まずは ProviderScope で包む
Widget ツリー上で、Provider にアクセスできるようにするために、一番根元を ProviderScope で包みます。

void main() {
 runApp(ProviderScope(child: MyApp()));
}

👉 2.  カウントの値を保持する Provider を定義 
状態の入れ物としての Provider を、グローバルに final で定義します。ここでは、StateProvider を使います。StateProvider は、内部に state_notifier を使っているため、state を変更するだけで変更が通知されるようになっています(便利!)。もちろん ChangeNotifierProvider / StateNotifierProvider / StreamProvider などを代わりに使うこともできます。

final counterProvider = StateProvider((ref) => 0);

👉 3.  状態を監視する
build メソッドの中で ref.watch を使って Counter の値を取得します。useProvider を使用する Widget は flutter_hooks で定義されている、HookConsumerWidget を継承している必要があります。ref.watch を使ってアクセスすることで、状態の変更通知がされた時にリビルドされるようになります。HookConsumerWidget を継承することにより、build メソッドの引数に、WidgetRef ref が追加されます。

class MyApp extends HookConsumerWidget {
 @override
 Widget build(BuildContext context, WidgetRef ref) {
   final int count = ref.watch(counterProvider);
   ...

👉 4.  状態を変更する
onPressed の中などで Counter の状態を変更します。それには ref.read メソッドを使います。stateを変更することで、自動的に変更通知が行われます。ref.read に渡す Provider は、.notifier を付与する点に注意です。

...
     floatingActionButton: FloatingActionButton(
       onPressed: () {
         ref.read(counterProvider.notifier).state++;
       },
     ),
...

全コード 
コード全体を貼っておきます。直感的でありながら非常にコード量が少ないことが分かります。

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
 runApp(ProviderScope(child: MyApp()));
}

final counterProvider = StateProvider<int>((ref) => 0);

class MyApp extends HookConsumerWidget {
 @override
 Widget build(BuildContext context, WidgetRef ref) {
   final int count = ref.watch(counterProvider);
   return MaterialApp(
     home: Scaffold(
       body: Center(child: Text('$count')),
       floatingActionButton: FloatingActionButton(
         onPressed: () {
           ref.read(counterProvider.notifier).state++;
         },
       ),
     ),
   );
 }
}

終わりに

ご覧いただきありがとうございました。riverpod は採用事例もどんどん増えているためぜひ使ってみてください。Twitter やってますので感想やご指摘などありましたらこちらまで👉 @ytiskw

🆕 Riverpod の実践的なアーキテクチャについてまとめました:


この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
YOTA
Flutter と Dart が好きです。 https://twitter.com/ytiskw