📲

ViewModelで次のページいや他のページへ値を渡す

2024/06/14に公開

🤔やってみたいこと

みなさん次のページへ値を渡すときは、コンストラクタを使ってますか?
私も普段からそうしています。しかし、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