0
0

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×Firebase】コーヒー焙煎アプリに目標ベース成功率システムを実装してアプリ全体を最適化した話 #7

Posted at

この記事は、Flutter + Firebase + AI で作るコーヒー焙煎ログアプリ開発シリーズの第7回です。

前回の記事: 【Flutter×Firebase】他ユーザープロフィール機能で拡がるコーヒーコミュニティを作った話

🎯 今回実装したもの

今回は大きく3つの改善を行いました:

  1. 目標ベース成功率システム - 味覚評価から目標達成度ベースへの変更
  2. アプリ全体の最適化 - 不要コード削除とサービス統合
  3. 統計機能の完全統合 - 高度なキャッシュシステムとパフォーマンス向上

📊 成功率計算の革新:味覚評価から目標達成へ

従来の問題点

これまでの成功率計算は味覚評価の平均値ベースでした:

// 従来の方式:味覚評価平均が3.0以上で成功
final avgTaste = (acidity + sweetness + bitterness) / 3;
if (avgTaste >= 3.0) successfulRoasts++;

この方式には以下の問題がありました:

  • 主観的な評価に依存
  • 目標が不明確
  • スキル向上の指標として不適切

目標ベースシステムの実装

新しいシステムでは、焙煎前に目標フレーバープロファイルを設定し、実際の結果との誤差で成功を判定します:

// 新方式:目標との誤差±0.5以内で成功
final targetAcidity = (data['targetAcidity'] as num?)?.toDouble();
final targetSweetness = (data['targetSweetness'] as num?)?.toDouble(); 
final targetBitterness = (data['targetBitterness'] as num?)?.toDouble();

if (targetAcidity != null && targetSweetness != null && targetBitterness != null) {
  final acidityDiff = (acidity - targetAcidity).abs();
  final sweetnessDiff = (sweetness - targetSweetness).abs();
  final bitternessDiff = (bitterness - targetBitterness).abs();
  final overallError = (acidityDiff + sweetnessDiff + bitternessDiff) / 3;
  
  if (overallError <= 0.5) successfulRoasts++;
}

目標設定UIの実装

焙煎セッション画面に目標設定スライダーを追加:

Widget _buildTargetFlavorSlider(String label, double value, ValueChanged<double> onChanged) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        '$label: ${value.toStringAsFixed(1)}',
        style: const TextStyle(fontWeight: FontWeight.bold),
      ),
      Slider(
        value: value,
        min: 1.0,
        max: 5.0,
        divisions: 8,
        activeColor: AppTheme.fire,
        onChanged: onChanged,
      ),
    ],
  );
}

🚀 アプリ全体の最適化

不要ファイルの削除

以下の3つのファイルを削除してコードベースをクリーンアップ:

  • lib/services/unified_skill_service.dart - 統一スキルサービス
  • lib/screens/roasting_session_screen.dart - 古い焙煎セッション画面
  • lib/services/user_statistics_service.dart - 重複する統計サービス

サービスの統合

UserStatisticsServiceRoastStatisticsService に統合し、重複するFirestoreクエリを削除:

// 統合前:2つの異なるサービス
final userStats = await UserStatisticsService.getUserStatistics(userId);
final roastStats = await RoastStatisticsService().getUserStatistics();

// 統合後:単一のサービス
final statistics = await RoastStatisticsService().getUserStatistics();
// コミュニティ投稿数も含む統合データを返す

⚡ 高度なキャッシュシステムの実装

2層キャッシュ戦略

ユーザータイプに応じて異なるキャッシュ期間を設定:

class _CacheEntry {
  final RoastStatistics statistics;
  final DateTime createdAt;
  final bool isOtherUser;

  bool isValid() {
    final validDuration = isOtherUser 
        ? Duration(minutes: 2)  // 他ユーザー:短時間キャッシュ
        : Duration(minutes: 5); // 自分:長時間キャッシュ
    
    return DateTime.now().difference(createdAt).compareTo(validDuration) < 0;
  }
}

LRU削除とサイズ制限

メモリ使用量を制御するため、最大20エントリの制限とLRU削除を実装:

static void _addToCache(String userId, RoastStatistics statistics, bool isOtherUser) {
  // キャッシュサイズが上限に達している場合、最古のエントリを削除
  if (_cache.length >= _maxCacheSize) {
    _cleanupOldCache();
  }

  final key = isOtherUser ? 'other_$userId' : userId;
  _cache[key] = _CacheEntry(
    statistics: statistics,
    createdAt: DateTime.now(),
    isOtherUser: isOtherUser,
  );
}

