Chapter 07

FutureProviderの使い方

JboyHashimoto
JboyHashimoto
2023.02.27に更新
このチャプターの目次

https://docs-v2.riverpod.dev/docs/providers/future_provider
FutureProviderはProviderと同等ですが、非同期コードのためのものです。

FutureProviderは、通常、次のような用途に使われます。

非同期オペレーション(ネットワーク・リクエストなど)の実行とキャッシュ
非同期操作のエラーやロードの状態をうまく処理する。
複数の非同期な値を別の値に結合する。
FutureProviderは、ref.watchと組み合わされると、多くの利益を得ます。この組み合わせは、いくつかの変数が変更されたときに、いくつかのデータの自動的な再取得を可能にし、常に最新の値を持つことを保証します。

インフォメーション
FutureProviderは、ユーザーとの対話の後に、計算を直接修正する方法を提供しません。これは、単純なユースケースを解決するために設計されています。
より高度なシナリオのために、StateNotifierProviderの使用を検討してください。


使用するユースケース

APIとの通信以外には、shared_preferencesと組み合わせる例があります。
非同期処理が必要なパッケージですので、StatefulWidgetで使う場合だと、async awaitつけたメソッドとinitStateが必要な場面がありますが、RiverpodのFutureProviderを使用すれば、メソッドを定義しなくも呼び出して、画面に保存したデータを表示するのに使うことができます。
https://pub.dev/packages/shared_preferences

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

// 数値を表示するプロバイダー
final countStateProvider = StateProvider<int>((ref) => 0);
// SharedPreferencesは、非同期処理が必要なので、FutureProviderを使用する
// SharedPreferencesから保存されたデータを表示する
final countFutureProvider = FutureProvider<int?>((ref) async {
  final pref = await SharedPreferences.getInstance();
  return pref.getInt('counter');
});

class TemplatePage extends ConsumerWidget {
  const TemplatePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    // SharedPreferencesを呼び出す
    final future = ref.watch(countFutureProvider);

    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
              onPressed: () async {
	      // 保存したデータを削除する
                final SharedPreferences prefs =
                    await SharedPreferences.getInstance();
                final success = await prefs.remove('counter');
              },
              icon: const Icon(Icons.delete))
        ],
        title: const Text('FutureProvider'),
      ),
      // 数値をSharedPreferencesに保存するロジック
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          final counter = ref.read(countStateProvider.notifier).state++;
          final SharedPreferences prefs = await SharedPreferences.getInstance();
          final set = prefs.setInt('counter', counter);
          final int? sum = prefs.getInt('counter');
          print(sum); // 保存された値をログに表示
        },
        child: const Icon(Icons.add),
      ),
      body: Center(
        // FutureProviderのデータを取得する
        child: future.when(
          error: (err, _) => Text(err.toString()), //エラー時
          loading: () => const CircularProgressIndicator(), //読み込み時
          data: (data) => Text(data.toString()), // 保存された値を表示
        ),
      ),
    );
  }
}

今回のユースケースだと、以下のようにFutureProviderを定義して、counterというキーを持っているデータを非同期処理で取得して画面に表示することができます。

// SharedPreferencesは、非同期処理が必要なので、FutureProviderを使用する
// SharedPreferencesから保存されたデータを表示する
final countFutureProvider = FutureProvider<int?>((ref) async {
  final pref = await SharedPreferences.getInstance();
  return pref.getInt('counter');
});

他には、最近趣味でやっていたSupabaseのデータを取得するのに使ったり、Firestoreの特定のドキュメントIDを取得して、コレクションのデータを取得する方法があります。

Supabaseのデータを一度だけ取得する
FutureProviderを使用して、一度だけデータを取得して、画面に表示するサンプル。

過去に書いた記事
https://zenn.dev/flutteruniv_dev/articles/e75597a1cbef19

// Supabaseから一度だけデータを取得するプロバイダー.
final notesFutureProvider = FutureProvider<List<Map<String, dynamic>>>((ref) async {
  return await Supabase.instance.client
      .from('notes')
      .select<List<Map<String, dynamic>>>();
});

画面に取得したデータを描画するコード

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_notes/presentation/utils/notes_provider.dart';

class NotesFuture extends ConsumerWidget {
  const NotesFuture({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // AsyncValue型が帰ってくるので、こちら指定する.
    AsyncValue config = ref.watch(notesFutureProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Future'),
      ),
      body: config.when(
        loading: () => const CircularProgressIndicator(),
        error: (err, stack) => Text('Error: $err'),
        data: (notes) {
          return ListView.builder(
            itemCount: notes.length,// なぜかコードの保管機能で出てこない?
            itemBuilder: (BuildContext context, int index) {
              final note = notes[index];
              return ListTile(
                title: Text(note['body']),
              );
            },
          );
        },
      ),
    );
  }
}

Firestoreから特定のデータを取得する
ハードコーディングで書いてますが、本来でしたらuidを指定します。

