📑

Riverpod での送信機能の条件分岐が意外とトリッキーな件

2022/12/25に公開

HUIT アドベントカレンダー 2022、11日目!いやー、ギリ間に合いましたね、クリスマス中には。

ということで、今回は Flutter でお馴染みの状態管理パッケージ、 Riverpod を使った送信系機能の実装方法のご提案です!

注意点

この記事は Riverpod 1.0.4 を使っていて感じた悩みとその解決方法を紹介しています。 2.0.0 以降は全く違った書き方があると思いますので、他の書き方を模索ください。

はじめに

Riverpod の AsyncValue 、便利ですよね!

FutureProvider はデフォルトで AsyncValue を返してくれるので、バックエンド側から取ってきたデータを構築するのに欠かせません。 watch している箇所を限定するなど意識することは増えますが、 .autoDispose 修飾子を使って自動でリフレッシュさせる機構も簡単に構築できます。そう、データ取得の時は直感的で簡単です。また、画面切り替えのタイミングと POST 等のリクエストとが一緒で良い場合も同様に簡単に実装できます。

Widget の描画とリクエストのタイミングとがリンクする場合

画面表示と同時で良い場合はこんな感じで良いと思います。

// POST等のリクエストが描画と同時に起きてしまう
...
	build(BuildContext context, WidgetRef ref){
		final someFuture = ref.watch(someFutureProvider);
		return Text(someFuture.when(
			data: (data) => '$data がいい感じ', ...
		));
	}
...

final someFutureProvider = FutureProvider.autoDispose((ref) => 
	ref.watch(someRepositoryProvider).delete());

build メソッドの最初で AutoDisposeFutureProviderwatch を始めることで、この Widget が画面から消えたタイミングのみで(← ここ怪しいです)適度に someFutureProviderdispose され、次の Widget 構築時にまた someRepositoryProvider でラップしているクラスの delete メソッドが発火します。

おそらく Riverpod を使った非同期処理の王道中の王道の書き方になるのかなと思いますし、実際めちゃめちゃコードも簡潔になるので気持ち良いです。

Widget の描画とは関係なくリクエストをかけたい場合

しかし、この書き方では達成できない要件もあります。
それが『ボタン押した時だけリクエストかけて〜。あ、その結果でその画面の他の部分の表示変えといてや。』です。

さあ、あなたならどうしますか?

FutureProvider と、ガンバルッッッ///

ボタンで ref.read(someFutureProvider) ってしたらいいんだろ!って一瞬なると思います。それで書いてみましょう。

	build(BuildContext context, WidgetRef ref){
		return TextButton(
			onPressed: () => ref.read(someFutureProvider),
			child: Text(ref.watch(someFutureProvider).when(
				data: (_) => 'いい感じ',
				loading: () => '削除中',
				error: (_, __) => 'ダメでした'),
				),
		);
	}
...

final someFutureProvider = FutureProvider.autoDispose((ref) => 
	ref.watch(someRepositoryProvider).delete());

あれ、あれれ??? onPressed で初めて someFutureProvider を起動して削除のロジックを発火させたいはずなのに、これじゃあ Text Widget の描画と同時に someFutureProvider が起動しちゃう?!?!

そ、そうしたら、こうれでどうや!!!!!

	build(BuildContext context, WidgetRef ref){
		return TextButton(
			onPressed: () {
				ref.read(switch.notifier).state = true;
				ref.read(someFutureProvider);
			},
			child: Text(
			ref.watch(switch) ?
				ref.watch(someFutureProvider).when(
					data: (_) => 'いい感じ',
					loading: () => '削除中',
					error: (_, __) => 'ダメでした'),
				) : '削除',
		);
	}
...

final someFutureProvider = FutureProvider.autoDispose<void>((ref) => 
	ref.watch(someRepositoryProvider).delete());
final switch = StateProvider.autoDispose<bool>((_) => false);

なんかもうわけわかんないですね。こんなコードを書いちゃった日の晩は溢れる涙でお家へ帰れません。

こんなコード書かないためにも、以下の書き方を覚えましょう。

⭕️ あきらめて StateNotifier 先輩に登場してもらう

StateNotifier、使いたくないですよね。もっと Riverpod っぽい書き方、したいですよね。でもこればかりはあきらめて使うのが良いと思うんです。

おすすめ ↓

	build(BuildContext context, WidgetRef ref){
		return TextButton(
			onPressed: () => 
				ref.read(deleteStatus.notifier).delete(),
			child: Text(ref.watch(deleteStatus).when(
				data: (_) => 'いい感じ',
				loading: () => '削除中',
				error: (_, __) => 'ダメでした'),
				),
		);
	}
...

class DeleteStatusNotifier extends StateNotifier<AsyncValue<void>> {
	DeleteStatusNotifier(this.ref) : super<const AsyncData<null>>;
	final Ref ref;
	
	Future<void> delete() async {
		state = const AsyncLoading();
		state = await AsyncValue.guard(() => 
			ref.read(someRepositoryProvider).delete());
	}
}

final deleteStatus = StateNotifierProvider
	.autoDispose<DeleteStatusNotifier, AsyncValue<void>>(
		DeleteStatusNotifier.new
	);

ボイラープレートコードは多くなってしまいますが、これだと何をやっているか分かり易いし、無駄に削除のメソッドが呼び出されたりしなくなって良さげですね。

終わりに

今回は Riverpod ver. 1 系の非同期処理について紹介しましたが、 ver. 2 になって Notifier や AsyncNotifier らが登場したと風の噂で聞いておりますので、それらを使うともっと Riverpod っぽい書き方ができるのではないかと思っています。

まだ Riverpod 1 系を使っていて、非同期処理に困った時は参考にしていただけると嬉しいです。

メリークリスマス🎄

Discussion