0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】Riverpodの基礎知識メモ

Last updated at Posted at 2024-07-16

■用途

状態管理を実現するためのアーキテクチャ
状態管理は煩雑なため、大規模開発では以下のアーキテクチャを利用する

  • Bloc: Streamを活用したリアクティブな状態管理
  • Redux: ActionCreator(ビジネスロジック) -> Action(状態への操作) -> Reducer(状態更新=Store更新)と役割が一方通行で処理を追いやすい
  • Provider: 直感的な状態管理
  • Riverpod: Providerの強化版

■メリット

  • Provider = データの管理、提供者 / Consumer:データの使用者
    と役割が明確に分かれているので状態管理がしやすい
  • Cosumerはデータを監視することでProviderから自動でデータを受け取ることができる。
  • アプリの状態を簡単にモック化することができ、テストがしやすい

■導入手順

プラグインの追加
image.png

pubspec.yamlにriverpod関連パッケージを追記し、Pub getを実行

image.png

※↑(コードの自動生成が不要なら以下3行は不要)

  build_runner: ^2.4.6
  riverpod_generator: ^2.3.3
  riverpod_analyzer_utils: ^0.3.4

image.png

■各モジュールの役割

■Provider

<Provider>

  • 値を更新する必要のない定数などの受け渡しに使う
// Provider
final titleProvider = Provider<String>((ref) => 'sampleTitle');
// これと同じ
// final titleProvider = Provider<String>((ref) {
//   return 'sampleTitle';
// });

// 参照側(Consumer)
AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(ref.watch(titleProvider)),
),

<StateProvider>

  • 値を変更する変数などの受け渡しに使う
// Provider
final countProvider = StateProvider<int>((ref) => 0);

// 更新(Consumer)
floatingActionButton: FloatingActionButton(
onPressed: () => ref.watch(countProvider.notifier).state++,
tooltip: 'Increment',
child: const Icon(Icons.add),
)

// 参照(Consumer)
Text(
  ref.watch(countProvider).toString(),
  style: Theme.of(context).textTheme.headlineMedium,
)

<FutureProvider>

  • 非同期通信のロジックを実行するためのProvider

<FutureProviderFamily>

  • FutureProviderに引数を渡すためのProvider
    (引数を変えるたびにインスタンスが生成されるため非推奨)

<AutoDisposeFutureProviderFamily>

  • FutureProviderFamilyのリソースを自動で開放してくれるProvider

<riverpod_generatorパッケージについて>

特徴:riverpod_generatorを使うと、独自Providerを自動生成してくれる
利用例:AutoDisposeFutureProviderFamilyを使う場合、引数は1つしか渡せないが、複数の引数を利用するよう独自定義できる

<StreamProvider>

  • FirebaseStoreなどの更新を監視するために利用できる。
  • 他のプロバイダで ref.watch を通じてストリームを監視することができる。
  • AsyncValue により loading/error のステートを適切に処理することができる。
  • ストリームから出力された直近の値をキャッシュしてくれる(途中で監視を開始しても最新の値を取得することができる)。
  • StreamProvider をオーバーライドすることでテスト時のストリームを簡単にモックすることができる。

<StateNotifierProvider>
StateProvider/FutureProvider/StreamProvider全てのユースケースに対応できる
https://zenn.dev/3ta/articles/fb2329ba2ab1dd

■Consumer

<ConsumerWidget>

  • RiverpodでのStatelessWidgetに相当
  • buildの引数は
Widget build(BuildContext context, WidgetRef ref)
  • デメリット:変更があった際にConsumerWidget.buildが実行されてウィジェットツリーが再構築される

<ConsumerStatefulWidget>

  • RiverpodでのStatelessWidgetに相当
  • buildの引数は
// refはConsumerStatefulWidget内にrefが定義されているので、受け取らずにどこでも使える
Widget build(BuildContext context)

<Comsumer>

  • Providerと値を送受信したいウィジェットをComsumerで囲んで使う
  • 変更点があったらそのウィジェットだけがrebuild(再構築)される
  • ConsumerWidgetやConsumerStatefulWidgetの中で利用しても有効。
