💡
Riverpod + freezedで状態管理
以下の超絶分かりやすいzennのbookを見て、僕も忘れないようにRiverpod + freezedの実装方法を記載します。
実装の流れ
1つの画面を作る際にMVVMの構造を用います。
1.ルートファイルでRiverpodを使う設定をします。
2.画面で扱う状態をfreezedで定義します。(Model)
状態のクラスです。immutableで定義します。freezedを使います。
ファイル名としてはxxx_state.dartとしています。
3.画面で使う処理を記載します。
RiverpodのStateNotiferを用いて画面の処理を書きます。(ViewModel)
ファイル名としてxxx_notifier.dartとしています。
4.画面に組み込みます。
コードは今開発しているプロジェクトでRiverpodの部分を抜粋し、説明しています。
(都合上、削りきれてないところもあります。。。。)
サンプル
ユーザーがカメラで写真を撮影したもの、もしくはアルバムから画像を選択したものを画面に表示。
保存ボタンを押した時に画像をローカルに保存し、相対パスをSharedPreferencesに保存します。
レイアウトは一部強引に作成しており、ご了承ください。
実装詳細
- 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(),
),
)
});
}
- 状態を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
- 状態の処理のクラスを書く。
ポイントは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);
}
}
- 画面に組み込む。
ポイントは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.公式サイト
2.Flutter x Riverpod でアプリ開発!実践入門
Providerの種類(Provider, StateProvider, StateNotifierProvider, FutureProvider等)、ref.watch, read, listenの違い。freezed等分かりやすく記載されております。
Discussion