ViewModelで次のページいや他のページへ値を渡す
🤔やってみたいこと
みなさん次のページへ値を渡すときは、コンストラクタを使ってますか?
私も普段からそうしています。しかし、ViewModel
を使って渡す方法を使うと、今日強強さんからお聞きしました😅
やったことない笑
riverpod + freezedを使えばできた✨
こちらの完成品を今回作ります
🚀やってみたこと
入力した値を保持するモデルの作成と、状態を表示する次のページ。他のページでも良いですが、それを作りました。これを使えば、コンストラクタいらないかも?
install package
プロジェクトを作成して、riverpod, freezedを追加しましょう。
riverpod:
flutter pub add \
flutter_riverpod \
riverpod_annotation \
dev:riverpod_generator \
dev:build_runner \
dev:custom_lint \
dev:riverpod_lint
freezed:
flutter pub add \
freezed_annotation \
--dev build_runner \
--dev freezed \
json_annotation \
--dev json_serializable
モデルクラスと状態を扱うクラスを作成します。freezed
は二つサンプル作って分けているので、インポート間違えると、「あれ表示されない?」って罠に陥ります😇
Old Code
モデルクラスを作成。今回は、Todoとしておきましょうか。
model class
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'task.freezed.dart';
part 'task.g.dart';
class Task with _$Task {
const factory Task({
required String firstName,
required String lastName,
(false) isDone,
}) = _Task;
factory Task.fromJson(Map<String, Object?> json)
=> _$TaskFromJson(json);
}
状態を扱うクラス。古いコードなので、 StateNotifierを使います。初期値には、モデルクラスの値が入ってきます。コンストラクタが保持しています。
ViewModel
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_next_page/old/entity/task.dart';
final taskProvider = StateNotifierProvider<TaskStateNotifier, Task>((ref) => TaskStateNotifier());
class TaskStateNotifier extends StateNotifier<Task> {
TaskStateNotifier() : super(const Task(firstName: '', lastName: ''));
void updateTask(Task task) {
state = task;
}
}
入力フォームを作成します。ConsumerStatefulWidget
にしている理由は、キーボードを閉じると、入力フォームのテキストがリセットされてしまうのを防ぐために、値を保持する目的で使っています。
input page
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_next_page/old/presentation/next_page.dart';
import 'package:state_next_page/old/presentation/task_view_model.dart';
class TaskFormPage extends ConsumerStatefulWidget {
const TaskFormPage({super.key});
ConsumerState<ConsumerStatefulWidget> createState() => _TaskFormPageState();
}
class _TaskFormPageState extends ConsumerState<TaskFormPage> {
final _formKey = GlobalKey<FormState>();
final fastNameController = TextEditingController();
final lastNameController = TextEditingController();
bool isDone = false;
void dispose() {
fastNameController.dispose();
lastNameController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final task = ref.watch(taskProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple,
title: const Text('入力画面')),
body: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: fastNameController,
decoration: const InputDecoration(labelText: 'First Name'),
onSaved: (value) {
ref
.read(taskProvider.notifier)
.updateTask(task.copyWith(firstName: value!));
},
),
TextFormField(
controller: lastNameController,
decoration: const InputDecoration(labelText: 'Last Name'),
onSaved: (value) {
ref
.read(taskProvider.notifier)
.updateTask(task.copyWith(lastName: value!));
},
),
CheckboxListTile(
title: const Text('Check me'),
value: task.isDone,
onChanged: (value) {
isDone = value!;
ref
.read(taskProvider.notifier)
.updateTask(task.copyWith(isDone: value));
},
),
ElevatedButton(
child: const Text('Submit'),
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
ref.read(taskProvider.notifier).updateTask(
task.copyWith(
firstName: fastNameController.text,
lastName: lastNameController.text,
isDone: isDone,
),
);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const NextPage()),
);
}
},
),
],
),
),
);
}
}
こちらが問題の次のページです。いつもならコンストラクタで値を受け取りますが、今回は、ViewModelで渡しています。ref.watch
で、プロバイダーを参照して、モデルクラスのプロパティを呼び出すことができます。
next page
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_next_page/old/presentation/task_view_model.dart';
class NextPage extends ConsumerWidget {
const NextPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final task = ref.watch(taskProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple,
title: const Text('入力確認画面')),
body: Padding(
padding: const EdgeInsets.only(top: 20, left: 20),
child: Column(
children: <Widget>[
Text('名字: ${task.firstName}'),
Text('名前: ${task.lastName}'),
Checkbox(
value: task.isDone,
onChanged: (value) {
},
),
],
),
),
);
}
}
New Code
Riverpod2.0からは、コードが自動生成されたり、StateNotifierが非推奨になりましたので、コードを修正して使います。
モデルクラスを作成。
model class
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'task.freezed.dart';
part 'task.g.dart';
class Task with _$Task {
const factory Task({
required String firstName,
required String lastName,
(false) isDone,
}) = _Task;
factory Task.fromJson(Map<String, Object?> json)
=> _$TaskFromJson(json);
}
状態を扱うクラス。新しいコードは、Notifierを使用することが推奨されています。コンストラクタがなくなり、buildメソッドの中に、初期値を渡すことになりました。
ViewModel
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:state_next_page/new/entity/task.dart';
part 'task_view_model.g.dart';
class TaskViewModelNotifier extends _$TaskViewModelNotifier {
Task build() {
return const Task(firstName: '', lastName: '');
}
void updateTask(Task task) {
state = task;
}
}
old codeと解説は同じです。
入力フォームを作成します。ConsumerStatefulWidget
にしている理由は、キーボードを閉じると、入力フォームのテキストがリセットされてしまうのを防ぐために、値を保持する目的で使っています。
input page
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_next_page/new/presentation/next_page.dart';
import 'package:state_next_page/new/presentation/task_view_model.dart';
class TaskFormPage extends ConsumerStatefulWidget {
const TaskFormPage({super.key});
ConsumerState<ConsumerStatefulWidget> createState() => _TaskFormPageState();
}
class _TaskFormPageState extends ConsumerState<TaskFormPage> {
final _formKey = GlobalKey<FormState>();
final fastNameController = TextEditingController();
final lastNameController = TextEditingController();
bool isDone = false;
void dispose() {
fastNameController.dispose();
lastNameController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final task = ref.watch(taskViewModelNotifierProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('入力画面G')),
body: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: fastNameController,
decoration: const InputDecoration(labelText: 'First Name'),
onSaved: (value) {
ref
.read(taskViewModelNotifierProvider.notifier)
.updateTask(task.copyWith(firstName: value!));
},
),
TextFormField(
controller: lastNameController,
decoration: const InputDecoration(labelText: 'Last Name'),
onSaved: (value) {
ref
.read(taskViewModelNotifierProvider.notifier)
.updateTask(task.copyWith(lastName: value!));
},
),
CheckboxListTile(
title: const Text('Check me'),
value: task.isDone,
onChanged: (value) {
isDone = value!;
ref
.read(taskViewModelNotifierProvider.notifier)
.updateTask(task.copyWith(isDone: value));
},
),
ElevatedButton(
child: const Text('Submit'),
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
ref.read(taskViewModelNotifierProvider.notifier).updateTask(
task.copyWith(
firstName: fastNameController.text,
lastName: lastNameController.text,
isDone: isDone,
),
);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const NextPage()),
);
}
},
),
],
),
),
);
}
}
old codeと解説は同じ
こちらが問題の次のページです。いつもならコンストラクタで値を受け取りますが、今回は、ViewModelで渡しています。ref.watch
で、プロバイダーを参照して、モデルクラスのプロパティを呼び出すことができます。
next page
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_next_page/new/presentation/task_view_model.dart';
class NextPage extends ConsumerWidget {
const NextPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final task = ref.watch(taskViewModelNotifierProvider);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red,
title: const Text('入力確認画面G')),
body: Padding(
padding: const EdgeInsets.only(top: 20, left: 20),
child: Column(
children: <Widget>[
Text('名字: ${task.firstName}'),
Text('名前: ${task.lastName}'),
Checkbox(
value: task.isDone,
onChanged: (value) {
},
),
],
),
),
);
}
}
[入力する]
[を渡せてますね✨]
🙂最後に
いかがでしたでしょうか?
コンストラクタ引数を使わなくても他のページに値を渡すことができました。riverpodを使う理由それは、どこからでも状態にアクセスできること!
って何かに書いてあったので、本当でしたね。有効活用したいな。
Discussion