1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「続かない」エンジニアのための記憶習慣 — 1日数秒で終わる暗記アプリを自作した話

1
Posted at

エンジニアとして働いていると、「覚えておきたいこと」は際限なく増えていく。資格試験の勉強、英語の勉強。手帳へメモ。それらをノートやアプリに書き留めても、次に見返さない。そういう経験は誰にでもあるのではないでしょうか?

続かない理由は意志の弱さではなく、ハードルの高さだと思っている。「まとまった時間を確保して、集中して復習する」という前提設計のツールが多すぎる。エンジニアの日常にそんな時間は簡単には取れない。

そこで、1日数秒で終わるフラッシュカード学習アプリを作った。この記事ではアプリの紹介と、実装の中で工夫した「連続学習日数(ストリーク)の計算ロジック」を解説する。


アプリでできること

Cobo Memo は iOS / Android で動作する無料のフラッシュカード暗記アプリだ。

カードの登録

覚えたい内容を自分でカードとして登録するのが基本だが、それが面倒なら AI カード生成が使える。テキストを貼り付けると Google Gemini がカードを自動生成してくれる。資料のスクリーンショットから OCR でテキストを読み取ってカード化することも可能だ。

他のユーザーが公開している共有デッキをダウンロードするという手もある。AWS、Linux コマンド、英単語など、よく使われるカテゴリはすでに誰かが作っている。

学習体験

学習画面はシンプルに設計してある。間隔反復アルゴリズムによって、苦手なカードほど頻繁に出題される。

疲れた日ややる気が出ない日は、1枚だけ学習して「今日も触ったぞ」という感覚を積み重ねることが目的なので、完璧主義にならなくていい設計にしている。1枚でも学習すると連続学習日数が更新されるゆとり仕様です(笑)やる気がある日にだけ頑張るようにしています。継続することに価値があり!

image.png

image.png

機能について

過去記事に掲載

いますぐためす


実装の話: さらなる甘やかし機能

せっかくなので、実装で工夫したポイントを1つ紹介する。

「1日忘れてもリカバリー可能」な仕様

連続学習日数(ストリーク)は多くの習慣化アプリが採用している機能だが、よくある実装の問題点がある。1日でも空くと即座にゼロに戻る、という厳格な設計だ。

Cobo Memoでは次のルールにした。

  • 今日か昨日まで連続していれば、ストリークは継続
  • 2日以上空いた場合のみリセット

つまり昨日サボっても、今日学習すれば連続扱いになる。何ともゆとり仕様ですが、私のモチベーションを継続するためこの仕様にしています。

これがどうコードに落ちているか見てほしい。

現在の連続学習日数を計算するロジック

// lib/function/utility/db_helper.dart

Future<int> calculateConsecutiveLearningDays(String userId) async {
  final allSummaries = await getAllLearningSummariesLocal();

  // ユーザーの DAILY サマリーのみ抽出
  final userSummaries = allSummaries
      .where((s) => s.userId == userId && s.summaryType == 'DAILY')
      .toList();

  // 日付ごとの学習時間を集計
  Map<String, int> dailyLearningTime = {};
  for (var summary in userSummaries) {
    String dateKey = summary.dateKey; // "YYYY-MM-DD" 形式
    dailyLearningTime[dateKey] =
        (dailyLearningTime[dateKey] ?? 0) + (summary.totalLearningTime ?? 0);
  }

  // 学習時間が 1 秒以上ある日だけを対象にする
  List<String> learningDates = dailyLearningTime.entries
      .where((e) => e.value > 0)
      .map((e) => e.key)
      .toList()
    ..sort((a, b) => b.compareTo(a)); // 降順ソート

  if (learningDates.isEmpty) return 0;

  DateTime now = DateTime.now();
  final todayStr = now.toIso8601String().substring(0, 10);

  // ポイント: 今日学習済みなら「今日」から、未学習なら「昨日」から遡る
  DateTime checkDate = learningDates.contains(todayStr)
      ? now
      : now.subtract(const Duration(days: 1));

  int consecutiveDays = 0;
  for (int i = 0; i < 9999; i++) {
    String checkDateStr = checkDate.toIso8601String().substring(0, 10);
    if (learningDates.contains(checkDateStr)) {
      consecutiveDays++;
      checkDate = checkDate.subtract(const Duration(days: 1));
    } else {
      break; // 連続が途切れた時点で終了
    }
  }

  return consecutiveDays;
}

