Riverpodを使用して開発をしていた時に学んだこと、学び直したこと
目次
はじめに
flutterを使ってアプリ開発をしていた時に学んだこと、学び直したこと、開発中につまずいたことなどをまとめたものになります。
状態管理
watch readをよく開発で使用していたのでここをまず調べなおすことから始めました。
そのなかで、watchとreadだけでなくlistenがあること、どの場合にlistenを使うべきなどを学んだので学んだことを書いていきます。
watch read listenとは?
watch read listenとは、指定したプロバイダの値の読み取りや更新、監視を行うことができる関数です。
この関数はWidgetRefクラスのメンバ関数となっており、使用するためには条件がありますが後述するConsumerが必要となります。詳細は後述します。
watchとは
- プロバイダで公開されている値を取得し、監視を行うことができる
- 監視を行うことで、値に変更があった際にwatchを使用しているwidgetの再構築を行い画面に値の変化を反映することができる
final counter = ref.watch(counterProvider);
注意
widgetの更新範囲は、watchを使用しているwidgetのbuildメソッド全体となる為、使用方法に注意する必要がある
readとは
- 使用した時点での値を取得することができる
void onPressed() {
final count = ref.read(counterProvider);
}
listenとは
- プロバイダで公開されている値を取得し、監視を行うことができる
- 監視を行うことができ、値の変更を検知することはできるが、widgetの再構築を行わない
ref.listen<int>(counterProvider, (previous, next) {
if (next > 10) {
showDialog(...);
}
});
比較表
項目 | watch | read | listen |
---|---|---|---|
値の監視 | ✅ | ❌ | ✅ |
widgetの更新 | ✅ | ❌ | ❌ |
おまけ
プロバイダーの一部分を部分的に監視し、その監視対象のメンバが変更された時にのみwidgetの更新を行いたい時はselect
関数が用意されています。
final age = ref.watch(personProvider.select((p) => p.age));
Consumer
Consumerとは、部分再構築を行う為に使用されるWidgetでwatchやreadなどを利用する場合に必要となるrefオブジェクトを取得できます。
Consumerの種類
Consumerには必要な箇所で必要な範囲でだけWidgetRef
使用するためのConsumer
と、StatelessWidgetのbuildメソッド全体で使用する為のConsumerWidget
、Statefulwidgetのbuildメソッド全体で使用する為のConsumerStatefulWidget
の3種類があります。
1. Consumer
ConsumerはStatelessWidgetやStatefulWidgetなどのrefオブジェクトを持たないwidget内の任意の範囲内でrefオブジェクトを使用したい時に使います。
class ConsumerTestWidget extends StatelessWidget {
const ConsumerTestWidget({super.key});
@override
Widget build(BuildContext context) => Consumer(
builder: (context, WidgetRef ref,child) => Text(
ref.watch(testProvider),
),
);
}
2. ConsumerWidget
ConsumerWidgetはStatelessWidgetを拡張したwidgetで、上記のConsumerとは違いbuildメソッド内でrefオブジェクトを明示的な指定なしで使用できます。
class ConsumerTestWidget extends ConsumerWidget {
const ConsumerTestWidget({super.key});
@override
Widget build(BuildContext context) => Text(
ref.watch(provider);
);
}
3. ConsumerStatefulWidget
ConsumerStatefulWidgetはStatefulWidgetを拡張したwidgetです。
Stateだった場所がConsumerStateに置き換わっていたりなど、ConsumerWidgetに比べて使うために拡張前から変更するところが多いWidgetです。
class ConsumerTestStatefulWidget extends ConsumerStatefulWidget {
const ConsumerTestStatefulWidget({super.key});
@override
ConsumerState<ConsumerTestStatefulWidget> createState()
=> ConsumerState<ConsumerTestStatefulWidget>;
}
class _ConsumerTestStatefulWidget
extends ConsumerState<ConsumerTestStatefulWidget> {
@override
Widget build(BuildContext context, WidgetRef ref) => Text(
ref.watch(provider);
);
}
Provider
Providerとは、Providerがラップしたステートの変更を監視するオブジェクトです。
グローバルにて宣言されるため、アプリの任意の箇所からアクセスすることが可能で、他のWidgetでの変更をrefオブジェクトを使用することで検出し、変更をWidgetに対して適応させることを可能にするオブジェクトで、Riverpodを使用した状態管理には欠かせない存在です。
Providerの種類
RiverpodのリファレンスにはProviderは6種類あり、それぞれ用途が異なるとあります。
1. Provider
基本のProviderです。
値を同期的に生成し、計算結果をキャッシュするなどの用途で使われることから、リストデータのフィルタリング等で使われるようです。
final filteredListProvider = Provider<List<Item>>((ref) {
final items = ref.watch(itemsProvider);
return items.where((item) => item.isActive).toList();
});
2. StateProvider
外部から変更が可能なステートを公開するプロバイダです。
シンプルなステートを管理することに使うことを推奨されている為、バリデーションやカスタムクラスやList/Mapを使用するようなステートを管理する場合はStateNotifierProvider
が推奨されています。
主にドロップダウンの更新や直近のデータに対しての加算処理などの簡易的な値の更新を行う場合に使用されます。
/// 例
final counterProvider = StateProvider<int>((ref) => 0);
ref.read(counterProvider.notifier).update((state) => state + 1);
3. StateNotifierProvider
StateNotifierを監視し、公開するためのプロバイダです。
ユーザ操作などによって状態を管理するためのソリューションとしてRiverpodが推奨しています。
このプロバイダが公開するステートはイミュータブル(不変)なステートであるため、外部からはステートの値を直接変更することはできません。
ステートの値を変更する場合は変更する為のメンバ関数を用意することで変更が可能です。(簡単にいうとカプセル化されたオブジェクトです。)
class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);
void add(Todo todo) {
state = [...state, todo];
}
void remove(String id) {
state = state.where((todo) => todo.id != id).toList();
}
}
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
4. FutureProvider
非同期操作が可能なProviderです。
errorとloadingのステートを処理することができる為、APIの呼び出しなどの非同期の処理のステータスによって表示するWidgetを自動で更新したりすることができる。
/// 例
Widget build(BuildContext context, WidgetRef ref) {
final config = ref.watch(fetchConfigurationProvider);
return switch (config) {
/// 異常時
AsyncError(:final error) => Text('Error: $error'),
/// 正常時
AsyncData(:final value) => Text(value.host),
/// 処理中
_ => const CircularProgressIndicator(),
};
}
5. StreamProvider
FutureProviderのStream版です。
Stream自体が値の更新を監視する性質を持つが、StreamProviderを使用することで、ref.watchを使用したwidgetの更新や、FutureProvider同様にerrorやloadingのステートごとに処理を変えることができる。
公式では、ライブチャットのチャット画面での、データ受信時の自動更新が例としてあげられていました。
(過去に個人制作でチャットアプリを作った時はここの処理に大変苦しめられた記憶があるので面白かったです。)
final messagesProvider = StreamProvider<List<Message>>((ref) {
return FirebaseFirestore.instance
.collection('messages')
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => Message.fromDoc(doc)).toList());
});
6. ChangeNotifierProvider(非推奨)
ChangeNotifierProviderはriverpod公式が非推奨としているプロバイダです。
理由としてはstateがミュータブル(可変)であり、stateを直接変更することができ、どこで変更されたかの追跡や、意図しないstateの変更が起こりうるため、StateNotifierProviderを使用することを推奨しています。
class Counter extends ChangeNotifier {
int _count = 0;
void increment() {
_count++;
notifyListeners();
}
}
あとがき
参考リソース
おわりに
開発中に学んだことや、この記事を書く上で改めて学び直したことを書いていきました。
開発中はgeneratorを使用していたので、意識していなかったことも知れたので、やってよかったと思います。
次はdartのバックエンドについて記事を書いていけたらと考えています。(公開予定はアドカレ)