🚥

FutureSignalを使ってみた

2024/11/08に公開

📕Overview

FutureBuilderのような機能が、Signals.dartにもある。今回は、Cloud FirestoreからQuerySnapshotで一度だけデータを取得して状態が変化したら、画面を更新してデータの追加・削除をする機能も再現してみました。

https://youtu.be/N4QX3rjb4Kc

まずは、FutureSignalについて解説しておきます。
https://dartsignals.dev/async/future/

FutureSignal

Future signals can be created by extension or method.

フューチャーシグナルは、エクステンションやメソッドで作ることができる。

futureSignal

final s = futureSignal(() async => 1);

toSignal()

final s = Future(() => 1).toSignal();

.value, .peek()

Returns AsyncState<T> for the value and can handle the various states.

The value getter returns the value of the future if it completed successfully.

.peek() can also be used to not subscribe in an effect

値の AsyncState<T> を返し、様々な状態を扱うことができる。

valueゲッターは、正常に完了した場合、futureの値を返します。

.peek() は、エフェクトを購読しない場合にも使用できます。

final s = futureSignal(() => Future(() => 1));
final value = s.value.value; // 1 or null

.reset()

The reset method resets the future to its initial state to recall on the next evaluation.

リセット・メソッドは、次の評価で呼び出すために、未来を初期状態にリセットする。

final s = futureSignal(() => Future(() => 1));
s.reset();

.refresh()

Refresh the future value by setting isLoading to true, but maintain the current state (AsyncData, AsyncLoading, AsyncError).

isLoadingをtrueに設定することで未来の値を更新するが、現在の状態は維持する(AsyncData、AsyncLoading、AsyncError)。

.reload()

Reload the future value by setting the state to AsyncLoading and pass in the value or error as data.

状態を AsyncLoading に設定して未来の値をリロードし、値またはエラーをデータとして渡します。

final s = futureSignal(() => Future(() => 1));
s.reload();
print(s.value is AsyncLoading); // true

Dependencies

By default the callback will be called once and the future will be cached unless a signal is read in the callback.

デフォルトでは、コールバックは一度だけ呼び出され、コールバック内でシグナルが読み込まれない限り、未来はキャッシュされる。

final count = signal(0);
final s = futureSignal(() async => count.value);

await s.future; // 0
count.value = 1;
await s.future; // 1

If there are signals that need to be tracked across an async gap then use the dependencies when creating the futureSignal to reset every time any signal in the dependency array changes.

非同期のギャップを越えて追跡する必要があるシグナルがある場合、依存関係の配列内のシグナルが変更されるたびにリセットするために、futureSignalを作成するときに依存関係を使用します。

final count = signal(0);
final s = futureSignal(
    () async => count.value,
    dependencies: [count],
);
s.value; // state with count 0
count.value = 1; // resets the future
s.value; // state with count 1

🧷summary

Cloud Firestoreからデータを取得するときは、.get*()メソッドを使用します。データは、QuerySnapshot型で返されます。データを1度だけ全て取得します。.doc().get*()だと、特定のユーザーの出ただけ取得するDocumentSnapShotが返される。

UIに取得したデータを表示するときは、watchメソッドを使用します。

// リフレッシュトリガー用のシグナル
    final refreshTrigger = signal(0);

    // futureSignalの作成(リフレッシュトリガーを監視)
    final usersSignal = futureSignal(
          () async {
        // リフレッシュトリガーの値を監視(値が変わるたびに再実行)
        refreshTrigger.value;
        return FirebaseFirestore.instance.collection('users').get();
      },
    );

    // リフレッシュ関数
    void refresh() {
      refreshTrigger.value++; // トリガーの値を変更してfutureSignalを再実行
    }

こちらが全体のコードです。アプリ開発で使うならもう少し改良が必要と思いますね。割と簡単に作ってある。

全体のコード
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

import 'firebase_options.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context) {
    // リフレッシュトリガー用のシグナル
    final refreshTrigger = signal(0);

    // futureSignalの作成(リフレッシュトリガーを監視)
    final usersSignal = futureSignal(
          () async {
        // リフレッシュトリガーの値を監視(値が変わるたびに再実行)
        refreshTrigger.value;
        return FirebaseFirestore.instance.collection('users').get();
      },
    );

    // リフレッシュ関数
    void refresh() {
      refreshTrigger.value++; // トリガーの値を変更してfutureSignalを再実行
    }

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('FutureSignal'),
        actions: [
          // 更新ボタン
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: refresh,
          ),
        ],
      ),
      body: Watch((context) {
        final state = usersSignal.value;

        if (state.hasError) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('Something went wrong'),
                ElevatedButton(
                  onPressed: refresh,
                  child: const Text('Retry'),
                ),
              ],
            ),
          );
        }

        if (state.isLoading) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }

        final snapshot = state.value;
        if (snapshot == null || snapshot.docs.isEmpty) {
          return const Center(
            child: Text('No data available'),
          );
        }

        return ListView(
          children: snapshot.docs.map((document) {
            final data = document.data();
            return ListTile(
              title: Text(data['name']),
              trailing: IconButton(
                icon: const Icon(Icons.delete),
                onPressed: () async {
                  await document.reference.delete();
                  refresh(); // データの再取得
                },
              ),
            );
          }).toList(),
        );
      }),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          await FirebaseFirestore.instance.collection('users').add({
            'name': 'New User ${DateTime.now()}',
            'createdAt': FieldValue.serverTimestamp(),
          });
          refresh(); // データの再取得
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

🧑‍🎓thoughts

今回は、FutureSignalを使ってみました。FutureBuilderと比較すると簡潔に書けているように思える。FutureBuilderを使うと以下のようにコードを書く。

https://firebase.flutter.dev/docs/firestore/usage

class GetUserName extends StatelessWidget {
  final String documentId;

  GetUserName(this.documentId);

  
  Widget build(BuildContext context) {
    CollectionReference users = FirebaseFirestore.instance.collection('users');

    return FutureBuilder<DocumentSnapshot>(
      future: users.doc(documentId).get(),
      builder:
          (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {

        if (snapshot.hasError) {
          return Text("Something went wrong");
        }

        if (snapshot.hasData && !snapshot.data!.exists) {
          return Text("Document does not exist");
        }

        if (snapshot.connectionState == ConnectionState.done) {
          Map<String, dynamic> data = snapshot.data!.data() as Map<String, dynamic>;
          return Text("Full Name: ${data['full_name']} ${data['last_name']}");
        }

        return Text("loading");
      },
    );
  }
}

パッケージを使わなくてもAPIやCloud Firestoreからデータを取得することができるが、UI State(画面の状態)が変化したときに切り替えるには、StatefulWidgetでsetState()メソッドを実行する必要があるので、使いすぎるとアプリが固まったり画面がチラつくので、できればRiverpodflutter_hooksを使うことが今ままでは、一般的であったが、Signalsを使えば、簡潔な処理でロジックを実装することができます。

とはいえ、Riverpodでないと難しい場面やStatefulWidgetが必要な場面もあるので、使い分ける必要はありますね。

Jboy王国メディア

Discussion