class MyHomePage extends StatelessWidget {
	...
    title: Consumer(
      // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(titleProvider),と同等
      builder: (context, ref, child) => Text(ref.watch(titleProvider),
      )
    ),
    ...
  floatingActionButton:
    Consumer(
      // builder: (BuildContext context, WidgetRef ref, Widget? child) => FloatingActionButton(と同等
      builder: (context, ref, child) => FloatingActionButton(
      onPressed: () => ref.watch(countProvider.notifier).state++,
      tooltip: 'Increment',
      child: const Icon(Icons.add),
    )
  ), 
  • デメリット:Providerを使う箇所全てにConsumerで囲うのは手間がかかる。視認性、保守性が下がる
      // title: Text(ref.watch(titleProvider), )
      // ↑こう書いていたのを↓このようにConsumerで囲まなくてはいけない

    title: Consumer(
      // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(titleProvider),と同等
      builder: (context, ref, child) => Text(ref.watch(titleProvider),
      )
    ),

■ref.watch/ref.readとselect

  • ref.watch: 監視し、変更があったらそれを使ってるウィジェットをrebuildする。
    -> Textの中でデータバインディングしているなど、監視が必要なときに利用

  • ref.read: 単に読み込むだけで、そのウィジェットのrebuildも走らずに反映される。
    -> onTapなど、イベント発生時に値を取得したいだけなら監視は不要。値を取得できればいいのでreadを使う

readとwatchでrebuildが走るタイミングを確認

	// watchを使うと該当部分の再描画のたびに以下ログが出力される
    Consumer(
        // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(countProvider).toString(),と同等
        builder: (context, ref, child) {
          print("⭐️rebuild 1");
          return Text(
            ref.watch(countProvider).toString(),
            style: Theme.of(context).textTheme.headlineMedium,
          );
      }
    ),

	// readを使うと該当部分の再描画の際に以下ログが出力されない
    Consumer(
        // builder: (BuildContext context, WidgetRef ref, Widget? child) => Text(ref.watch(countProvider).toString(),と同等
        builder: (context, ref, child) {
          print("⭐️rebuild 2");
          return Text(
            ref.read(countProvider).toString(),
            style: Theme.of(context).textTheme.headlineMedium,
          );
      }
    ),

  • selectは、watchの中で監視する対象を絞り込める。余計なものを監視しないので、必要な項目が変更されたときだけrebuild走るようになる

selectの実装例

// selectなし: countUpだけじゃなく、countDownに変更があった場合もTextはrebuildされる
Text(ref.watch(countDataProvider).countUp.toString(),)

// selectあり: countDownに変更があった場合はTextはrebuildされない
Text(ref.watch(countDataProvider.state.select((value) => value.state.countUp)).toString(),)

※補足

// 旧参照処理
ref.watch(countProvider).state.toString();
// 新参照処理
ref.watch(countProvider).toString(),

// 旧更新処理
ref.watch(countProvider).state++;
// 新更新処理
ref.watch(countProvider.notifier).state++

■補足: freezedというパッケージの利用

