Flutter: riverpodのFutureProviderのキホン

f:id:branu_techblog:20220208104149p:plain

はじめに

こんにちは、BRANU開発部でアプリエンジニアを担当している薩間です。

Flutterで非同期処理で取得したデータを使ってWidgetを描画する際、FutureBuilderを使用するとローディングやエラーなどのハンドリング処理をゴリゴリ記述する必要があり、非常に面倒です。

FututeProviderを使用すると簡単に実装できますので、今回はriverpodのFutureProviderの使い方について解説していきたいと思います。

環境

  • macOS Monterey 12.1
  • Flutter 2.5.1
  • Dart 2.14.2

インストール

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  hooks_riverpod: ^1.0.3

非同期処理

FutureProviderから呼び出す非同期処理を実装します。

今回はAccountクラスをList形式で返す処理を実装します。

account.dart

class Account {
  Account({
    required this.id,
    required this.name,
  });

  final String id;
  final String name;
}

account_repository.dart

class AccountRepository {
  static Future<List<Account>> get() async {
    return [
      Account(
          id: '1',
          name: 'ユーザー1'
      ),
      Account(
          id: '2',
          name: 'ユーザー2'
      ),
      Account(
          id: '3',
          name: 'ユーザー3'
      ),
    ];
  }
}

FutureProvider

非同期処理を呼び出すFutureProiderを実装します。

List型のFutureProviderを用意し、引数に先ほど作成したAccountRepositoryのgetを呼び出すメソッドを渡します。

account_provider.dart

final accountProvider = FutureProvider<List<Account>>((_) async {
    return await AccountRepository.get();
});

Widget

FutureProviderから値を取得し、Widgetを描画する処理を実装します。

account_list_view.dart

class AccountListView extends HookConsumerWidget {
  const AccountListView({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<List<Account>> watcher = ref.watch(accountProvider);

    return watcher.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
      data: (accounts) {
        if (accounts.isEmpty) {
          return const Center(
            child: Text('アカウントが存在しません'),
          );
        }

        return ListView.separated(
          itemCount: accounts.length,
          itemBuilder: (context, index) {
            Account account = accounts[index];

                        return ListTile(
              title: Text(account.name),
            );
          },
          separatorBuilder: (_, __) => const Divider(
            height: 1,
          ),
        );
      },
    );
  }
}

build処理の冒頭でaccountProviderの値を取得します。

値の変更を検知する必要がない場合はread、ある場合はwatchで取得します。

(今回はのちの解説で必要になるためwatchを使用)

AsyncValue<List<Account>> watcher = ref.read(accountProvider);

    または

AsyncValue<List<Account>> watcher = ref.watch(accountProvider);

FutureProviderの戻り値はAsyncValueオブジェクトとして取得され、こちらのオブジェクトのwhenメソッドが非同期通信処理のハンドリングを可能にしてくれます。

whenにはloading, error, dataの3つの引数があり、それぞれ

  • loading ... ローディング中の場合の処理
  • error ... エラーが発生した場合の処理
  • data ... データを正常に取得できた場合の処理

を行うメソッドを渡してあげます。

今回はAsyncValueの結果を元にWidgetの描画を行いますので、それぞれの場合に表示したいWidgetを描画するメソッドを渡します。

リロードしたい場合

WidgetRefのrefreshメソッドに、リロードしたい FutureProviderを渡して呼び出すことでリロードを行うことができます。

ここでリロードされるFutureProviderをwatchで監視することによって、値の変更を検知しWidgetを再描画することができます。

account_list_view.dartのbuildメソッド

@override
Widget build(BuildContext context, WidgetRef ref) {
  AsyncValue<List<Account>> watcher = ref.watch(accountProvider);

    return watcher.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (accounts) {
      if (accounts.isEmpty) {
        return Center(
          child: Text('アカウントが存在しません'),
        );
      }

      return RefreshIndicator(
              onRefresh: () async {
                return await ref.refresh(accountProvider);
              },
              child: ListView.separated(
                itemCount: accounts.length,
                itemBuilder: (context, index) {
                  Account account = accounts[index];

                        return ListTile(
              title: Text(account.name),
            );
                },
                separatorBuilder: (_, __) => const Divider(
                  height: 1,
                ),
              ),
            );
    },
  );
}