static void _cleanupOldCache() {
  // 期限切れエントリを削除
  final keysToRemove = _cache.entries
      .where((entry) => !entry.value.isValid())
      .map((entry) => entry.key)
      .toList();
  
  for (final key in keysToRemove) {
    _cache.remove(key);
  }

  // まだ上限を超えている場合、最古のエントリを削除
  if (_cache.length >= _maxCacheSize) {
    final oldestKey = _cache.entries
        .reduce((a, b) => a.value.createdAt.isBefore(b.value.createdAt) ? a : b)
        .key;
    _cache.remove(oldestKey);
  }
}

🎮 チャレンジシステムの刷新

レベルベースから目標達成ベースへ全面的に変更しました。

初心者レベル(許容誤差±0.5)

static Map<String, dynamic> _createBeginnerTargetChallenge() {
  return {
    'title': '初めての目標設定チャレンジ',
    'description': '酸味3.0、甘味3.5、苦味3.0を±0.5以内で達成してみましょう',
    'targetAcidity': 3.0,
    'targetSweetness': 3.5,
    'targetBitterness': 3.0,
    'tolerance': 0.5,
  };
}

中級レベル(許容誤差±0.3)

static Map<String, dynamic> _createIntermediateHighPrecisionChallenge() {
  return {
    'title': '高精度目標達成',
    'description': '±0.3以内の高精度で目標を達成してください',
    'tolerance': 0.3,
  };
}

上級レベル(許容誤差±0.2)

static Map<String, dynamic> _createAdvancedExtremePrecisionChallenge() {
  return {
    'title': 'エクストリーム精度チャレンジ',
    'description': '±0.2以内の超高精度で目標を達成してください',
    'tolerance': 0.2,
  };
}

📈 パフォーマンス向上の成果

最適化結果

  • キャッシュヒット率: 最大90%の応答時間短縮
  • メモリ使用量: サイズ制限により安定化
  • 削除されたコード: 約200行
  • 解消されたエラー: コンパイルエラー3件、Lint警告全て

ユーザー体験の向上

  • 成功率の意味向上: 自分の目標達成度が明確に
  • リアルタイム統計: コミュニティ投稿数の正確な表示
  • 高速レスポンス: キャッシュによる即座の統計表示

🔄 後方互換性の確保

古いデータとの互換性も維持:

if (targetAcidity != null && targetSweetness != null && targetBitterness != null) {
  // 新方式:目標ベースの判定
  final overallError = (acidityDiff + sweetnessDiff + bitternessDiff) / 3;
  if (overallError <= 0.5) successfulRoasts++;
} else {
  // 旧方式:味覚評価ベース(後方互換性)
  final avgTaste = (acidity + sweetness + bitterness) / 3;
  if (avgTaste >= 3.0) successfulRoasts++;
}

🛠️ 実装時の技術的課題と解決

1. メソッドスコープ問題

問題: _buildTargetFlavorSliderが間違ったクラスで定義されていた
解決: 正しいクラス(_IntegratedRoastSessionScreenState)に移動

2. キャッシュシステムの型不整合

問題: _CacheEntryRoastStatisticsの型管理
解決: 厳密な型管理で安全性を確保

3. コミュニティ投稿数の統合

問題: RoastStatisticsコンストラクタにcommunityPostsパラメータが不足
解決: 必要なパラメータを追加し、統合メソッドを実装

🎉 まとめ

今回の実装により、以下の大きな改善を達成しました:

  1. 明確な目標設定: ユーザーが具体的な目標を持って焙煎に取り組める
  2. 客観的な成功指標: 主観的評価から客観的な達成度測定へ
  3. パフォーマンス大幅向上: キャッシュシステムによる高速化
  4. コードの品質向上: 重複削除と統合による保守性向上

次回は、目標達成の可視化機能やAIによる目標推奨機能の実装を予定しています。


シリーズ記事一覧

  1. 企画・設計編
  2. Firebase環境構築編
  3. 基本機能実装編
  4. AI統合編
  5. コミュニティ機能編
  6. 他ユーザープロフィール機能編
  7. 目標ベース成功率システム・最適化編 ← 今回
  8. 次回予告: 目標達成可視化機能編

📚 参考リソース

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?