😆

[flutter]初心者が2週間で最低限の機能を持ったtodoリストを作った話

2023/06/17に公開

「大きく見やすい忘れ物リストアプリ『大きく見やすい忘れ物リスト』を作成しました。

作ったもの

altテキスト

https://apps.apple.com/jp/app/大きく見やすい忘れ物リスト/id6450145814

「私は大きく見やすい忘れ物リスト」、いわゆるToDoリストを作成しました。このアプリの目的は、まずはアプリをリリースしてみることでした。初めに簡単なToDoリストを作成することから始めました。

ToDoのタスクの保存とデータベース

まず、ToDoリストのタスクをデータベースに保存するために、Driftというパッケージを使用しました。
DriftはFlutterのための軽量なデータベースパッケージであり、簡単にデータを永続化できます。ToDoの追加、編集、削除などの操作が行われるたびに、Driftを使用してデータベース内のタスク情報を更新しました。

アプリの主な機能紹介

  1. ToDoの追加、削除、並び替え
    このアプリでは、基本的なToDoリストの機能を備えています。ユーザーはタスクを追加したり、完了したタスクを削除したりできます。また、タスクの並び替えも行えるので、重要なタスクや期限が近いタスクを優先して表示することができます。

  2. 通知機能
    忘れ物チェックリストは、ユーザーにタスクのリマインダーを送信する通知機能も備えています。設定した時間が近づくと、アプリはユーザーに通知を送信し、タスクの完了を促します。これにより、ユーザーは重要なタスクを見落とすことなく管理できます。

  3. サイドメニュー
    アプリにはサイドメニューがあり、ユーザーが簡単に他の機能や設定にアクセスできます。サイドメニューを通じて、タスクの並び替え、一掃削除、さまざまなオプションを利用することができます。ユーザーは自分の使いやすい方法でアプリをカスタマイズできます。

通知機能

// ファイル名: noti.dart

// 必要なパッケージをインポートします
import 'package:flutter/cupertino.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';
import 'package:timezone/timezone.dart' as tz;

// 通知機能を管理するクラスを定義します
class Noti {
  // flutterLocalNotificationsPlugin インスタンスの作成
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  // プラットフォーム固有の初期化を行うメソッドです
  void initializePlatformSpecifics() {
    // Android の初期化設定
    const initializationSettingsAndroid =
        AndroidInitializationSettings('app_icon');

    // iOS の初期化設定
    var initializationSettingsIOS = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: false,
      onDidReceiveLocalNotification: (id, title, body, payload) async {},
    );

    // 初期化設定を組み合わせて全体の初期化設定を作成します
    var initializationSettings = InitializationSettings(
        android: initializationSettingsAndroid, iOS: initializationSettingsIOS);

    // 初期化設定を使用して通知プラグインを初期化します
    flutterLocalNotificationsPlugin.initialize(initializationSettings,
        onDidReceiveNotificationResponse: (NotificationResponse res) {
      print("呼ばれました");
      debugPrint('payload:${res.payload}');
    });
  }

  // 通知を表示するメソッドです
  Future<void> showNotification(int minutes) async {
    // Android 用の通知設定
    var androidChannelSpecifics = const AndroidNotificationDetails(
      'CHANNEL_ID',
      'CHANNEL_NAME',
      channelDescription: "CHANNEL_DESCRIPTION",
      importance: Importance.max,
      priority: Priority.high,
      playSound: false,
      timeoutAfter: 5000,
      styleInformation: DefaultStyleInformation(
        true,
        true,
      ),
    );

    // iOS 用の通知設定
    var iosChannelSpecifics = const DarwinNotificationDetails();

    // プラットフォームごとの通知設定を組み合わせます
    var platformChannelSpecifics = NotificationDetails(
      android: androidChannelSpecifics,
      iOS: iosChannelSpecifics,
    );

    // 通知を表示します
    await flutterLocalNotificationsPlugin.show(
      0, // 通知のID
      '設定完了!', // 通知のタイトル
      '$minutes分後にアラームを設定しました', // 通知の本文
      platformChannelSpecifics,
      payload: 'Alarm Payload',
    );
  }

  // 指定された時間後に通知をスケジュールするメソッドです
  Future<void> scheduleNotification(int minutes) async {
    // スケジュールする日時を計算します
    var scheduleNotificationDateTime =
        tz.TZDateTime.now(tz.local).add(Duration(minutes: minutes));

    // Android 用の通知設定
    var androidChannelSpecifics = const AndroidNotificationDetails(
      'CHANNEL_ID 1',
      'CHANNEL_NAME 1',
      channelDescription: "CHANNEL_DESCRIPTION 1",
      icon: 'app_icon',
      enableLights: true,
      color: Color.fromARGB(255, 255, 0, 0),
      ledColor: Color.fromARGB(255, 255, 0, 0),
      ledOnMs: 1000,
      ledOffMs: 500,
      importance: Importance.max,
      priority: Priority.high,
      playSound: false,
      timeoutAfter: 10000,
      styleInformation: DefaultStyleInformation(true, true),
    );

    // iOS 用の通知設定
    var iosChannelSpecifics = const DarwinNotificationDetails();

    // プラットフォームごとの通知設定を組み合わせます
    var platformChannelSpecifics = NotificationDetails(
      android: androidChannelSpecifics,
      iOS: iosChannelSpecifics,
    );

    // 通知をスケジュールします
    await flutterLocalNotificationsPlugin.zonedSchedule(
      0, // 通知のID
      '忘れ物はありませんか?', // 通知のタイトル
      '外出前にもう一度リストを確認しましょう!', // 通知の本文
      tz.TZDateTime.from(scheduleNotificationDateTime, tz.local),
      platformChannelSpecifics,
      payload: 'Test Payload',
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
    );
  }

  // 保留中の通知の数を取得するメソッドです
  Future<int> getPendingNotificationCount() async {
    List<PendingNotificationRequest> p =
        await flutterLocalNotificationsPlugin.pendingNotificationRequests();
    print(p.length);
    return p.length;
  }

  // 指定した通知IDの通知をキャンセルするメソッドです
  Future<void> _cancelNotification(int id) async {
    await flutterLocalNotificationsPlugin.cancel(id);
  }

  // すべての通知をキャンセルするメソッドです
  Future<void> cancelAllNotification() async {
    await flutterLocalNotificationsPlugin.cancelAll();
  }
}

  • initializePlatformSpecifics(): プラットフォーム固有の初期化を行います。
  • showNotification(int minutes): 通知を表示します。
  • scheduleNotification(int minutes): 指定された時間後に通知をスケジュールします。
  • getPendingNotificationCount(): 保留中の通知の数を取得します。
  • _cancelNotification(int id): 指定した通知IDの通知をキャンセルします。
  • cancelAllNotification(): すべての通知をキャンセルします。

