【Flutter】Riverpod + MVVM実装例


■MVP
->プレゼンターが能動的にViewを更新しに行く

- Model = ビジネスロジック+データアクセス
- Presenter　= アプリケーションロジック+Viewの更新
- View = UI制御

■MVVM
->データバインディングでViewModelの値がViewに反映される

- Model = ビジネスロジック+データアクセス
- ViewModel　= アプリケーションロジック
- View = UI制御

■Riverpod
- Provider = データの管理、提供者
-> ViewModelはProviderを使ってデータを操作
- Consumer:データの使用者
-> ViewはConsumerWidgetを継承してデータを監視しておくと、Providerからデータが渡ってくる（データバインディング）

■実装例
```dart models/todo.dart
// Model
// データクラス
class Todo {
  const Todo({required this.id, required this.title, this.isCompleted = false});

  final String id;
  final String title;
  final bool isCompleted;
}

```

```dart view_models/todos_view_model.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/todo.dart';

// ViewModel
// ProviderとViewModel

final todosViewModelProvider =
    StateNotifierProvider<TodosViewModel, List<Todo>>((ref) {
  return TodosViewModel();
});

class TodosViewModel extends StateNotifier<List<Todo>> {
  // 初期状態として空のリストを追加
  TodosViewModel() : super([]);

  void addTodo(Todo todo) {
    state = [...state, todo];
  }

  void addTodoByTitle(String title) {
    addTodo(Todo(id: "$title${state.length + 1}", title: title));
  }

  void toggleTodoStatus(String id) {
    // 完了フラグを反転させる
    state = state.map((todo) {
      if (todo.id == id) {
        return Todo(
            id: todo.id, title: todo.title, isCompleted: !todo.isCompleted);
      }
      return todo;
    }).toList();
  }
}

```

```dart main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mvvm_riverpod_test/view_models/todos_view_model.dart';
import 'models/todo.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: TodosPage(),
    );
  }
}

class TodosPage extends ConsumerWidget {
  TodosPage({super.key});

  TextEditingController controller = TextEditingController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // データの監視
    List<Todo> todos = ref.watch(todosViewModelProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('TODO'),
      ),
      body: ListView.builder(
        // ListView.builderは以下のカウント数分ループ
        itemCount: todos.length + 1,
        itemBuilder: (context, index) {
          if (index == 0) {
            // 一つ目の要素はテキストフィールド
            return ListTile(
              title: TextField(controller: controller),
              trailing: ElevatedButton(
                onPressed: () {
                  ref
                      .read(todosViewModelProvider.notifier)
                      .addTodoByTitle(controller.text);
                  controller.clear();
                },
                child: const Text('登録'),
              ),
            );
          }
          final todo = todos[index - 1];
          return ListTile(
            title: Text(todo.title),
            trailing: Checkbox(
              value: todo.isCompleted,
              onChanged: (bool? newValue) {
                ref
                    .read(todosViewModelProvider.notifier)
                    .toggleTodoStatus(todo.id);
              },
            ),
          );
        },
      ),
    );
  }
}

```