今回はユーザーが入力した生年月日をもとに年齢を割り出すという、
2画面の簡単なアプリをRiverpodを使って実装していく。
使用するライブラリは以下の通り。
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.3.6
freezed_annotation: ^2.4.1
// 生年月日を入力するためのライブラリ
flutter_datetime_picker_plus: ^2.0.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
freezed: ^2.4.1
build_runner: ^2.4.6
model
modelクラスを作成していく。
Freezed
を使うことで簡単にimmutable(不変)なクラスを作ることができる。
コードを書いているといくつか必ずエラーが出るが、後のコマンドを回してファイルが自動生成されればエラーは消えるので無視でOK。
書き方についてはある程度お約束要素があるので、とりあえずこの形で覚えておく。
import 'package:freezed_annotation/freezed_annotation.dart';
//自動生成されるファイル名を指定する。
part 'age_state.freezed.dart';
@freezed
class AgeState with _$AgeState {
factory AgeState({
@Default(2023) int year,
@Default(7) int month,
@Default(19) int day,
@Default(0) int age
}) = _AgeState;
}
書けたらターミナルで以下のコマンドを回す。
flutter pub run build_runner watch --delete-conflicting-outputs
これで'age_state.freezed.dart'ファイルが生成された。
コマンドに--delete-conflicting-outputsをつけることでクラスに変更が加えられるたびに再生成されるらしいが、私はなぜかわからないがされなかったので手動でいちいち更新をかけていた。
view_model
view_modelを作成していく。
StateNotifierを使用してviewで使いたいビジネスロジックをまとめる。
プロバイダ修飾子のautoDispose
をつけることで参照されなくなったプロバイダのステート(状態)を自動で破棄してくれるようになる。
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mvvm_test/model/age_state.dart';
//AgeNotifierの状態を管理する
final ageProvider =
StateNotifierProvider.autoDispose<AgeNotifier, AgeState>(
(ref) => AgeNotifier(),
);
class AgeNotifier extends StateNotifier<AgeState> {
AgeNotifier(): super(AgeState());
// 現在の日付と入力された生年月日から年齢を算出する
void culculateAge() {
state = state.copyWith(age: (((DateTime.now().year * 10000 + DateTime.now().month * 100 + DateTime.now().day) - (state.year * 10000 + state.month * 100 + state.day))/10000).floor());
}
// modelを入力された生年月日の日付に更新する
void changeBirthday (int year, int month, int day) {
state = state.copyWith(year: year, month: month, day: day);
}
}
view
view(UI)を作成していく。
ConsumerWidget
はbuildメソッドに第2パラメータ(ref)があること以外はStatelessWidgetと同じ。
import 'package:flutter/material.dart';
import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mvvm_test/view/second_page.dart';
import 'package:mvvm_test/view_model/age_notifier.dart';
class FirstPage extends ConsumerWidget {
const FirstPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ageNotifier = ref.watch(ageProvider.notifier);
final ageState = ref.watch(ageProvider);
return Scaffold(
appBar: AppBar(
title: const Text('FirstPage'),
centerTitle: true,
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Column(
children: [
Text("${ageState.year}年${ageState.month}月${ageState.day}日"),
ElevatedButton(
child: const Text('生年月日を入力する'),
onPressed: (){
// 生年月日を選択することができるドラムロールを表示する
DatePicker.showDatePicker(context,
showTitleActions: true,
currentTime: DateTime.now(),
locale: LocaleType.jp,
minTime: DateTime(1900, 1, 1),
maxTime: DateTime(2023, 7, 18),
onChanged: (date) {
// modelを更新する(age以外)
ageNotifier.changeBirthday(date.year, date.month, date.day);
// 更新したmodelを使って年齢を算出する(modelのageのみ更新する)
ageNotifier.culculateAge();
},
);
},
),
ElevatedButton(
child: const Text('次のページへ'),
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SecondPage(),
),
);
},
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mvvm_test/view_model/age_notifier.dart';
class SecondPage extends ConsumerWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ageState = ref.watch(ageProvider);
return Scaffold(
appBar: AppBar(
title: const Text('SecondPage'),
centerTitle: true,
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: Column(
children: [
Text("誕生日は${ageState.year}年${ageState.month}月${ageState.day}日です"),
Text('${ageState.age}歳です')
],
),
),
);
}
}
以上で完成。
2画面にした理由はどこからでもプロバイダーにアクセスできるという使用感を確めたかったからだったが、コードが簡易すぎてそりゃそうって感じになってしまった。
あと、modelやviewmodelの命名だが、Pageという文字を入れてページごとに作っているものが多く見受けられた。今回のように複数の画面で利用するということもあると思うが、これで合ってたのか違和感を拭えない。