概要
Flutter の状態管理パターンの一つである hooks_riverpod + StateNotifier + freezed を使いカウンターアプリを作成する。
環境
- Flutter 2.8.1
- hooks_riverpod 1.0.3
- state_notifier 0.7.2
- freezed 1.1.1
- freezed_annotation 1.1.0
- build_runner 2.1.7
設計
ファイル(責務)分割
今回は以下のように MVVM を意識して責務を分けた。
- lib
- main.dart : main メソッドのみ
- app.dart : build メソッドのみ
- widget.dart : View 部分 MyHomePage Widget
- provider.dart : ViewModel 部分 Riverpod Provider
- coutner_state.dart : Model 部分カウンター state
hooks_riverpod / sate_notifier / freezed
以前まとめた。
チュートリアル実践
準備
プロジェクト作成。
flutter create tutorial_hooks_freezed cd tutorial_hooks_freezed
必要なパッケージを導入( hooks_riverpod を入れると state_notifier も自動で入るが明示的に)。
flutter pub add hooks_riverpod flutter pub add state_notifier flutter pub add freezed_annotation flutter pub add --dev build_runner flutter pub add --dev freezed
pubspec.yaml は以下のようになる。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
hooks_riverpod: ^1.0.3
state_notifier: ^0.7.2+1
freezed_annotation: ^1.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
build_runner: ^2.1.7
freezed: ^1.1.1
責務ごとにファイル分割
main.dart
アプリ実行のための main() のみを記述。
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app.dart';
void main() {
runApp(
const ProviderScope(
child: App(),
),
);
}
app.dart
MaterialApp を宣言する build メソッドのみを記述。
import 'package:flutter/material.dart';
import 'widget.dart';
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
widget.dart
View 部分で画面表示を責務とする。
そのため floactingActionButton は分割するべきかも。
特徴をいくつか抜粋すると
- HookConsumerWidget による hooks_riverpod を利用可能に
- counterProvider の state の監視
- 表示するカウント数 ${count.count}
- floatingActionButton クリック時に increment する ref.read(counterProvider.notifier).increment();
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'provider.dart';
class MyHomePage extends HookConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod counter example'),
),
body: Center(
child: Text(
'${counter.count}',
style: Theme.of(context).textTheme.headline4,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: const Icon(Icons.add),
),
);
}
}
provider.dart
ViewModel として Model (state) を操作するため橋渡しを責務とする。
CounterNotifier
をインスタンスに持つcounterProvider
をグローバルで宣言increment
メソッド内でcopyWith
によるstate
の複製、状態の更新
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'counter_state.dart';
final counterProvider = StateNotifierProvider<CounterNotifier, CounterState>(
(ref) => CounterNotifier(const CounterState()),
);
class CounterNotifier extends StateNotifier<CounterState> {
CounterNotifier(CounterState state) : super(state);
// state は immutable なため、copyWith で複製する
void increment() {
state = state.copyWith(count: state.count + 1);
}
}
counter_state.dart
状態 ( state ) を保持することを責務とする。
- Immutable な CounterState を宣言
- NullSafety 対応のため デフォルト値を宣言
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'counter_state.freezed.dart';
@freezed
class CounterState with _$CounterState {
const factory CounterState({
//NullSafetyのためデフォルト値の指定が必須
@Default(0) int count,
}) = _Counter;
}
実行
ビルド
flutter pub run build_runner build --delete-conflicting-outputs
起動
flutter run
いつものカウンターアプリが起動したら OK 。
まとめ
カウンターアプリで freezed を使う意味はまったくないが、ハローワールド的な立ち位置。
コードは Github にアップ。
GitHub - runble1/flutter_riverpod_statenotifier_freezed
Contribute to runble1/flutter_riverpod_statenotifier_freezed development by creating an account on GitHub.
参考
【Flutter】Statefulなカウンターアプリをhooks_riverpod+state_notifier+freezedでリファクタリングする - Qiita
FlutterのNullSafetyがstableになってしばらくたち、最近になってriverpodがstableになったので、記念にhooks_riverpod + state_notifier …
Riverpod+Flutter Hooks+freezedで、カウンターアプリを作ってみた
コメント