過去に書いた記事
https://zenn.dev/flutteruniv_dev/articles/95d88cd4f62f71

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// FutureProviderで単一のドキュメントを取得するコード.
final getProvider = FutureProvider((ref) async {
  final id = 'rkoqDMhR6BynStA6AyXI';
  final docRef = FirebaseFirestore.instance.collection('barchart');
  final fetch = await docRef.doc(id).get();
  return fetch;
});

過去に書いた棒グラフの記事のソースコード
画面に取得したデータを描画して特定のuidのデータをグラフで可視化します。

import 'dart:async';
import 'dart:ffi';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:timelineview_app/provider.dart';

class LineChartSample extends ConsumerWidget {
  const LineChartSample({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    // 変数configはAsyncValue型になる.
    // finalをAsyncValueに変更.
    AsyncValue config = ref.watch(getProvider);
    // 棒グラフの棒の横幅
    const double barWidth = 20.0;

    // グラフタイトルのラベル書式
    final TextStyle _labelStyle =
        const TextStyle(fontSize: 16, fontWeight: FontWeight.w800);
    return Scaffold(
        appBar: AppBar(
            centerTitle: true,
            automaticallyImplyLeading: false,
            title: const Text(
              'ブログの棒グラフをFutureProviderで表示',
              style: TextStyle(color: Colors.white),
            )),
        body: config.when(
          // FutureBuilderから、変数.whenに変更.
          loading: () => const CircularProgressIndicator(),
          error: (err, stack) => Text('Error: $err'),
          data: (config) {
            return Column(
              // FutureBuilderの中に、Widgetを組み込む.
              children: [
                const SizedBox(height: 50),
                AspectRatio(
                    // ここから棒グラフのWidget.
                    aspectRatio: 2,
                    child: BarChart(
                      BarChartData(
                          maxY: 20,
                          //Y軸の最大値を指定.

                          // 棒グラフの位置
                          alignment: BarChartAlignment.spaceEvenly,

                          // 棒グラフタッチ時の動作設定
                          barTouchData: BarTouchData(
                              touchTooltipData: BarTouchTooltipData(
                            tooltipBgColor: Colors.black,
                          )),

                          // グラフタイトルのパラメータ
                          titlesData: FlTitlesData(
                            show: true,
                            //右タイトル
                            rightTitles: AxisTitles(
                                sideTitles: SideTitles(showTitles: false)),
                            //上タイトル
                            topTitles: AxisTitles(
                              sideTitles: SideTitles(showTitles: false),
                            ),
                            //下タイトル
                            bottomTitles: AxisTitles(
                              axisNameWidget: Text(
                                'ブログ運営年月',
                                style: _labelStyle,
                              ),
                              axisNameSize: 40,
                              sideTitles: SideTitles(
                                showTitles: true,
                              ),
                            ),
                            // 左タイトル
                            leftTitles: AxisTitles(
                              axisNameWidget: Container(
                                  alignment: Alignment.topCenter,
                                  child: Text(
                                    '記事数',
                                    style: _labelStyle,
                                  )),
                              axisNameSize: 25,
                              sideTitles: SideTitles(
                                showTitles: true,
                              ),
                            ),
                          ),

                          // 外枠表の線を表示/非表示
                          borderData: FlBorderData(
                              border: const Border(
                            top: BorderSide.none,
                            right: BorderSide.none,
                            left: BorderSide(width: 1),
                            bottom: BorderSide(width: 1),
                          )),

                          // barGroups: 棒グラフのグループを表す
                          // BarChartGroupData: 棒グラフの1つのグループを表す
                          // X : 横軸
                          // barRods: 棒グラフのデータを含むBarRodクラスのリスト
                          // BarChartRodData
                          // toY : 高さ
                          // width : 棒の幅
                          /// [DartのListを使って、値をグラフに入れる]
                          barGroups: [
                            BarChartGroupData(x: 2, barRods: [
                              BarChartRodData(
                                  toY: config['blogLog'][0].toDouble(), // 0
                                  width: barWidth),
                            ]),
                            BarChartGroupData(x: 3, barRods: [
                              BarChartRodData(
                                  toY: config['blogLog'][1].toDouble(), // 1
                                  width: barWidth),
                            ]),
                            BarChartGroupData(x: 4, barRods: [
                              BarChartRodData(
                                  toY: config['blogLog'][2].toDouble(), // 2
                                  width: barWidth),
                            ]),
                            BarChartGroupData(x: 5, barRods: [
                              BarChartRodData(
                                  toY: config['blogLog'][3].toDouble(), // 3
                                  width: barWidth),
                            ]),
                            BarChartGroupData(x: 6, barRods: [
                              BarChartRodData(
                                  toY: config['blogLog'][4].toDouble(), // 4
                                  width: barWidth),
                            ]),
                            BarChartGroupData(x: 7, barRods: [
                              BarChartRodData(
                                  toY: config['blogLog'][5].toDouble(), // 5
                                  width: barWidth),
                            ]),
                            BarChartGroupData(x: 8, barRods: [
                              BarChartRodData(
                                  toY: config['blogLog'][6].toDouble(), // 6
                                  width: barWidth),
                            ]),
                          ]),
                    )),
              ],
            );
          },
        ));
  }
}