1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】TODO / タスク管理アプリを作成する方法

Posted at

1. はじめに

この記事では、Flutterを使って基本的な機能を持ったTODO / タスク管理アプリを作成します。Flutterの基礎を学びながら、簡単なアプリを作ってみましょう。

2. タスク管理アプリの主な機能:

  1. タスクの追加(作成):
    画面右下の「+」ボタンをタップすると、ダイアログが表示され、タスク名と締め切りを入力してタスクを追加できます。

  2. タスクの編集:
    リスト上の任意のタスクをタップすると、編集ダイアログが表示され、タスク名や締め切りを変更することができます。

  3. タスクの削除:
    タスクを左または右にスワイプすると削除されます。

  4. タスクの完了/未完了の管理:
    リストの右側にあるチェックボックスをタップして、タスクの完了状態を切り替え、完了したタスクは削除します。

  5. タスクのソート
    タスクの並び順として、締め切り日 (deadline) が近い順にタスクを並べています。

  6. 端末のストレージへのタスクの書き込み・読み込み
    端末のローカルストレージにタスクを保存し、アプリが再起動されてもタスクを保持(永続化)することができます。

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 / タスク管理アプリをベースにして、通知機能や独自の機能を追加することで、より本格的なアプリを作成することも可能ですので、ぜひチャレンジしてみてください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?