1. はじめに
この記事では、Flutterを使って基本的な機能を持ったTODO / タスク管理アプリを作成します。Flutterの基礎を学びながら、簡単なアプリを作ってみましょう。
2. タスク管理アプリの主な機能:
-
タスクの追加(作成):
画面右下の「+」ボタンをタップすると、ダイアログが表示され、タスク名と締め切りを入力してタスクを追加できます。 -
タスクの編集:
リスト上の任意のタスクをタップすると、編集ダイアログが表示され、タスク名や締め切りを変更することができます。 -
タスクの削除:
タスクを左または右にスワイプすると削除されます。 -
タスクの完了/未完了の管理:
リストの右側にあるチェックボックスをタップして、タスクの完了状態を切り替え、完了したタスクは削除します。 -
タスクのソート
タスクの並び順として、締め切り日 (deadline) が近い順にタスクを並べています。 -
端末のストレージへのタスクの書き込み・読み込み
端末のローカルストレージにタスクを保存し、アプリが再起動されてもタスクを保持(永続化)することができます。
3. 実装
日付をフォーマットするために、intl
パッケージをインストールします。
flutter pub add intl
アプリを再起動してもタスクを保持するため、shared_preferences
パッケージをインストールします。
flutter pub add shared_preferences
main.dart
を以下の通り実装します。
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
void main() {
runApp(TaskManagerApp());
}
class TaskManagerApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Task Manager',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TaskManagerScreen(),
);
}
}
/* Taskクラスの定義
・「タスク名」「締め切り」「完了/未完了」の要素を含む
・ name:タスク名
・ deadline:締め切り日
・ isCompleted:完了/未完了の状態(初期値は`false`)
*/
class Task {
String name;
DateTime deadline;
bool isCompleted;
Task({required this.name, required this.deadline, this.isCompleted = false});
// JSONへ変換するためのメソッド(shared_preferenceへの書き込み時に使用)
Map<String, dynamic> toJson() {
return {
'name': name,
'deadline': deadline.toIso8601String(),
'isCompleted': isCompleted,
};
}
// JSONからTaskオブジェクトを作成するためのメソッド(shared_preferenceからの読み込み時に使用)
factory Task.fromJson(Map<String, dynamic> json) {
return Task(
name: json['name'],
deadline: DateTime.parse(json['deadline']),
isCompleted: json['isCompleted'],
);
}
}
class TaskManagerScreen extends StatefulWidget {
@override
_TaskManagerScreenState createState() => _TaskManagerScreenState();
}
class _TaskManagerScreenState extends State<TaskManagerScreen> {
List<Task> _tasks = [];
final myFormat = DateFormat('yyyy年MM月dd日');
@override
void initState() {
super.initState();
// アプリ起動時に保存しているタスクを読み込む
_loadTasks();
}
// タスクを保存する関数
Future<void> _saveTasks() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// タスクリストをJSON形式に変換
List<String> tasksJson = _tasks.map((task) => jsonEncode(task.toJson())).toList();
await prefs.setStringList('tasks', tasksJson); // リストを保存
}
// タスクを読み込む関数
Future<void> _loadTasks() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String>? tasksJson = prefs.getStringList('tasks');
if (tasksJson != null) {
setState(() {
_tasks = tasksJson.map((task) => Task.fromJson(jsonDecode(task))).toList();
});
}
}
// タスクを作成する関数
Future<void> _showAddTaskDialog() async {
// TextFieldの値を取得、変更、リセットできるコントローラー
final _nameController = TextEditingController();
// 締め切りの初期値として今日の日付を指定
DateTime _selectedDeadline = DateTime.now();
// ダイアログはasync/awaitで書く必要
await showDialog(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text('タスクを追加'),
content: Column(
//Columnの長さを最小化する
mainAxisSize: MainAxisSize.min,
children: [
// タスク名を設定
TextField(
controller: _nameController,
decoration: InputDecoration(labelText: 'タスク名'),
),
SizedBox(height: 20),
// 締め切り日を設定
ListTile(
title: Text('締め切り: ${myFormat.format(_selectedDeadline.toLocal())}'),
trailing: Icon(Icons.calendar_today),
onTap: () async {
// 締め切り日をカレンダーから選択
final DateTime? picked = await showDatePicker(
context: context,
// 締め切り日の初期値
initialDate: _selectedDeadline.toLocal(),
// 締切日に指定できるのは2024年〜2100年
firstDate: DateTime(2024),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
_selectedDeadline = picked;
});
}
},
),
],
),
// ダイアログの下部に出すボタンの設定
actions: [
// タスク作成をキャンセルするボタン
TextButton(
child: Text('キャンセル'),
onPressed: () => Navigator.pop(context),
),
// タスク作成を完了するボタン
TextButton(
child: Text('追加'),
onPressed: () {
// タスク名が空でないか確認して実行
if (_nameController.text.isNotEmpty) {
_tasks.add(Task(
name: _nameController.text,
deadline: _selectedDeadline,
));
// 作成したタスクを保存
_saveTasks();
Navigator.pop(context);
}
},
),
],
);
}
);
},
);
setState(() {});
}
// タスクを編集する関数
Future<void> _showEditTaskDialog(int index) async {
// 選択したn番目のタスクの名前を取得
final _nameController = TextEditingController(text: _tasks[index].name);
// 選択したn番目のタスクの締め切り日を取得
DateTime _selectedDeadline = _tasks[index].deadline;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('タスクを編集'),
content: Column(
//Columnの長さを最小化する
mainAxisSize: MainAxisSize.min,
children: [
// タスク名を設定
TextField(
controller: _nameController,
decoration: InputDecoration(labelText: 'タスク名'),
),
SizedBox(height: 20),
// 締め切り日を設定
ListTile(
title: Text('締め切り: ${_selectedDeadline.toLocal()}'),
trailing: Icon(Icons.calendar_today),
onTap: () async {
// 締め切り日をカレンダーから選択
final DateTime? picked = await showDatePicker(
context: context,
// 締め切り日の初期値
initialDate: _selectedDeadline,
// 締切日に指定できるのは2024年〜2100年
firstDate: DateTime(2024),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
_selectedDeadline = picked;
});
}
},
),
],
),
actions: [
// タスク編集をキャンセルするボタン
TextButton(
child: Text('キャンセル'),
onPressed: () => Navigator.pop(context),
),
// タスク編集を完了するボタン
TextButton(
child: Text('保存'),
onPressed: () {
// タスク名が空でないか確認して実行
if (_nameController.text.isNotEmpty) {
_tasks[index].name = _nameController.text;
_tasks[index].deadline = _selectedDeadline;
// タスクの変更を保存
_saveTasks();
Navigator.pop(context);
}
},
),
],
);
},
);
setState(() {});
}
// タスクを削除する関数
void _deleteTask(int index) {
setState(() {
_tasks.removeAt(index);
// タスクの削除を保存
_saveTasks();
});
}
@override
Widget build(BuildContext context) {
// 締め切り日が近い順にタスクをソート
_tasks.sort((a, b) => a.deadline.compareTo(b.deadline));
return Scaffold(
appBar: AppBar(
title: Text('タスク管理アプリ'),
),
// タスク一覧を表示
body: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
// n番目のタスク
Task task = _tasks[index];
// ListのアイテムをDismissibleでラップする事でスワイプ削除を実現できる
return Dismissible(
key: Key(task.name),
background: Container(color: Colors.red, child: Icon(Icons.delete, color: Colors.white)),
// スワイプした時に_deleteTaskを実行
onDismissed: (direction) => _deleteTask(index),
// Listのアイテム(タスク)の中身
child: ListTile(
// タスク名
title: Text(task.name),
// 締切日
subtitle: Text('締め切り: ${ myFormat.format(task.deadline.toLocal())}'),
// 完了/未完了を表すチェックボックス
trailing: Checkbox(
value: task.isCompleted,
// チェックボックスが切り替わった時に以下を実行
onChanged: (value) {
setState(() {
_tasks[index].isCompleted = !_tasks[index].isCompleted;
_tasks.removeAt(index);
// タスクの完了/未完了の変更を保存
_saveTasks();
});
},
),
// Listのアイテム(タスク)をタップするとタスクの編集可能
onTap: () {
_showEditTaskDialog(index);
},
),
);
},
),
// タスクを追加(作成)するボタン
floatingActionButton: FloatingActionButton(
onPressed: _showAddTaskDialog,
child: Icon(Icons.add),
),
);
}
}
解説
1. Taskクラス
Task
クラスは、アプリで扱うタスクの基本情報を管理するためのクラスです。各タスクは名前、締め切り、完了ステータスを持っています。isCompleted
プロパティを使って、タスクが完了しているかどうかを管理しています。
2. タスクの追加
_showAddTaskDialog
関数では、ダイアログを表示してタスクを追加することができます。タスク名を入力し、締め切り日をカレンダーから選択後、「追加」ボタンを押すことで、新しいタスクが_tasks
リストに追加されます。
3. タスクの編集
_showEditTaskDialog
関数では、ダイアログを表示して既存のタスクを編集することができます。タスク名、締め切り日を編集し、変更内容を保存することができます。
4. タスクの削除
Dismissible
ウィジェットを使うことで、タスクのスワイプ削除を実装することができます。スワイプすると_deleteTask
関数が呼び出され、タスクがリストから削除されます。
5. タスクの完了/未完了の切り替え
各タスクにはCheckbox
があり、チェックを付けることでタスクの完了/未完了状態を切り替えることができます。チェックを付けると、isCompleted
プロパティが反転します。
6. タスクのソート
build
メソッド内で、_tasks.sort((a, b) => a.deadline.compareTo(b.deadline));
を使って、締め切り日 (deadline
) が近い順にタスクを並べ替えています。このソートは、毎回画面を描画するたびに行われます。これにより、タスクは常に締め切りが近い順に表示されるようになります。
7. 端末のストレージへのタスクの保存・読み込み
SharedPreferences
を使って、タスクをJSON形式で端末のストレージに書き込み・読み込みします。_saveTasks()
関数でタスクを書き込み、_loadTasks()
関数でタスクを読み込みます。また、Task
クラスにJSON変換用のメソッドを記載しており、SharedPreferences
で扱える形式に変換しています。
4. おわりに
この記事では、Flutterを使って基本的な機能を持ったTODO / タスク管理アプリを作成しました。今回のTODO / タスク管理アプリをベースにして、通知機能や独自の機能を追加することで、より本格的なアプリを作成することも可能ですので、ぜひチャレンジしてみてください。