FutureProviderに引数を渡したい場合

検索処理などを行うためにFutureProviderに引数を渡したい場合は、FutureProvider.familyを使用します。

例えばIDでの検索を行いたい場合は

  1. AccountRepositoryのgetメソッドでID検索ができるよう修正
class AccountRepository {
  static Future<List<Account>> get({
    required String id,
  }) async {
    List<Account> accounts = [
      Account(id: '1', name: 'ユーザー1'),
      Account(id: '2', name: 'ユーザー2'),
      Account(id: '3', name: 'ユーザー3'),
    ];
    return accounts.where((Account account) => account.id == id).toList();
  }
}
  1. accountProviderをFutureProvider.familyに変更し、idを渡せるよう修正
final accountProvider = FutureProvider.family<List<Account>, String>((_, id) async {
  return await AccountRepository.get(id: id);
});
  1. account_list_viewのaccountProviderの値を取得している箇所で、引数にIDを渡すよう修正
@override
  Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<List<Account>> watcher = ref.watch(accountProvider('1'));

    return watcher.when(

実際に開発を行う際は、accountProviderに渡す引数をTextFieldの値などにすることによって、FutureProviderでの検索を行うことができます。

FutureBuilderとの比較

FutureBuilderを使った場合とriverpod + FutureProviderを使った場合それぞれのbuildメソッドの内容を比較してみます。

FutureBuilder

@override
Widget build(BuildContext context, WidgetRef ref) {
  return FutureBuilder(
    future: AccountRepository.get(),
    builder: (BuildContext context, AsyncSnapshot<List<Account>> snapshot) {
      if (!snapshot.hasData) {
        return const CircularProgressIndicator();
      }
      if (snapshot.hasError) {
        return Text('Error: ${snapshot.error}');
      }

      return ListView.separated(
        itemCount: snapshot.data!.length,
        itemBuilder: (context, index) {
          Account account = snapshot.data![index];

          return ListTile(
            title: Text(account.name),
          );
        },
        separatorBuilder: (_, __) => const Divider(
          height: 1,
        ),
      );
    },
  );
}

riverpod + FutureProvider

@override
Widget build(BuildContext context, WidgetRef ref) {
  AsyncValue<List<Account>> watcher = ref.watch(accountProvider);

  return watcher.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (accounts) {
      if (accounts.isEmpty) {
        return const Center(
          child: Text('アカウントが存在しません'),
        );
      }

      return ListView.separated(
        itemCount: accounts.length,
        itemBuilder: (context, index) {
          Account account = accounts[index];

          return ListTile(
            title: Text(account.name),
          );
        },
        separatorBuilder: (_, __) => const Divider(
          height: 1,
        ),
      );
    },
  );
}

今回の場合ですとコードの総量にあまり差はないですが、whenの引数にそれぞれの状態の場合に描画するWidgetを明示的に記述できるため、FutureBuilderに比べてハンドリング処理が実装しやすいです。

またreload処理もFutureBuilderを使用すると少々複雑な実装をする必要がありますが、FutureProviderを使用すると簡単に実装することができます。

まとめ

今回は、Flutterでriverpod + FutureProviderの基本的な使い方について解説しました。

riverpodはキャッチアップが難しく慣れるまで時間がかかるかもしれませんが、FutureProviderの他にも開発効率をぶち上げる様々な機能がありますので、StatefulWidgetやProviderでの状態管理に慣れてきたらぜひ挑戦してみてください!

BRANUではFlutterエンジニアを積極的に募集しています!Flutterを使ったアプリ開発に興味がある方、建設業界向けのプロダクト開発に興味がある方はぜひご応募ください!

www.wantedly.com