注目すべきは checkDate の初期値の決め方だ。

DateTime checkDate = learningDates.contains(todayStr)
    ? now
    : now.subtract(const Duration(days: 1));

今日まだ学習していなくても、昨日から遡ることでストリークを生かしておく。昨日学習した記録があれば consecutiveDays はそこからカウントされ、今日学習するとさらに伸びる。2日以上空いていれば break がすぐに発動してゼロになる。

UI 側での通知処理

ホーム画面側では、ストリークの更新をトリガーにダイアログを出す処理がある。

// lib/function/home/home_screen.dart

Future<void> _checkAndUpdateConsecutiveDays() async {
  final today = DateTime.now().toIso8601String().substring(0, 10);
  final lastLearningDate = _prefs.getString(_lastLearningDateKey) ?? '';

  // 今日すでにチェック済みならスキップ
  if (lastLearningDate == today) return;

  final hasLearnedToday = await DBHelper().hasLearnedToday(userIdentifier);
  if (!hasLearnedToday) return;

  final currentStreak =
      await DBHelper().calculateConsecutiveLearningDays(userIdentifier);
  final previousStreak = _prefs.getInt(_lastConsecutiveDaysKey) ?? 0;

  await _prefs.setString(_lastLearningDateKey, today);
  await _prefs.setInt(_lastConsecutiveDaysKey, currentStreak);

  if (currentStreak > previousStreak && currentStreak > 1) {
    // 連続記録が更新されたことをポップアップで通知
    _showConsecutiveDaysUpdateDialog(currentStreak);
  } else if (currentStreak == 1 && previousStreak == 0) {
    // 学習を再開したことを通知
    _showLearningRestartDialog();
  }
}

SharedPreferences でローカルにキャッシュすることで、毎回 DB を読みに行かずに済む。今日すでに処理済みなら即 return するので、ホーム画面を何度開いてもオーバーヘッドはほぼない。


なぜ「完璧を求めない」設計にしたか

継続系アプリが嫌われるとき、大抵こういう瞬間がある。

旅行やら残業やらで1日だけ触れなかった日があり、翌朝アプリを開いたら連続記録がゼロになっている。あの絶望感は経験した人にはわかると思う。「もうどうでもいいか」という気持ちになって、そのままアプリを削除する流れ。

学習の本質は記憶の定着であって、連続記録の維持ではない。ストリークは継続のモチベーションを補助する道具であるべきで、プレッシャーをかける道具であってはいけない。

1日の猶予を持たせることで、「昨日やれなかったけど今日やっておこう」という自然な行動が生まれやすくなる。完璧にやれない日があっても、そこで止まらず続けることの方が長期的には重要だという判断だ。

同じ思想は学習体験全体にも通じている。1セッションのカード枚数を少なく設定できるのも、「今日は3枚だけ」で済ませることを許容するためだ。毎日100枚より、毎日5枚を1年続ける方が確実に記憶に残る。


継続は力です

資格試験も、新しい技術のキャッチアップも、続けること自体が一番難しい。ハードルを下げ、1日数秒でも触れる習慣を作ることが、長期的な知識の積み上げにつながる。

Cobo Memo はそのための道具として作った。学習ステータスの完全定着を目指して、無理のない暗記学習を続けてほしい。

おわりに

最後まで読んでいただきありがとうございました。

COBO MEMOの開発進捗や、個人開発・資格学習についての発信をXでしています。
よかったらフォローしてください。

フィードバックや機能リクエストはコメント欄へ記入ください!

1
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?