【ProviderとRiverpodの違い③】結合が難しくエラーが発生しやすい

【ProviderとRiverpodの違い③】結合が難しくエラーが発生しやすい

はじめに

前回に引き続き、今回もRiverpodのモチベーションについて解説していきます。

今回の記事では、「Providerの結合が難しくエラーが発生しやすい」というセクションを掘り下げていきます。

Providerの結合が難しくエラーが発生しやすい

公式ドキュメントの説明

Provider を使用する際、provider の create内で context.watchを使用したくなるかもしれません。

これは信頼できず、エラーが発生しやすい方法です。

didChangeDependencies がトリガーされる可能性があります(例:ウィジェットツリーに GlobalKey が含まれている場合など)。

Provider には ProxyProvider という特別なソリューションがありますが、これは面倒でエラーが発生しやすいとされています。

Riverpod では、ref.watchref.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()を使用すると、以下の問題が発生する可能性があります。

  1. ライフサイクルの問題
    • createメソッドはプロバイダーが作成されるときに一度だけ呼び出されます。このタイミングで他のプロバイダーをwatchすると、依存関係が正しく設定されず、更新が正確に反映されない可能性があります。
  2. エラーの発生
    • Providerのドキュメントによると、create内でcontextを使用することは安全ではなく、特にwatchを使うと予期しないエラーが発生しやすくなります。これはcontextが完全に構築される前に依存関係を解決しようとするためです。

これを解決する推奨されるアプローチとして、依存関係がある場合はProxyProviderの使用が推奨されていて、他のプロバイダーからのデータを安全に取得し、新しいプロバイダーの生成に利用できます。

しかし、この方法においても ProxyProvider は面倒でエラーが発生しやすいとされています。(なぜかまでの詳細はここでの解説は割愛させていただきます。)

Riverpodにおける結合の改善

Riverpodでは、Providerの依存関係はProviderReferenceref)を通じて管理されます。

これにより、BuildContextに依存しないため、ウィジェットのライフサイクルから独立して動作し、より信頼性と効率が向上します。

以下の例では、doubledProvidernumberProviderの値を監視し、その値を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で改善されたことにより、状態管理がさらに扱いやすくなったことが分かりました。

次回、「安全性の欠如」デュエルスタンバイ!

Anycloudではプロダクト開発の支援を行っています

プロダクト開発をお考えの方はぜひAnycloudにご相談ください。

まずは相談する

記事を書いた人

Matsuura

エンジニア

Matsuura

Twitter

Anycloudでエンジニアしてます!主にFlutterやWebフロントをやっていて、最近はバックエンドやインフラに挑戦・苦戦中。