🫥

FutureBuilderを使ってみる

2023/02/09に公開

どんな仕組みか?

こちらが公式なんですけど、サンプルコードが分かりにくいそうです。APIと通信してるようには見えない...
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

日本語に翻訳してみた

Futureとのインタラクションの最新スナップショットに基づいて、自己構築するWidget。

Futureは、State.initState、State.didUpdateWidget、またはState.didChangeDependenciesの間など、以前に取得されている必要があります。FutureBuilderを構築するときに、State.buildまたはStatelessWidget.buildメソッド呼び出し中に作成されてはなりません。FutureBuilderと同時に作成された場合、FutureBuilderの親が再構築されるたびに、非同期タスクが再起動されます。

一般的なガイドラインは、すべてのビルド・メソッドが毎フレーム呼び出される可能性があると想定し、省略された呼び出しは最適化として扱うことである。
なので私が作りたいプログラムがあったので、作ってみました。

タイミング

Widgetの再構築は、State.setStateを使用して未来の完了によってスケジュールされますが、それ以外は未来のタイミングから切り離されます。builderコールバックはFlutterパイプラインの裁量で呼び出され、未来との対話を表すスナップショットのタイミングに依存したサブシーケンスを受け取ることになります。

この副作用として、FutureBuilderに新しいがすでに完成した未来を提供すると、1つのフレームがConnectionState.waitingの状態になります。これは、Futureがすでに完了していることを同期的に判断する方法がないためです。

ビルダー契約
initialData が NULL であると仮定して、データが正常に完了した future に対して、ビルダーは以下のスナップショットの両方または後者のみを伴って呼び出されます。

AsyncSnapshot<String>.withData(ConnectionState.waiting, null)
AsyncSnapshot<String>.withData(ConnectionState.done, 'あるデータ')
その同じ未来が代わりにエラーで完了した場合、ビルダーは、両方または後者のどちらかのみで呼び出されるでしょう。

AsyncSnapshot<String>.withData(ConnectionState.waiting, null)
AsyncSnapshot<String>.withError(ConnectionState.done, '何らかのエラー', someStackTrace)
initialDataを指定することで、スナップショットの初期データを制御することができます。この機能は、future が完了する前にビルダーが呼び出された場合、スナップショットがデフォルトの null 値ではなく、選択したデータを運ぶことを保証するために使用されます。

スナップショットのデータとエラーフィールドは、接続状態フィールドが waiting から done に遷移するときにのみ変化し、FutureBuilder の設定を別の future に変更しても、それらは保持されます。古い未来が上記のようにデータですでに正常に完了している場合、新しい未来に設定を変更すると、フォームのスナップショットのペアが発生します。

AsyncSnapshot<String>.withData(ConnectionState.none, '最初の未来のデータ')
AsyncSnapshot<String>.withData(ConnectionState.waiting, '2番目の未来のデータ')
一般に、後者は新しい未来が非NULLのときのみ生成され、前者は古い未来が非NULLのときのみ生成されます。

FutureBuilder は future?.asStream() で設定された StreamBuilder と同じように動作しますが、ストリームの実装方法によっては、後者で ConnectionState.active のスナップショットが表示される場合があることを除けば、同じように動作します。

StreamBuilderと何が違う?

リアルタイムにデータが更新されないので、データが変化しても画面が変化しません!
なので、画面をまた読み込まないとデータが変化したのが分かりません。

作成したサンプル

まずは、ダミーのデータを作成しておいてください。

変更前の画面
佐藤さんという方がいますね。こちらのデータをFirebaseのコンソールを操作して変えてみます。ですが、画面は変化しません!
でもアプリを更新すると、画面が変化します。宮崎駿さんになっていますね。

変更前

変更後


サンプルコード

utilsディレクトリを作成して、user_data.dartを作成してください。そちらにFirestoreから全てのデータを取得するメソッドを定義したクラスを作成します。
QuerySnapshotですけど、調べてみると、こんな風に描かれていました。
Contains the results of a query. It can contain zero or more [DocumentSnapshot] objects.

翻訳
クエリの結果を格納する。0個以上の[DocumentSnapshot]オブジェクトを含むことができる。
つまり、存在するデータの数だけ取得することができるということでしょうね。

utils/user_data.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';

// Firestoreの値を取得するメソッドを使えるクラス.
class UserData {
  // Firestoreから全てのユーザーデータを取得する.
  Future<QuerySnapshot<Object?>> getdata() async {
    final store = await FirebaseFirestore.instance;
    CollectionReference users = store.collection('user');
    return users.get();
  }
}

画面にデータを表示するページ

uiディレクトリを作成して、get_user_name.dartを作成してください。
こちらのページで、一度だけFirestoreのデータを取得して、取得したデータの数だけ画面に表示します。

ui/get_user_name.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:review_app/utils/user_data.dart';

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

  
  Widget build(BuildContext context) {
    final userData = UserData();// UserDataクラスをインスタンス化する.

    return Scaffold(
      appBar: AppBar(
        title: Text('GetUserData'),
      ),
      // DocumentSnapshotから<QuerySnapshot<Object?>>に書き換えないと
      // メソッドが使えないので変更する!
      body: FutureBuilder<QuerySnapshot<Object?>>(
        future: userData.getdata(),// UserDataクラスのメソッドを使用する.
        builder: (context, snapshot) {
          // connectionStateは、非同期計算への接続の現在の状態
          if (snapshot.connectionState == ConnectionState.done) {
            // ListView.builderで数えるListを定義する.
            List<QueryDocumentSnapshot<Object?>> listAllDocs =
                snapshot.data!.docs;
            return ListView.builder(
              itemCount: listAllDocs.length,// Listの中のデータを数える.
              itemBuilder: (context, index) {// 画面にデータの数だけ描画をする.
              // as Map<String, dynamic>にデータを変換しておく.
                final data = listAllDocs[index].data() as Map<String, dynamic>;
                return ListTile(
                  title: Text(data["name"]),// nameフィールドを表示.
                  subtitle: Text(data["phone"]),// phoneフィールドを表示.
                );
              },
            );
          }

          return const Center(
            child: CircularProgressIndicator(),// エラーだったらローディングの処理をする.
          );
        },
      ),
    );
  }
}

アプリを実行するコード

main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:review_app/firebase_options.dart';
import 'package:review_app/ui/get_user_name.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: GetUserName(), // FireStoreのドキュメントIDを貼り付ける.
    );
  }
}

最後に

Udemyの過去の動画を見て

<QuerySnapshot<Object?>>

なる書き方を見つけましが、これが正しい書き方なのかは疑問な気がします。インドネシア語の教材で勉強したのだから、驚きでしょう!
もしこの書き方が変だという方がいたら、ご意見をお願いいたします。

Discussion