[Drift] データベース操作のためのシンプルなコード

import 'dart:io';

import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

part 'database.g.dart';

// テーブルの定義
class Tasks extends Table {
  TextColumn get name => text()();
  BoolColumn get isMemorized => boolean().withDefault(Constant(false))();

  
  Set<Column> get primaryKey => {name};
}

// データベースの接続をオープンする関数
LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'tasks.db'));
    return NativeDatabase(file);
  });
}

// データベースクラスの定義
(tables: [Tasks])
class MyDatabase extends _$MyDatabase {
  MyDatabase() : super(_openConnection());

  
  int get schemaVersion => 1;

  // Taskオブジェクトをデータベースに挿入する非同期関数
  Future insertTask(Task task) => into(tasks).insert(task);

  // データベース内のすべてのTaskオブジェクトを取得する非同期関数
  Future<List<Task>> get allTasks => select(tasks).get();

  // 指定したTaskオブジェクトをデータベース内で更新する非同期関数
  Future updateTask(Task task) => update(tasks).replace(task);

  // 指定したTaskオブジェクトをデータベースから削除する非同期関数
  Future deleteTask(Task task) =>
      (delete(tasks)..where((table) => table.name.equals(task.name))).go();

  // データベース内のすべてのTaskオブジェクトを削除する非同期関数
  Future deleteAllTasks() => delete(tasks).go();

  // 暗記済みのTaskが下になるようにソートしてすべてのTaskオブジェクトを取得する非同期関数
  Future<List<Task>> get allWordsSortedDescending => (select(tasks)
        ..orderBy([(table) => OrderingTerm(expression: table.isMemorized)]))
      .get();

  // 暗記済みのTaskが上になるようにソートしてすべてのTaskオブジェクトを取得する非同期関数
  Future<List<Task>> get allWordsSortedAscending => (select(tasks)
        ..orderBy([
          (table) => OrderingTerm(
              expression: table.isMemorized, mode: OrderingMode.desc)
        ]))
      .get();

  // すべてのTaskのチェックを外すための非同期関数
  Future<void> uncheckAllTasks() async {
    await (update(tasks)..where((table) => table.isMemorized.equals(true)))
        .write(const TasksCompanion(isMemorized: Value(false)));
  }
}

このコードでは、Tasksというテーブルを定義し、MyDatabaseクラスでデータベースの操作を行っています。insertTask関数は、Taskオブジェクトをデータベースに挿入し、allTasks関数はデータベース内のすべてのTaskオブジェクトを取得します。また、updateTask関数は指定したTaskオブジェクトを更新し、deleteTask関数は指定したTaskオブジェクトを削除します。

さらに、暗記済みのTaskをソートするための関数や、すべてのTaskのチェックを外すための関数も提供されています。

