見出し画像

【Flutter】 Riverpod の使い方入門編

今回紹介する Riverpod は Flutter の新しめの状態管理パッケージです。
Riverpod は Flutter でよく使われている provider パッケージを開発している人が、このパッケージで問題のある部分を改良したものです。
今まで provider パッケージをフル活用してアプリを開発してきましたが、新しいアプリでは Riverpod パッケージを使ってみようということで備忘録を兼ねてまとめておきます。現在はまだβ版なのでプロダクションで使うのは慎重になった方がいいかもしれませんが、これからメジャーに使われるようになってくると思っています。
以前 provider パッケージについて解説した記事はこちらです:
Provider のススメ
Riverpod の実践的なアーキテクチャについては、こちらまで。

provider の問題点と Riverpod の解答

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

Riverpod は Dart 言語のみを使った実装になっているので例えばサーバーサイド Dart などでも使えます。

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 は 状態を保持しておく入れ物であり、その状態を監視できるものを言います。この Provider クラス群が Riverpod でも中心となっています。Riverpod では 以下のようにグローバルな final 値として定義することが一般的です。 Provider は完全に immutable なのでグローバルに定義しても問題は起こりません。ここで ref というオブジェクトは、他の Provider にアクセスしたり、破棄の時の処理をいれるために使えます (onDispose メソッド)。

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 にアクセスする方法はいくつかあって、場所や条件によって変わってきます。これについては、公式のフローチャートを参考にして(丸コピして)日本語版を作成したのでご自由にご利用ください。hooks_riverpod ライブラリを使う場合には、useProvider を使うことが多くなるかと思います。

画像2

Provider から他の Provider にアクセスする

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 にトークンを渡すような場面では以下の疑似コードのようになります。

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

// 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).state;

   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.11.0
 hooks_riverpod: ^0.6.1

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

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

👉 2.  カウントの値を保持する Provider を定義 
状態の入れ物としての Provider を、グローバルに final で定義します。ここでは、StateProvider を使います。ChangeNotifierProvider / StateNotifierProvider / StreamProvider などを代わりに使うこともできます。

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

👉 3.  状態にアクセスする
build メソッドの中で useProvider を使って Counter の値を取得します。useProvider を使用する Widget は flutter_hooks で定義されている、HookWidget を継承している必要があります。

class MyHome extends HookWidget {
 @override
 Widget build(BuildContext context) {
   final int count = useProvider(counterProvider).state;
   ...

👉 4.  状態を変更する
onPressed の中などで Counter の状態を変更します。それには context.read メソッドを使います(ここは provider パッケージと同じですね)

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

全コード 
コード全体を貼っておきます。直感的でありながら非常にコード量が少ないことが分かります。 実際のプロジェクトでは、ファイルを分離しましょう。

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

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

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

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) => MaterialApp(home: MyHome());
}

class MyHome extends HookWidget {
 @override
 Widget build(BuildContext context) {
   final int count = useProvider(counterProvider).state;
   return Scaffold(
     body: Center(child: Text('$count')),
     floatingActionButton: FloatingActionButton(
       onPressed: () {
         context.read(counterProvider).state++;
       },
     ),
   );
 }
}

一応 ChangeNotifier での例も載せておきます

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

final counterProvider = ChangeNotifierProvider((ref) => CounterNotifier());

class CounterNotifier with ChangeNotifier {
 int _count = 0;
 int get count => _count;
 void increment() {
   _count++;
   notifyListeners();
 }
}

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

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) => MaterialApp(home: MyHome());
}

class MyHome extends HookWidget {
 @override
 Widget build(BuildContext context) {
   final int count = useProvider(counterProvider).count;
   return Scaffold(
     body: Center(child: Text('$count')),
     floatingActionButton: FloatingActionButton(
       onPressed: context.read(counterProvider).increment,
     ),
   );
 }
}

終わりに

ご覧いただきありがとうございました。まだβ版ですが、個人的にはプロダクションでも充分使えそうだな、と感じました。需要がありそうだったらもっと Riverpod について掘り下げていきます。Twitter やってますので感想やご指摘などありましたらこちらまで👉 @mxiskw

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


この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
8
ソフトウェアを作るのが好きです。Flutter / Vue / Firebase など。

こちらでもピックアップされています

Flutter 通信
Flutter 通信
  • 4本

Flutter について、書いていきます。

コメントを投稿するには、 ログイン または 会員登録 をする必要があります。