  • データクラスの自動生成
  • copyWithなどの関数を自動で用意してくれる
  • json形式の取り込みを簡単にできる

■サンプルコード

dart items_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

// 自動生成ありの場合
// ターミナルからflutter pub run build_runner buildコマンドで自動生成されるファイル名
// 拡張子前に.gをつける命名規則
// part 'items_provider.g.dart';
//
// @riverpod
// List<String> items(ItemsRef ref) {
//   return [
//     'アニメ',
//     'グルメ',
//     '不動産',
//     'ニュース',
//     'エンタメ',
//     '教育',
//     'ビジネス',
//     '自動車',
//     'バイク',
//     '観光',
//     '手芸',
//   ];
// }

// 自動生成なしの場合
final itemsProvider = Provider<List<String>>((ref) {
  return [
    'アニメ',
    'グルメ',
    '不動産',
    'ニュース',
    'エンタメ',
    '教育',
    'ビジネス',
    '自動車',
    'バイク',
    '観光',
    '手芸',
  ];
});
dart favorites_provider.dart
// 自動生成なしの場合は必要
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 自動生成ありの場合は必要
// import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

const _key = 'favorites';

// ダミーデータ
// @riverpod
// Future<Set<String>> favorites (FavoritesRef ref) async {
//   return {
//     'アニメ',
//     'グルメ',
//   };
// }

// 自動生成ありの場合
// part 'favorites_provider.g.dart';
// @riverpod
// class Favorites extends _$Favorites {
//   @override
//   FutureOr<Set<String>> build() async {
//     final sharedPreferences = await SharedPreferences.getInstance();
//     final favorites = sharedPreferences.getStringList(_key) ?? <String>[];
//     return Set.from(favorites);
//   }
//
//   Future<void> addItem(String item) async {
//     final set = state.value ?? {};
//     set.add(item);
//     await _saveSet(set);
//   }
//
//   Future<void> removeItem(String item) async {
//     final set = state.value ?? {};
//     set.remove(item);
//     await _saveSet(set);
//   }
//
//   Future<void> _saveSet(Set<String> set) async {
//     final sharedPreferences = await SharedPreferences.getInstance();
//     sharedPreferences.setStringList(_key, set.toList(growable: false));
//     state = AsyncData(set);
//   }
// }

// 自動生成なしの場合
final favoritesProvider = StateNotifierProvider<FavoritesNotifier, Set<String>>((ref) {
  return FavoritesNotifier();
});

class FavoritesNotifier extends StateNotifier<Set<String>> {
  FavoritesNotifier() : super({}) {
    loadFavorites(); // 初期化時にお気に入りを読み込む
  }

  Future<void> loadFavorites() async {
    final sharedPreferences = await SharedPreferences.getInstance();
    final favorites = sharedPreferences.getStringList(_key) ?? <String>[];
    state = Set.from(favorites);
  }

  Future<void> addItem(String item) async {
    final set = Set<String>.from(state);
    set.add(item);
    await _saveSet(set);
    state = set;
  }

  Future<void> removeItem(String item) async {
    final set = Set<String>.from(state);
    set.remove(item);
    await _saveSet(set);
    state = set;
  }

  Future<void> _saveSet(Set<String> set) async {
    final sharedPreferences = await SharedPreferences.getInstance();
    sharedPreferences.setStringList(_key, set.toList(growable: false));
    state = set;
  }
}

dart main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'items/items_provider.dart';
import 'favorites/favorites_provider.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

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

  // This widget is the root of your application.
  @override
  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 ConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(itemsProvider);
    final favorites = ref.watch(favoritesProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('あなたの興味は?'),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          final item = items[index];
          // 自動生成ありの場合
          // final isFavorite = favorites.valueOrNull?.contains(item) ?? false;
          // 自動生成なしの場合
          final isFavorite = favorites.contains(item);
          return ListTile(
            leading: IconButton(
              onPressed: () {
                if (isFavorite) {
                  ref.read(favoritesProvider.notifier).removeItem(item);
                } else {
                  ref.read(favoritesProvider.notifier).addItem(item);
                }
              },
              icon: isFavorite
                  ? const Icon(Icons.favorite, color: Colors.pink)
                  : const Icon(Icons.favorite_outline),
            ),
            title: Text(item),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ItemDetailPage(item: item),
                ),
              );
            },
          );
        },
        itemCount: items.length,
      ),
    );
  }
}

class ItemDetailPage extends ConsumerWidget {
  const ItemDetailPage({required this.item, super.key});

  final String item;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final favorites = ref.watch(favoritesProvider);
    // 自動生成ありの場合
    // final isFavorite = favorites.valueOrNull?.contains(item) ?? false;
    // 自動生成なしの場合
    final isFavorite = favorites.contains(item) ?? false;

    return Scaffold(
      appBar: AppBar(
        title: Text('$item'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              '$itemの詳細画面',
              style: TextStyle(fontSize: 24),
            ),
            IconButton(
                onPressed: () {
                  if (isFavorite) {
                    ref.read(favoritesProvider.notifier).removeItem(item);
                  } else {
                    ref.read(favoritesProvider.notifier).addItem(item);
                  }
                },
                icon: isFavorite
                    ? const Icon(
                        Icons.favorite,
                        size: 50,
                        color: Colors.pink,
                      )
                    : const Icon(
                        Icons.favorite_outline,
                        size: 50,
                      ))
          ],
        ),
      ),
    );
  }
}

image.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?