このコードを使用することで、シンプルかつ効率的なデータベース操作が可能となります。詳細な使用方法や他の機能については、Driftパッケージのドキュメントを参照してください。

サイドメニュー ShrinkSidemenu

  1. パッケージのインポート
import 'package:shrink_sidemenu/shrink_sidemenu.dart';
  1. メイン画面の状態管理クラスの実装
class _MyHomePageState extends State<MyHomePage> {
  List<Task> _tasksList = []; // タスクのリストを格納する変数
  bool isOpened = false; // サイドメニューが開いているかどうかの状態を管理する変数

  // ... 以下省略
}
  • _MyHomePageStateはMyHomePageの状態管理クラスで、State<MyHomePage>を継承しています。
  • _tasksListはタスクのリストを格納するための変数です。
  • isOpenedはサイドメニューが開いているかどうかの状態を管理するための変数です。

4.サイドメニューのトグルを切り替えるメソッドの実装

	// サイドメニューのトグルを切り替えるメソッド
  toggleMenu([bool end = false]) {
    // サイドメニューのキーを取得する
    final _state =
        end ? _endSideMenuKey.currentState! : _sideMenuKey.currentState!;

    if (_state.isOpened) {
      // サイドメニューが開いている場合、閉じる。
      _state.closeSideMenu();
    } else {
      //閉じている場合、開く。
      _state.openSideMenu();
    }
  }

  • toggleMenuはサイドメニューのトグルを切り替えるメソッドです。
  • end引数を使用して、エンドサイドメニューかどうかを判定します。
  • サイドメニューの状態を取得し、開いている場合は閉じ、閉じている場合は開くように切り替えます。

5.サイドメニューのlistTile

 Widget _SideMenuItems() {
    return SingleChildScrollView(
      padding: const EdgeInsets.symmetric(vertical: 50.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.only(left: 16.0),
          ),
          ListTile(
            onTap: () {
              toggleMenu(true);
            },
            leading: const Icon(Icons.home, size: 35.0, color: Colors.white),
            title: const Text("Home"),
            textColor: Colors.white,
            dense: true,
          ),
          SizedBox(
            height: 20,
          ),
          ListTile(
            onTap: () {
              _sortUpItems();
              toggleMenu(true);
            },
            leading: Icon(Icons.arrow_upward, size: 35.0, color: Colors.white),
            title: const Text("チェック済みを上に"),
            textColor: Colors.white,
            dense: true,
          ),
          SizedBox(
            height: 20,
          ),
          ListTile(
            onTap: () {
              _sortDownItems();
              toggleMenu(true);
            },
            leading: const Icon(Icons.arrow_downward,
                size: 35.0, color: Colors.white),
            title: const Text("チェック済みを下に"),
            textColor: Colors.white,
            dense: true,
          ),
          SizedBox(
            height: 20,
          ),
          ListTile(
            onTap: () {
              _uncheckAllTasks();
              toggleMenu(true);
            },
            leading: const Icon(Icons.check_box_outline_blank,
                size: 35.0, color: Colors.white),
            title: const Text("チェックを全てクリアする"),
            textColor: Colors.white,
            dense: true,
          ),
          SizedBox(
            height: 20,
          ),
          ListTile(
            onTap: () {
              showDialog(
                  context: context,
                  builder: (context) {
                    return AlertDialog(
                      content: Text("全てのリストを削除してもよろしいでしょうか?"),
                      actions: [
                        TextButton(
                          child: Text("はい"),
                          onPressed: () {
                            _allDeleteItem();
                            _getAllTask();
                            Navigator.of(context).pop(true);
                            toggleMenu(true);
                            _showSnackBar(
                                text: "リストを全て削除しました",
                                darkModeBackgroundColor: Colors.yellowAccent,
                                lightModeBackgroundColor:
                                    Colors.deepPurpleAccent);
                          },
                        ),
                        TextButton(
                          child: Text("いいえ"),
                          onPressed: () {
                            //そのまま画面が閉じる
                            Navigator.of(context).pop(false);
                          },
                        )
                      ],
                    );
                  });
            },
            leading: const Icon(Icons.delete, size: 35.0, color: Colors.white),
            title: const Text("リスト全て削除"),
            textColor: Colors.white,
            dense: true,
          ),
          SizedBox(
            height: 20,
          ),
          ListTile(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => SettingsScreen()),
              );
            },
            leading:
                const Icon(Icons.settings, size: 35.0, color: Colors.white),
            title: const Text("設定"),
            textColor: Colors.white,
            dense: true,
          ),
        ],
      ),
    );
  }	

まとめ

この数少ないコードだけでもちゃんと機能するアプリができて感動しました!
次はもっと複雑なアプリに挑戦したいです
最後まで読んでいただきありがとうございました。

Discussion