💡

Riverpod + freezedで状態管理

2022/01/02に公開

以下の超絶分かりやすいzennのbookを見て、僕も忘れないようにRiverpod + freezedの実装方法を記載します。
https://zenn.dev/riscait/books/flutter-riverpod-practical-introduction

実装の流れ

1つの画面を作る際にMVVMの構造を用います。

1.ルートファイルでRiverpodを使う設定をします。
2.画面で扱う状態をfreezedで定義します。(Model)
状態のクラスです。immutableで定義します。freezedを使います。
ファイル名としてはxxx_state.dartとしています。
3.画面で使う処理を記載します。
RiverpodのStateNotiferを用いて画面の処理を書きます。(ViewModel)
ファイル名としてxxx_notifier.dartとしています。
4.画面に組み込みます。

コードは今開発しているプロジェクトでRiverpodの部分を抜粋し、説明しています。
(都合上、削りきれてないところもあります。。。。)

サンプル

ユーザーがカメラで写真を撮影したもの、もしくはアルバムから画像を選択したものを画面に表示。
保存ボタンを押した時に画像をローカルに保存し、相対パスをSharedPreferencesに保存します。
レイアウトは一部強引に作成しており、ご了承ください。

実装詳細

  1. main.dartファイルにRiverpodを使うための設定を行う。ProviderScopeで囲む。
main.dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDateFormatting('ja_JP');
  await Firebase.initializeApp();
  SystemChrome.setPreferredOrientations(
          [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight])
      .then((_) => {
            runApp(
              //Riverpodを使うための設定。ProviderScopeで囲む
              ProviderScope(
                child: OurHomeTown(),
              ),
            )
          });
}
  1. 状態をfreezedで定義し、freezedで定義したファイルからコードを生成します。
    ポイントは2つ
  • ①生成されるファイル名を指定する。( 生成元ファイル名.freezed.dart
  • ②imageFileを状態をもつ変数として定義する。
taking_reference_state.dart
import 'dart:io';
import 'package:freezed_annotation/freezed_annotation.dart';

//①生成されるファイル名を指定する( `生成元ファイル名.freezed.dart` )
part 'taking_reference_state.freezed.dart';


class TakingReferenceState with _$TakingReferenceState {
  const factory TakingReferenceState({
    //②画像ファイル
    File? imageFile,

  }) = _TakingReferenceState;
}

このファイルを作成した後、コードを生成します。

flutter pub run build_runner build
  1. 状態の処理のクラスを書く。
    ポイントは3つ
  • ①Riverpodで使うプロバイダーの種類をextendsで設定。StateNotifierを使用します。
  • ②StateNotifierで状態のクラスをジェネリックの型に設定します。
  • ③immutableで定義しているので、copyWithでstateを新規で作成します。
taking_reference_notifier.dart
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:our_home_town/domain/photo/photo_domain.dart';
import 'package:our_home_town/presentations/write/tabs/map/taking_reference/taking_reference_state.dart';

// ①、②Providerの定義
class TakingReferenceNotifier extends StateNotifier<TakingReferenceState> {
  final Reader _read;
  TakingReferenceNotifier(this._read) : super(const TakingReferenceState());

    //画像ファイルの状態を更新
  setImageFile(File imageFile) {
    //③stateを更新
    state = state.copyWith(imageFile:imageFile);
  }

  //画像ファイルを保存
  saveNowImg(File imageFile) async {
    final photoDomain = PhotoDomain();
    await photoDomain.saveNowImageAndPath(imageFile);
  }
}
  1. 画面に組み込む。
    ポイントは3つ
  • ①プロバイダーをGlobalで定義します。
  • ②状態を使う変数を定義します。
  • ③状態を更新等行うメソッドを使う変数を定義します。
taking_reference_page.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:our_home_town/presentations/write/tabs/map/taking_reference/taking_reference_notifier.dart';
import 'package:our_home_town/presentations/write/tabs/map/taking_reference/taking_reference_state.dart';
import '../../../../../hometown_theme.dart';

//①RiverpodのプロバイダーをGlobalで定義
final takingReferenceProvider =
    StateNotifierProvider<TakingReferenceNotifier, TakingReferenceState>(
  (ref) => TakingReferenceNotifier(ref.read),
);

class TakingReferencePage extends ConsumerWidget {
  final picker = ImagePicker();

  
  Widget build(BuildContext context, WidgetRef ref) {

    //Riverpodの変数定義
    //②freezedで定義した変数を使うための変数を定義
    final takingReferenceState = ref.watch(takingReferenceProvider);
    //③notifierで定義したメソッドを使うための変数を定義
    final takingReferenceNotifier = ref.watch(takingReferenceProvider.notifier);

    return Scaffold(
        body: SingleChildScrollView(
      child: Padding(
        padding: const EdgeInsets.all(30.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            takingReferenceState.imageFile == null
                ? Padding(
                    padding: const EdgeInsets.all(30.0),
                    child: Text(' '),
                  )
                : Image.file(takingReferenceState.imageFile!),
            if (takingReferenceState.imageFile != null)
              //画像が表示された時に再度画像のとる 表示を変えるのに使用
              Padding(
                padding: const EdgeInsets.all(20.0),
                child: Text(' '),
              ),
            Padding(
              padding: const EdgeInsets.all(20),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  FloatingActionButton.extended(
                    label: Text('カメラで撮る',
                        style: HometownTheme.LightTextTheme.headline4),
                    backgroundColor: Colors.lightGreen[100],
                    onPressed: () async {
                      final pickedFile =
                          await picker.pickImage(source: ImageSource.camera);
                      if (pickedFile != null) {
                        //画像ファイルの変数を更新。notifierで定義したメソッドを使用
                        takingReferenceNotifier
                            .setImageFile(File(pickedFile.path));
                      }
                    },
                  ),
                  if (takingReferenceState.imageFile != null)
                    //保存ボタンの表示
                    FloatingActionButton.extended(
                      label: Text('保存',
                          style: HometownTheme.LightTextTheme.headline2),
                      backgroundColor: Colors.amber,
                      onPressed: () {
                        //画像を保存。notifierで定義したメソッドを使用
                        takingReferenceNotifier
                            .saveNowImg(takingReferenceState.imageFile!);
                        //まとめページ「資料1」へ遷移
                        DefaultTabController.of(context)!.animateTo(5);
                      },
                    ),
                  FloatingActionButton.extended(
                    label: Text('アルバムから選択',
                        style: HometownTheme.LightTextTheme.headline4),
                    backgroundColor: Colors.lightGreen[100],
                    onPressed: () async {
                      final pickedFile =
                          await picker.pickImage(source: ImageSource.gallery);
                      if (pickedFile != null) {
                        //画像ファイルの変数を更新。notifierで定義したメソッドを使用
                        takingReferenceNotifier
                            .setImageFile(File(pickedFile.path));
                      }
                    },
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    ));
  }
}

参考サイト

1.公式サイト
https://riverpod.dev/

2.Flutter x Riverpod でアプリ開発!実践入門
Providerの種類(Provider, StateProvider, StateNotifierProvider, FutureProvider等)、ref.watch, read, listenの違い。freezed等分かりやすく記載されております。
https://zenn.dev/riscait/books/flutter-riverpod-practical-introduction

Discussion