FutureSignalを使ってみた
📕Overview
FutureBuilderのような機能が、Signals.dartにもある。今回は、Cloud FirestoreからQuerySnapshot
で一度だけデータを取得して状態が変化したら、画面を更新してデータの追加・削除をする機能も再現してみました。
まずは、FutureSignalについて解説しておきます。
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
を使うと以下のようにコードを書く。
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()
メソッドを実行する必要があるので、使いすぎるとアプリが固まったり画面がチラつくので、できればRiverpod
やflutter_hooks
を使うことが今ままでは、一般的であったが、Signalsを使えば、簡潔な処理でロジックを実装することができます。
とはいえ、Riverpod
でないと難しい場面やStatefulWidgetが必要な場面もあるので、使い分ける必要はありますね。
Discussion