【Flutter】Futureでデータ再取得!リロードを実装

対象者

  • Flutterを使ったアプリ開発に取り組んでいる方
  • 非同期処理の扱い方に不安を感じている方
  • 効率的なデータ取得と表示の方法を学びたい方

はじめに

今回の検証では、FlutterにおけるFutureの効果的な使い方に焦点を当てています。これまではRiverpodを用いてデータを取得し、必要に応じて無効化して再取得するプロセスは実施したことがありました。そこで、今回はFlutterの標準機能とFutureBuilderを使用して同様の動作を実現する方法を検討しました。

検討の結果

FlutterにおけるFutureBuilderの挙動に関して以下の3点が明らかになりました:

  1. Future型で再度データを取得したい場合は、FutureをsetStateで上書きする: Future型を他の方と同様に setStateを利用して上書きすると、FutureBuilderがデータ取得の動作をしてくれます。

  2. Future型に新たなデータを設定しても、スナップショットのデータは保持されます: 新しいFutureが完了するまで、前回のFutureで取得したデータが引き続き利用可能であることを意味します。

  3. Futureがnull、または初回のデータを取得する前の挙動: 最初のFutureがデータをまだ取得していない状態では、snapshot.hasDatafalseとなります。しかし、一度Futureを設定し、データを取得した後は、hasDatatrueになり、以降は新しいデータを取得している最中でも前回のデータが利用できます。

  4. ConnectionStateがwaitingの間の挙動: 新しいデータを取得している間、ConnectionStateはwaitingとなります。この間も前回のFutureで取得したデータが表示され続け、新しいFutureの取得が完了すると、waiting状態が解消され、最新のデータが表示されるようになります。

この4点の理解は、Flutterアプリケーションにおけるデータの取得と表示の管理をより効果的に行うために重要です。

状態 hasData value connectionState
Future設定前 false null none
Futureを設定 false null waiting
Futureの値を初取得 true 初取得値 done
Futureを再設定 true 初取得値 waiting
Futureの値を再取得 true 再取得値 done

使用できるユースケース

この実験を通じて、FutureBuilderを用いたデータ取得と表示の管理に関して重要なポイントがわかりました。この知見を活かすことで、以下のようなユースケースが考えられます:

  1. Pull-to-Refreshの実装: ユーザーが画面を引っ張って更新する際、ConnectionStateがwaitingである間にCircularProgressIndicatorや想定される次のデータ用のダミーWidgeなどtを表示し、データ取得中であることを示します。これにより、ユーザーに対してフィードバックを与えることができます。

  2. 新旧データのスムーズな切り替え: 以前取得したデータを表示している状態から、新しいデータの取得が完了したら、アニメーションを伴ってスムーズに更新することができます。これにより、ユーザーは新しい情報を効果的に受け取ることができ、アプリケーションの使い勝手が向上します。

これらのユースケースは、アプリケーションにおけるユーザー体験の向上に大きく寄与する可能性があります。FutureBuilderを適切に活用することで、データの取得と表示をより効果的に管理することが可能になります。

まとめ

この記事を通じてFlutterのFutureBuilderの使い方を学びました。非同期処理を扱う際のポイントとして、Future型の変数を活用し、データ取得中に進行中を表示する方法と以前のデータを取得できることを理解しました。また、新しいデータが取得されるたびに画面が更新されることも確認しました。これらの知識を活かすことで、ユーザーにとってより良い体験を提供するアプリケーションの開発が可能になります。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<int>? _futureInt;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            FilledButton(
                onPressed: () async {
                  setState(() {
                    _futureInt = _futureInt == null
                        ? getNextValue(Future.value(0))
                        : getNextValue(_futureInt!);
                  });
                },
                child: Text('push')),
            FutureBuilder(
              future: _futureInt,
              builder: (context, snapshot) {
                print('status: ${snapshot.connectionState}');

                if (snapshot.hasError) {
                  return Text('error');
                }
                return Column(
                  children: [
                    Text(snapshot.data?.toString() ?? 'No Data'),
                    Text('state: ${snapshot.connectionState}'),
                    if (snapshot.connectionState == ConnectionState.waiting)
                      const CircularProgressIndicator(),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }

  Future<int> getNextValue(Future<int> currentValue) async {
    int nextValue = await currentValue + 1;
    print('before delay');
    await Future.delayed(const Duration(milliseconds: 1000));
    print('after delay');
    return nextValue;
  }
}

上記のコードは、FlutterでFutureBuilderを用いたデータ取得と表示の例を示しています。以下は、このコードについての解説です:

  1. MyHomePageクラス: このクラスは、アプリのホームページを表すウィジェットです。ここで、Future<int>型のプライベート変数_futureIntを定義しています。この変数は、非同期で整数値を取得するために使用されます。

  2. ボタンの定義: 画面には「push」というラベルのボタンがあります。このボタンが押されると、_futureIntに新たなFuture<int>が割り当てられます。ここで、getNextValue関数を呼び出しています。この関数は、現在の_futureIntの値に1を加えた値を非同期で返します。

  3. FutureBuilderの使用: FutureBuilderウィジェットは、_futureIntの状態に基づいて異なるウィジェットを表示します。snapshotオブジェクトを使用して、Futureの状態をチェックします。

    • データがない場合: snapshot.datanullの場合、'No Data'と表示されます。
    • データ取得中: snapshot.connectionStateConnectionState.waitingの場合、CircularProgressIndicatorを表示してデータ取得中であることを示します。
    • データが取得された場合: 取得したデータ(整数値)を表示します。
  4. getNextValue関数: この関数は、現在の値に1を加えた後、1秒間の遅延を挟んでその値を返します。これにより、非同期処理の動作をシミュレートしています。

このコード例は、FutureBuilderを使用して非同期データを効果的に扱う方法を示しています。特に、データ取得中にプログレスインディケーターを表示することで、ユーザーに対してフィードバックを提供することができます。また、データが更新されるたびに新たなFutureが割り当てられ、結果が反映されることがわかります。