RxDart入門:Flutterでリアルタイムデータ管理の完全ガイド

IT開発

Flutterアプリ開発において、複数のデータソースを効率的に管理したいと思ったことはありませんか?Firebase AuthとFirestoreのデータを同時に監視して、リアルタイムに画面を更新したい場面などで威力を発揮するのがRxDartです。

注意:現在はpackage:asyncStreamGroupという選択肢もあり、シンプルなStream結合であればそちらの方が適しているケースもあります。

今回は、実際のクイズアプリプロジェクトを例に、RxDartの基本的な使い方を紹介します。

参考プロジェクト:https://github.com/morisakisan/share_quiz

RxDartとは何か?

RxDartは、ReactiveXのDart実装で、Flutterアプリケーションでリアクティブプログラミングを行うためのライブラリです。標準のDart StreamにRxの強力な機能を追加し、複数のデータソースを効率的に組み合わせることができます。

使い所

RxDartは主にリアルタイムデータなどに用いられるStreamを結合する時に使用します。例えば、Firestoreの複数のコレクションからのデータを組み合わせて、一つの画面で表示したい場合などです。Firebase Authの認証状態とFirestoreのデータを同時に監視し、ユーザーの状態に応じて画面の表示を動的に変更する際に威力を発揮します。

ライブラリの読み込み

pubspec.yamlに以下を追加します:

dependencies:
  rxdart: 0.28.0

そして、使用したいファイルでimportします:

import 'package:rxdart/rxdart.dart';

実際のコード例

ここからは、実際のクイズアプリプロジェクトで使用しているRxDartのコードを見ていきましょう。Clean Architectureを採用したプロジェクトで、Repository層でRxDartを活用している例です。

例1: 設定画面での基本的な使用

アプリの設定画面で、アプリ情報とユーザーのログイン状態を組み合わせる例です。

結合されるエンティティ:

class Setting {
  final PackageInfo packageInfo;  // アプリのバージョン情報など
  final bool isLogin;             // ユーザーのログイン状態
}

なぜ結合が必要?
設定画面では「アプリのバージョン情報」と「ログイン状態によって表示を変える項目」を同時に表示する必要があります。例えば、ログインしている場合のみ「ログアウト」ボタンを表示したり、ユーザー情報を表示したりするためです。

class SettingRepositoryImpl extends SettingRepository {
  final _userDataStore = FirebaseAuthStore();

  @override
  Stream<Setting> fetch() {
    return CombineLatestStream.combine2(
        Stream.fromFuture(PackageInfo.fromPlatform()),
        _userDataStore.listenToUserChanges(),
        (info, user) => Setting(
            packageInfo: info, 
            isLogin: user != null
        )
    );
  }
}

ポイント:

  • CombineLatestStream.combine2で2つのStreamを結合
  • アプリ情報の取得とユーザー認証状態の監視を同時に行う
  • どちらかが更新されると自動的に新しいSettingオブジェクトが生成される

例2: クイズ詳細画面での複雑な使用

より複雑な例として、クイズの詳細情報、ユーザーの回答状態、いいね状態を組み合わせる例です。

結合されるエンティティ:

class QuizDetail {
  final Quiz quiz;                           // クイズの基本情報
  final UserQuizInteraction userQuizInteraction; // ユーザーとの関係性
}

class UserQuizInteraction {
  final bool isLogin;        // ログイン状態
  final int? selectAnswer;   // ユーザーが選択した回答(未回答の場合null)
  final bool hasGood;        // いいねしているかどうか
}

なぜ結合が必要?
クイズ詳細画面では以下の情報を同時に表示・制御する必要があります:

  • クイズの内容と選択肢
  • ユーザーが既に回答済みかどうか(回答済みなら結果表示、未回答なら回答可能)
  • いいねボタンの状態(既にいいね済みかどうか)
  • ログイン状態(未ログインなら回答・いいね不可)

これらの情報は異なるFirebaseコレクション(quizzes, answers, goods)と認証状態から取得するため、RxDartで結合する必要があります。

class QuizDetailRepositoryImpl extends QuizDetailRepository {
  final _quizDataStore = QuizFirebaseStore();
  final _answerDataStore = AnswerFirebaseStore();
  final _userDataStore = FirebaseAuthStore();
  final _goodDataStore = GoodFirebaseStore();

