【ProviderとRiverpodの違い③】結合が難しくエラーが発生しやすい
はじめに
前回に引き続き、今回もRiverpodのモチベーションについて解説していきます。
今回の記事では、「Providerの結合が難しくエラーが発生しやすい」というセクションを掘り下げていきます。
Providerの結合が難しくエラーが発生しやすい
公式ドキュメントの説明
Provider を使用する際、provider の
create
内でcontext.watch
を使用したくなるかもしれません。これは信頼できず、エラーが発生しやすい方法です。
didChangeDependencies
がトリガーされる可能性があります(例:ウィジェットツリーに GlobalKey が含まれている場合など)。Provider には
ProxyProvider
という特別なソリューションがありますが、これは面倒でエラーが発生しやすいとされています。Riverpod では、ref.watchやref.listenなどのシンプルで強力なユーティリティを使用して、オーバーヘッドなしで値をリアクティブに結合およびキャッシュできます。
@riverpod
int number(NumberRef ref) {
return Random().nextInt(10);
}
@riverpod
int doubled(DoubledRef ref) {
final number = ref.watch(numberProvider);
return number * 2;
}
値の結合は Riverpod では自然に感じられます:依存関係は読み取り可能で、API は一貫しています。
create内でのcontext.watchの使用
Providerのcreate
メソッドは、プロバイダーが生成される際に実行される関数というのはご存知のはず。
このメソッド内でデータを初期化し、プロバイダーが提供するデータのインスタンスを作成します。
例えば、Provider(create: (_) => MyModel())
のように記述しますね。
ここで、create
メソッド内で他のプロバイダーのデータにアクセスしたくなる場合があります。
その場合、context.watch()
を使用することですが、これはProviderのcreate
コンテキスト内では推奨されていません。
Provider(
create: (context) {
// これはアンチパターン
var otherData = context.watch<OtherProvider>().data;
return MyModel(otherData);
},
)
なぜ推奨されないのか?
create
メソッド内でcontext.watch()
を使用すると、以下の問題が発生する可能性があります。
- ライフサイクルの問題
create
メソッドはプロバイダーが作成されるときに一度だけ呼び出されます。このタイミングで他のプロバイダーをwatch
すると、依存関係が正しく設定されず、更新が正確に反映されない可能性があります。
- エラーの発生
- Providerのドキュメントによると、
create
内でcontext
を使用することは安全ではなく、特にwatch
を使うと予期しないエラーが発生しやすくなります。これはcontext
が完全に構築される前に依存関係を解決しようとするためです。
- Providerのドキュメントによると、
これを解決する推奨されるアプローチとして、依存関係がある場合はProxyProvider
の使用が推奨されていて、他のプロバイダーからのデータを安全に取得し、新しいプロバイダーの生成に利用できます。
しかし、この方法においても ProxyProvider
は面倒でエラーが発生しやすいとされています。(なぜかまでの詳細はここでの解説は割愛させていただきます。)
Riverpodにおける結合の改善
Riverpodでは、Providerの依存関係はProviderReference
(ref
)を通じて管理されます。
これにより、BuildContext
に依存しないため、ウィジェットのライフサイクルから独立して動作し、より信頼性と効率が向上します。
以下の例では、doubledProvider
はnumberProvider
の値を監視し、その値を2倍にしています。この依存関係は、ウィジェットのライフサイクルに関係なく管理されるため、BuildContext
の問題を避けることができます。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final numberProvider = Provider<int>((ref) => 10);
final doubledProvider = Provider<int>((ref) {
final number = ref.watch(numberProvider);
return number * 2;
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final doubled = ref.watch(doubledProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Riverpod Dependency Example")),
body: Text('Doubled number: $doubled'),
),
);
}
}
まとめ
Providerで管理していく上での結合における問題点がRiverpodで改善されたことにより、状態管理がさらに扱いやすくなったことが分かりました。
次回、「安全性の欠如」デュエルスタンバイ!