Chapter 08

StreamProviderの使い方

JboyHashimoto
JboyHashimoto
2023.02.27に更新
このチャプターの目次

https://docs-v2.riverpod.dev/docs/providers/stream_provider
StreamProviderはFutureProviderと似ていますが、FutureではなくStreamのためのものです。

StreamProviderは通常、次のような用途に使用されます。

Firebase や Web-socket をリッスンする
数秒ごとに別のプロバイダをリビルドする

StreamBuilderの代わりにStreamProviderを使用すると、多くの利点があります。

他のプロバイダが ref.watch を使ってストリームをリッスンできるようになります。
AsyncValue のおかげで、ロードとエラー・ケースが適切に処理されることが保証される。
ブロードキャスト・ストリームと通常のストリームを区別する必要がない。
ストリームから送信された最新の値をキャッシュし、イベントが送信された後にリスナーが追加された場合でも、リスナーが最新のイベントにすぐにアクセスできることを保証します。
StreamProvider をオーバーライドすることで、テスト中に簡単にストリームをモックすることができます。


使用するユースケース

データが変化するのを監視して、変化があったら画面を更新したり、FirebaseAuthenticationを使用したときに、ユーザーがログインをしていれば、ログイン後のページへ移動させる方法があります。

Freezedと組み合わせた例だと、全てのデータをリアルタイムに取得するロジックの実装に使用することができます。この方法は、ローカルDBでも使用することができます。

Firestoreから、指定した全てのコレクションのデータを取得する例
https://zenn.dev/flutteruniv_dev/articles/0edaf38f13169f

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:strem_app/model/user.dart';

final usersStreamProvider = StreamProvider<List<User>>((ref) {
  final collection = FirebaseFirestore.instance.collection('users');
  // データ(Map型)を取得
  final stream = collection.snapshots().map(
        // CollectionのデータからUserクラスを生成する
        (e) => e.docs.map((e) => User.fromJson(e.data())).toList(),
      );
  return stream;
});

ログインの維持をする機能を実装するのにも使用されます。こちらは過去に書いた記事のコードです。
https://zenn.dev/flutteruniv_dev/articles/41cd105bc02bd9

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
  // 以下のプロバイダからFirebaseAuthを取得します。
  final firebaseAuth = ref.watch(firebaseAuthProvider);
  // Stream<User?> を返すメソッドを呼び出す。
  return firebaseAuth.authStateChanges();
});

// プロバイダを使用して、FirebaseAuth インスタンスにアクセスします。
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
  return FirebaseAuth.instance;
});
// メールアドレスのテキストを保存するProvider
final emailProvider = StateProvider.autoDispose((ref) {
  return TextEditingController(text: '');
});

final passwordProvider = StateProvider.autoDispose((ref) {
  // パスワードのテキストを保存するProvider
  return TextEditingController(text: '');
});

ログインの判定をストリームを使用して行う。

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_auth/firebase_options.dart';
import 'package:riverpod_auth/service/firebase_provider.dart';
import 'package:riverpod_auth/service/router.dart';
import 'package:riverpod_auth/ui/auth/signup_page.dart';
import 'package:riverpod_auth/ui/page/my_page.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(
    const ProviderScope(child: MyApp()),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

// go_routerで最初に呼び出されるページ。何も表示されることはない.
// ログインしていなければ認証のページに移動し、ログインして入れば、
// ログイン後のページへ移動する.
class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    // StreamProvider を監視し、AsyncValue<User?> を取得する。
    final authStateAsync = ref.watch(authStateChangesProvider);
    // パターンマッチングを使用して、状態をUIにマッピングする
    return authStateAsync.when(
      data: (user) => user != null ? MyPage() : SignUpPage(),
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

Objextboxを使用する例
https://zenn.dev/flutteruniv_dev/articles/5d00ef980ac5ce

Streamでデータを監視していないと、変更があっても画面が更新されないので、技術記事を参考にして作ったコードがなぜ画面が切り替わらないのか疑問が解けた気がします。

// objectboxをインスタンス化して、DBにアクセスできるようにするProvider.
final objectboxProvider = Provider((ref) => objectbox.store.box<User>());

// StreamでListを使って、Userを型にしてモデルの情報を取得するProvider
final objectStreamProvider = StreamProvider<List<User>>((ref) {
  final builder = ref.watch(objectboxProvider).query()
    ..order(User_.id, flags: Order.descending);
  return builder.watch(triggerImmediately: true).map((query) => query.find());
});

Supabaseからデータを取得する例
SupabaseのデーターベースであるPostgreSQLにアクセスをして、全てのデータをリアルタイムに取得するコード。他の外部サービスのデータもFirebaseと似たような仕組みで、取得することができる。

最近本を作りました。ご興味あれば見てみて下さい
https://zenn.dev/joo_hashi/books/a5e2247b85dc0a

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

// Supabaseをインスタンス化するプロバイダー.
final notesProvider = StateProvider<SupabaseClient>((ref) {
  return Supabase.instance.client;
});
// Supabaseからリアルタイムにデータを取得するプロバイダー.
final notesStreamProvider = StreamProvider<List<Map<String, dynamic>>>((ref) {
  return Supabase.instance.client.from('notes').stream(primaryKey: ['id']);
});

// TextEditingControllerを使うためのProvider
final bodyProvider = StateProvider.autoDispose((ref) {
  // riverpodで使うには、('')が必要
  return TextEditingController(text: '');
});