  @override
  Stream<QuizDetail> fetch(String quizId) {
    // クイズデータの取得
    final quiz = _quizDataStore
        .fetchWhereByQuizId(quizId)
        .asyncMap((quizDto) => QuizMapper.transform(quizDto!));
    
    // ユーザー関連データの取得
    final userStream = _userDataStore
        .listenToUserChanges()
        .asyncExpand<UserQuizInteraction>((user) {
      if (user != null) {
        final userId = user.uid;
        final goodStream = _goodDataStore.fetchMyGood(quizId, userId);
        final answerStream = _answerDataStore.fetchMyAnswers(quizId, userId);

        return CombineLatestStream.combine2(
          goodStream,
          answerStream,
          (good, answer) => UserQuizInteraction(
              isLogin: true,
              selectAnswer: answer?.answer,
              hasGood: good != null)
        );
      } else {
        return Stream.value(const UserQuizInteraction(
            isLogin: false, selectAnswer: null, hasGood: false));
      }
    });

    // 最終的にクイズデータとユーザーデータを結合
    return CombineLatestStream.combine2(
        quiz,
        userStream,
        (quiz, userQuizInteraction) =>
            QuizDetail(quiz: quiz, userQuizInteraction: userQuizInteraction));
  }
}

ポイント:

  • asyncMapでFirestoreのデータを非同期でマッピング
  • asyncExpandでユーザー状態に応じた条件分岐処理
  • ネストしたCombineLatestStreamで複数のFirebaseコレクションを結合
  • ログイン状態に応じて異なるStreamを返す

StreamGroupという選択肢

現在はpackage:asyncに含まれるStreamGroupを使って、よりシンプルにStreamを結合することもできます。今回のケースであれば、こちらを使うのもありです。

import 'package:async/async.dart';

class SettingRepositoryImpl extends SettingRepository {
  final _userDataStore = FirebaseAuthStore();

  @override
  Stream<Setting> fetch() {
    final packageInfoStream = Stream.fromFuture(PackageInfo.fromPlatform());
    final userStream = _userDataStore.listenToUserChanges();

    final group = StreamGroup.merge<dynamic>([
      packageInfoStream.map((info) => {'type': 'package', 'data': info}),
      userStream.map((user) => {'type': 'user', 'data': user}),
    ]);

    PackageInfo? lastPackageInfo;
    bool? lastIsLogin;

    return group.asyncMap((event) {
      // イベントのタイプに応じて最新値を更新
      if (event['type'] == 'package') {
        lastPackageInfo = event['data'] as PackageInfo;
      } else if (event['type'] == 'user') {
        lastIsLogin = event['data'] != null;
      }

      // 両方のデータが揃ったらSettingを返す
      if (lastPackageInfo != null && lastIsLogin != null) {
        return Setting(
          packageInfo: lastPackageInfo!,
          isLogin: lastIsLogin!,
        );
      }

      // データが揃っていない場合はnullを返してスキップ
      return null;
    }).where((setting) => setting != null).cast<Setting>();
  }
}

StreamGroupのメリット:

  • 追加ライブラリが不要(package:asyncは標準ライブラリ)
  • シンプルな結合には十分
  • 軽量

RxDartのメリット:

  • 豊富な演算子と機能
  • リアクティブプログラミングの標準パターン
  • 複雑なデータ変換に対応

シンプルな結合だけならStreamGroup、より複雑なリアクティブ処理が必要ならRxDartという使い分けがおすすめです。

RxDartの他の機能

今回紹介した機能以外にも、RxDartには多くの便利な機能があります。用途別に主要なものを紹介します。

Subject系(データの発行と購読):
BehaviorSubjectPublishSubjectReplaySubjectなど、データの状態管理に使用します。

時間制御系:
debounceTime(連続入力の制御)、throttleTime(実行頻度の制限)、delay(遅延実行)など、時間に関する制御を行います。

データ変換系:
switchMapflatMapconcatMapなど、Streamのデータを別のStreamに変換する際に使用します。

結合・フィルタリング系:
merge(複数Streamの統合)、zip(順序を保った結合)、distinctUntilChanged(重複データの除去)など、データの結合や絞り込みに使用します。

これらの機能を組み合わせることで、より複雑で効率的なリアクティブプログラミングが可能になります。

まとめ

RxDartを使うことで、複数のデータソースを効率的に組み合わせることができます。特にFirebaseのようなリアルタイムデータベースと組み合わせると、ユーザーの操作に応じてリアルタイムに画面が更新される、よりリッチなアプリケーションを作ることができます。

今回紹介したCombineLatestStreamasyncMapasyncExpandは、RxDartの基本的な機能の一部です。これらをマスターすることで、Flutterアプリケーションでのデータ管理がより簡潔で保守しやすくなります。

コメント

タイトルとURLをコピーしました