BLOG
MVCを使用したシンプルなTodoアプリ
この記事では、MVC(Model-View-Controller)アーキテクチャを使用して、シンプルなTodoアプリを作成する方法を説明します。
MVCは、アプリケーションの構造を整理し、コードの再利用性とメンテナンスのしやすさを向上させるための一般的な設計パターンです。
MVCとは?
MVCは、アプリケーションを以下の三つの主要な部分に分割します。
- Model(モデル): データとビジネスロジックを担当。データの登録・更新・削除などの処理を行います。また、データの変更をViewに通知するのもModelの役割です。
- View(ビュー): 表示や入出力などのユーザーインターフェースを担当。 リクエストデータをControllerに送ったり、Controllerからレスポンスデータを受け取って画面に表示したりします。
- Controller(コントローラー): ModelとViewの間の仲介役として、ユーザーの入力を処理し、Modelの更新やViewの変更を行います。
Todoアプリの実装
今回やっていないこと
- Riverpodの使用
- ユニットテストの実装
- 以下の要件以外のTodoアプリの機能実装
Todoアプリの要件
今回は以下のような、Todoアイテムの追加、表示、完了状態の切り替え、削除が可能なシンプルなTodoアプリを取り上げます。
- Todoアイテムの追加:
- ユーザーは新しいTodoアイテムを追加できる。
- 追加する際には、Todoアイテムのタイトルを入力できる。
- Todoアイテムの表示:
- 追加されたTodoアイテムはリスト形式で表示される。
- 各Todoアイテムには、タイトルと完了状態(チェックボックス)が表示される。
- Todoアイテムの完了状態の切り替え:
- ユーザーは各Todoアイテムのチェックボックスをクリックすることで、そのアイテムの完了状態を切り替えられる。
- Todoアイテムの削除:
- 各Todoアイテムには削除ボタンがあり、ユーザーは特定のアイテムをリストから削除できる。
- ユーザーインターフェイス:
- アプリにはヘッダーがあり、「TODO List」と表示される。
- 新しいTodoアイテムを追加するためのボタンがあり、押すとダイアログが表示される。
- ダイアログでは、新しいTodoアイテムのタイトルを入力し、追加ボタンをクリックしてリストに追加できる。
- アプリ内のデータを端末に保存:
- shared_preference を用いて、アプリ内のTodoデータを端末に保存する。
ディレクトリ構成
lib/
├ controllers/
│ └ todo_controller.dart
├ models/
│ └ todo.dart
│ └ todo_list.dart
├ views/
│ └ todo_app_view.dart
│ └ todo_list_view.dart
└ main.dart
Modelの実装
Modelの役割は、アプリのデータとビジネスロジックを管理することです。
ここでは、modelsディレクトリの中にtodo.dartとtodo_list.dartを作成します。
todo.dartには、Todoクラスが定義されており、個々のTodoアイテムを表すクラスです。
class Todo {
/// 各Todoのデータ構造を定義
int id; // 一意の識別子
String title; // タイトル
bool isCompleted; // 完了状態
/// Todoクラスのコントラクタ
Todo({
required this.id, // 必須
required this.title, // 必須
this.isCompleted = false // 必須ではない
});
/// Todoの完了状態を切り替え
void toggleCompleted() {
isCompleted = !isCompleted;
}
/// JSONからTodoオブジェクトを生成
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
isCompleted: json['isCompleted'],
);
}
/// TodoオブジェクトをJSONに変換
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isCompleted': isCompleted,
};
}
}
todo_list.dartには、TodoListクラスが定義されており、Todoアイテムのリストを管理するクラスとなっています。Todoアイテムの追加・完了状態の切り替え・削除、端末へのデータ保存・取得などの操作が記述されています。
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:todo_app/models/todo.dart';
class TodoList {
List<Todo> todos = [];
/// 一意のidを生成
int _generateUniqueId() {
return todos.isEmpty ? 0 : todos.last.id + 1;
}
/// idを用いて、ToDoをリストに追加
void addTodo(String title) {
final todo = Todo(id: _generateUniqueId(), title: title);
todos.add(todo);
saveTodos();
}
/// 指定されたidのTodoの完了状態を切り替え
void toggleCompleted(int id) {
Todo? todo;
try {
todo = todos.firstWhere((todo) => todo.id == id);
todo.toggleCompleted();
saveTodos();
} catch (e) {
return;
}
}
/// 指定されたidのToDoをリストから削除
void deleteTodo(int id) {
todos.removeWhere((todo) => todo.id == id);
saveTodos();
}
/// データの保存
Future<void> saveTodos() async {
final prefs = await SharedPreferences.getInstance();
final String encodedData = jsonEncode(todos.map((todo) => todo.toJson()).toList());
await prefs.setString('todoList', encodedData);
}
/// shared_preferenceからデータを取得
Future<void> loadTodos() async {
final prefs = await SharedPreferences.getInstance();
final String? encodedData = prefs.getString('todoList');
if (encodedData != null) {
final List<dynamic> decodedData = jsonDecode(encodedData);
todos = decodedData.map((item) => Todo.fromJson(item)).toList();
}
}
}
これらのクラスとメソッドは、Todoアプリのデータモデルとビジネスロジックを構成しています。
Controllerの実装
Controllerの役割は、ユーザーからの入力やアクション(今回は、Todoアイテムの追加・削除・完了状態の切り替え、データの保存・取得)を受け取り、Modelの操作やViewの更新を行うことです。
ここでは、Controllerディレクトリの中に、todo_controller.dartを作成します。
todo_controller.dartには、TodoControllerクラスが定義されており、ユーザーの操作に対するTodoListモデルの操作が記述されてあります。
import 'package:todo_app/models/todo_list.dart';
class TodoController {
final TodoList todoList = TodoList();
void dispose() {
// 現在は何も解放するリソースがないので空
}
void addTodo(String title) {
todoList.addTodo(title);
}
void toggleTodo(int id) {
todoList.toggleCompleted(id);
}
void deleteTodo(int id) {
todoList.deleteTodo(id);
}
Future<void> loadTodos() async {
await todoList.loadTodos();
}
Future<void> saveTodos() async {
await todoList.saveTodos();
}
}
TodoController
クラスは、ViewとModel(データとビジネスロジック)の間の仲介役として機能し、ユーザーの操作に応じて適切なアクションを実行します。
Viewの実装
Viewの役割は、ユーザーインターフェイスの表示・更新とユーザー入力の受け取りです。
ユーザーインターフェイスの表示は、Todo アイテムのリストや操作用のボタンなど、ユーザーに対するインターフェイスを提供します。また、Controllerからの指示に基づいて、Modelの最新の状態をViewに表示します。
ユーザー入力の受け取りは、ユーザーからの操作を受け取り、それをControllerに伝えるという役割です。
ここでは、Viewsディレクトリの中にtodo_app_view.dartとtodo_list_view.dartを作成します。
todo_app_view.dartには、Todoアプリのメイン画面を定義しています。
import 'package:flutter/material.dart';
import 'package:todo_app/controllers/todo_controller.dart';
import 'package:todo_app/views/todo_list_view.dart';
class TodoAppView extends StatefulWidget {
const TodoAppView({super.key});
@override
_TodoAppViewState createState() => _TodoAppViewState();
}
class _TodoAppViewState extends State<TodoAppView> {
final _controller = TodoController();
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
await _controller.loadTodos();
setState(() {});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TODO List'),
),
body: TodoListView(
todos: _controller.todoList.todos,
onToggle: (id) {
setState(() {
_controller.toggleTodo(id);
});
},
onDelete: (id) {
setState(() {
_controller.deleteTodo(id);
});
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await _showAddTodoDialog(context);
await _controller.saveTodos();
},
child: const Icon(Icons.add),
),
);
}
Future<void> _showAddTodoDialog(BuildContext context) async {
final textEditingController = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add new TODO'),
content: TextField(
controller: textEditingController,
decoration: const InputDecoration(hintText: 'Enter TODO title'),
),
actions: <Widget>[
ElevatedButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
ElevatedButton(
child: const Text('Add'),
onPressed: () {
setState(() {
_controller.addTodo(textEditingController.text);
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
このビューは、ユーザーとアプリのインタラクションを管理し、ユーザーの操作に応じてコントローラーを通じてモデルを更新する役割を果たしています。
todo_list_view.dartには、Todoアイテムのリストを表示するための画面が定義されています。
import 'package:flutter/material.dart';
import 'package:todo_app/models/todo.dart';
class TodoListView extends StatelessWidget {
final List<Todo> todos;
final Function onToggle;
final Function onDelete;
const TodoListView({super.key, required this.todos, required this.onToggle, required this.onDelete});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
leading: Checkbox(
value: todos[index].isCompleted,
onChanged: (bool? value) {
if (value != null) {
onToggle(todos[index].id);
}
},
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
onDelete(todos[index].id);
},
),
);
},
);
}
}
このビューは、Todoアイテムのリストを表示し、ユーザーが各アイテムの完了状態を切り替えたり、アイテムを削除したりできるようにするインターフェイスを提供します。また、ユーザーの操作をコントローラーに伝達する役割も果たしています。
ソースコード全体は、こちらからご覧いただけます。
まとめ
この記事では、MVC(Model-View-Controller)アーキテクチャを使用して、シンプルなTodoアプリをFlutterで作成する方法について説明しました。MVCは、アプリケーションの構造を整理し、コードの再利用性とメンテナンスのしやすさを向上させるための一般的な設計パターンです。アプリケーションをModel、View、Controllerの三つの主要な部分に分割することで、それぞれの役割を明確にし、開発を効率化します。
FlutterでMVCを適用する際の難しさは、Flutterが主にウィジェットベースのフレームワークであるため、従来のMVCアーキテクチャの適用が直感的でない場合があることです。特に、ViewとControllerの分離が難しく、ウィジェットの状態管理が複雑になる可能性があります。また、Flutterの状態管理のアプローチが多様であるため、適切な状態管理戦略を選択することも重要です。
今回のTodoアプリでは、シンプルなMVC構造を採用し、Modelはデータとビジネスロジックを、Viewはユーザーインターフェースを、Controllerはモデルとビューの間の仲介役を担いました。各コンポーネントの役割を明確にすることで、アプリケーションの構造が整理され、メンテナンスや拡張が容易になります。
FlutterでMVCを適用する際は、ウィジェットの構造と状態管理に注意を払いながら、Model、View、Controllerの役割を適切に分離することが重要です。また、プロジェクトの規模や要件に応じて、適切な状態管理戦略を選択することも重要です。