# LinguaForge MVP 技術規格文件 ## 1. 技術架構總覽 ``` ┌─────────────────────────────────────────┐ │ Flutter App │ │ ┌─────────────────────────────────┐ │ │ │ Presentation Layer │ │ │ │ (Screens & Widgets) │ │ │ └─────────────────────────────────┘ │ │ ┌─────────────────────────────────┐ │ │ │ Business Logic │ │ │ │ (Provider State Management) │ │ │ └─────────────────────────────────┘ │ │ ┌─────────────────────────────────┐ │ │ │ Data Layer │ │ │ │ (Repositories & Services) │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘ ↓ ┌───────────────────────┐ │ External Services │ ├───────────────────────┤ │ • Firebase Auth │ │ • Supabase Database │ │ • Gemini API │ │ • Local Storage │ └───────────────────────┘ ``` ## 2. 技術棧詳細規格 ### 2.1 前端技術棧 ```yaml Flutter: 版本: 3.16.0+ Dart: 3.2.0+ 核心套件: 狀態管理: provider: ^6.1.0 網路請求: dio: ^5.4.0 dio_retry: ^4.1.0 本地存儲: hive: ^2.2.3 hive_flutter: ^1.1.0 認證: firebase_auth: ^4.15.0 google_sign_in: ^6.1.0 資料庫: supabase_flutter: ^2.0.0 UI 組件: flutter_screenutil: ^5.9.0 shimmer: ^3.0.0 lottie: ^2.7.0 工具類: intl: ^0.18.0 uuid: ^4.2.0 connectivity_plus: ^5.0.0 ``` ### 2.2 後端服務 ```yaml Supabase: 資料庫: PostgreSQL 15 即時訂閱: Realtime 檔案存儲: Storage Edge Functions: Deno Firebase: 認證: Firebase Auth 崩潰報告: Crashlytics 效能監控: Performance 分析: Analytics ``` ## 3. 專案結構 ``` lib/ ├── main.dart ├── app.dart ├── config/ │ ├── constants.dart │ ├── theme.dart │ └── routes.dart ├── core/ │ ├── errors/ │ │ ├── exceptions.dart │ │ └── failures.dart │ ├── utils/ │ │ ├── validators.dart │ │ └── formatters.dart │ └── extensions/ │ └── string_extensions.dart ├── data/ │ ├── models/ │ │ ├── user_model.dart │ │ ├── card_model.dart │ │ └── review_model.dart │ ├── repositories/ │ │ ├── auth_repository.dart │ │ ├── card_repository.dart │ │ └── review_repository.dart │ └── services/ │ ├── api_service.dart │ ├── gemini_service.dart │ └── storage_service.dart ├── domain/ │ ├── entities/ │ │ ├── user.dart │ │ ├── card.dart │ │ └── review.dart │ └── usecases/ │ ├── auth/ │ ├── cards/ │ └── review/ ├── presentation/ │ ├── providers/ │ │ ├── auth_provider.dart │ │ ├── card_provider.dart │ │ └── review_provider.dart │ ├── screens/ │ │ ├── auth/ │ │ ├── home/ │ │ ├── cards/ │ │ └── review/ │ └── widgets/ │ ├── common/ │ └── cards/ └── l10n/ └── app_zh.arb ``` ## 4. 核心模組實作 ### 4.1 認證模組 ```dart // lib/data/repositories/auth_repository.dart class AuthRepository { final FirebaseAuth _firebaseAuth; final SupabaseClient _supabase; AuthRepository({ required FirebaseAuth firebaseAuth, required SupabaseClient supabase, }) : _firebaseAuth = firebaseAuth, _supabase = supabase; // 註冊 Future signUp({ required String email, required String password, String? nickname, }) async { try { // 1. Firebase 註冊 final credential = await _firebaseAuth.createUserWithEmailAndPassword( email: email, password: password, ); // 2. 取得 ID Token final idToken = await credential.user?.getIdToken(); // 3. Supabase 創建用戶資料 await _supabase.from('users').insert({ 'id': credential.user?.uid, 'email': email, 'nickname': nickname, 'created_at': DateTime.now().toIso8601String(), }); return User( id: credential.user!.uid, email: email, nickname: nickname, ); } catch (e) { throw AuthException(e.toString()); } } // 登入 Future signIn({ required String email, required String password, }) async { // 實作... } // 登出 Future signOut() async { await _firebaseAuth.signOut(); } // 取得當前用戶 Stream get authStateChanges { return _firebaseAuth.authStateChanges().map((firebaseUser) { if (firebaseUser == null) return null; return User( id: firebaseUser.uid, email: firebaseUser.email!, ); }); } } ``` ### 4.2 AI 詞卡生成服務 ```dart // lib/data/services/gemini_service.dart class GeminiService { static const String _apiKey = 'YOUR_API_KEY'; static const String _baseUrl = 'https://generativelanguage.googleapis.com'; final Dio _dio; GeminiService() : _dio = Dio(BaseOptions( baseUrl: _baseUrl, headers: { 'Content-Type': 'application/json', }, )); Future generateCard({ required String sentence, required String targetWord, }) async { try { final prompt = _buildPrompt(sentence, targetWord); final response = await _dio.post( '/v1beta/models/gemini-pro:generateContent', queryParameters: {'key': _apiKey}, data: { 'contents': [{ 'parts': [{ 'text': prompt, }] }], 'generationConfig': { 'temperature': 0.7, 'maxOutputTokens': 1024, }, }, ); final content = response.data['candidates'][0]['content']['parts'][0]['text']; final json = jsonDecode(_extractJson(content)); return CardModel.fromJson(json); } catch (e) { throw GeminiException('Failed to generate card: $e'); } } String _buildPrompt(String sentence, String targetWord) { return ''' 你是一個專業的英語教學助手。請根據以下資訊生成詞彙學習卡片。 原始句子:$sentence 目標單字:$targetWord 請以純 JSON 格式回應(不要包含其他文字): { "word": "目標單字", "pronunciation": "IPA音標", "definition": "繁體中文定義(簡潔明瞭)", "partOfSpeech": "詞性(noun/verb/adjective等)", "examples": [ { "english": "英文例句1", "chinese": "中文翻譯1" }, { "english": "英文例句2", "chinese": "中文翻譯2" } ], "difficulty": "難度等級(beginner/intermediate/advanced)" } '''; } String _extractJson(String content) { // 提取 JSON 部分 final start = content.indexOf('{'); final end = content.lastIndexOf('}') + 1; return content.substring(start, end); } } ``` ### 4.3 間隔重複演算法 ```dart // lib/core/algorithms/sm2_algorithm.dart class SM2Algorithm { static const double _minEasinessFactor = 1.3; static const double _defaultEasinessFactor = 2.5; static ReviewResult calculate({ required int quality, // 1-5 required double currentEF, required int currentInterval, required int repetitions, }) { assert(quality >= 1 && quality <= 5); double newEF = currentEF; int newInterval = currentInterval; int newRepetitions = repetitions; if (quality < 3) { // 回答錯誤,重置 newInterval = 1; newRepetitions = 0; } else { // 計算新的 EF newEF = currentEF + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)); newEF = max(_minEasinessFactor, newEF); // 計算新的間隔 if (repetitions == 0) { newInterval = 1; } else if (repetitions == 1) { newInterval = 6; } else { newInterval = (currentInterval * newEF).round(); } newRepetitions++; } return ReviewResult( easinessFactor: newEF, interval: newInterval, repetitions: newRepetitions, nextReviewDate: DateTime.now().add(Duration(days: newInterval)), ); } } class ReviewResult { final double easinessFactor; final int interval; final int repetitions; final DateTime nextReviewDate; ReviewResult({ required this.easinessFactor, required this.interval, required this.repetitions, required this.nextReviewDate, }); } ``` ### 4.4 本地存儲服務 ```dart // lib/data/services/storage_service.dart class StorageService { static const String _cardsBoxName = 'cards'; static const String _userBoxName = 'user'; static const String _settingsBoxName = 'settings'; late Box _cardsBox; late Box _userBox; late Box _settingsBox; Future init() async { await Hive.initFlutter(); // 註冊 Adapters Hive.registerAdapter(CardModelAdapter()); // 開啟 Boxes _cardsBox = await Hive.openBox(_cardsBoxName); _userBox = await Hive.openBox(_userBoxName); _settingsBox = await Hive.openBox(_settingsBoxName); } // 詞卡操作 Future saveCard(CardModel card) async { await _cardsBox.put(card.id, card); } List getAllCards() { return _cardsBox.values.toList(); } List getTodayReviewCards() { final today = DateTime.now(); return _cardsBox.values.where((card) { return card.nextReviewDate.isBefore(today) || card.nextReviewDate.isAtSameMomentAs(today); }).toList(); } Future updateCard(CardModel card) async { await _cardsBox.put(card.id, card); } Future deleteCard(String cardId) async { await _cardsBox.delete(cardId); } // 設定操作 Future saveSetting(String key, dynamic value) async { await _settingsBox.put(key, value); } T? getSetting(String key) { return _settingsBox.get(key) as T?; } // 清除所有資料 Future clearAll() async { await _cardsBox.clear(); await _userBox.clear(); await _settingsBox.clear(); } } ``` ## 5. 狀態管理 ### 5.1 Provider 架構 ```dart // lib/presentation/providers/card_provider.dart class CardProvider extends ChangeNotifier { final CardRepository _repository; final GeminiService _geminiService; final StorageService _storageService; List _cards = []; List _todayReviewCards = []; bool _isLoading = false; String? _error; CardProvider({ required CardRepository repository, required GeminiService geminiService, required StorageService storageService, }) : _repository = repository, _geminiService = geminiService, _storageService = storageService; List get cards => _cards; List get todayReviewCards => _todayReviewCards; bool get isLoading => _isLoading; String? get error => _error; Future generateCard({ required String sentence, required String targetWord, }) async { _setLoading(true); _error = null; try { // 1. 呼叫 Gemini API final card = await _geminiService.generateCard( sentence: sentence, targetWord: targetWord, ); // 2. 儲存到遠端 await _repository.createCard(card); // 3. 儲存到本地 await _storageService.saveCard(card); // 4. 更新狀態 _cards.add(card); notifyListeners(); } catch (e) { _error = e.toString(); notifyListeners(); } finally { _setLoading(false); } } Future loadTodayReviewCards() async { _setLoading(true); try { // 優先從本地載入 _todayReviewCards = _storageService.getTodayReviewCards(); notifyListeners(); // 背景同步遠端資料 final remoteCards = await _repository.getTodayReviewCards(); _todayReviewCards = remoteCards; notifyListeners(); } catch (e) { _error = e.toString(); notifyListeners(); } finally { _setLoading(false); } } void _setLoading(bool value) { _isLoading = value; notifyListeners(); } } ``` ## 6. 網路層設計 ### 6.1 API 客戶端 ```dart // lib/data/services/api_service.dart class ApiService { late Dio _dio; final SupabaseClient _supabase; ApiService({required SupabaseClient supabase}) : _supabase = supabase { _dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); // 加入重試機制 _dio.interceptors.add( RetryInterceptor( dio: _dio, retries: 3, retryDelays: const [ Duration(seconds: 1), Duration(seconds: 2), Duration(seconds: 3), ], ), ); // 加入日誌 if (kDebugMode) { _dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: true, )); } } // Supabase 操作封裝 Future>> getCards(String userId) async { final response = await _supabase .from('cards') .select() .eq('user_id', userId) .order('created_at', ascending: false); return List>.from(response); } Future createCard(Map card) async { await _supabase.from('cards').insert(card); } Future updateCard(String id, Map updates) async { await _supabase .from('cards') .update(updates) .eq('id', id); } Future deleteCard(String id) async { await _supabase .from('cards') .delete() .eq('id', id); } } ``` ## 7. 錯誤處理 ### 7.1 例外定義 ```dart // lib/core/errors/exceptions.dart class AppException implements Exception { final String message; final String? code; AppException(this.message, [this.code]); } class AuthException extends AppException { AuthException(String message, [String? code]) : super(message, code); } class NetworkException extends AppException { NetworkException(String message, [String? code]) : super(message, code); } class GeminiException extends AppException { GeminiException(String message, [String? code]) : super(message, code); } class StorageException extends AppException { StorageException(String message, [String? code]) : super(message, code); } ``` ### 7.2 錯誤處理器 ```dart // lib/core/utils/error_handler.dart class ErrorHandler { static String getMessage(dynamic error) { if (error is AppException) { return error.message; } else if (error is FirebaseAuthException) { return _getFirebaseAuthMessage(error.code); } else if (error is DioException) { return _getDioMessage(error); } else { return '發生未知錯誤,請稍後再試'; } } static String _getFirebaseAuthMessage(String code) { switch (code) { case 'email-already-in-use': return '此 Email 已被註冊'; case 'invalid-email': return 'Email 格式不正確'; case 'weak-password': return '密碼強度不足'; case 'user-not-found': return '找不到此用戶'; case 'wrong-password': return '密碼錯誤'; default: return '認證失敗,請稍後再試'; } } static String _getDioMessage(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return '連線逾時,請檢查網路'; case DioExceptionType.connectionError: return '無法連線到伺服器'; default: return '網路錯誤,請稍後再試'; } } } ``` ## 8. 效能優化 ### 8.1 圖片快取策略 ```dart // 使用 cached_network_image CachedNetworkImage( imageUrl: imageUrl, placeholder: (context, url) => Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( color: Colors.white, ), ), errorWidget: (context, url, error) => Icon(Icons.error), cacheKey: imageUrl, // 使用 URL 作為快取鍵 maxHeightDiskCache: 1000, maxWidthDiskCache: 1000, ) ``` ### 8.2 列表優化 ```dart // 使用 ListView.builder 避免一次渲染所有項目 ListView.builder( itemCount: cards.length, itemBuilder: (context, index) { return CardItem(card: cards[index]); }, // 優化捲動效能 addAutomaticKeepAlives: false, addRepaintBoundaries: false, cacheExtent: 100, ) ``` ### 8.3 防抖處理 ```dart // lib/core/utils/debouncer.dart class Debouncer { final int milliseconds; Timer? _timer; Debouncer({required this.milliseconds}); void run(VoidCallback action) { _timer?.cancel(); _timer = Timer(Duration(milliseconds: milliseconds), action); } void dispose() { _timer?.cancel(); } } // 使用範例 final _debouncer = Debouncer(milliseconds: 500); onSearchChanged(String query) { _debouncer.run(() { // 執行搜尋 performSearch(query); }); } ``` ## 9. 安全性考量 ### 9.1 API Key 管理 ```dart // 使用環境變數 // .env 檔案(加入 .gitignore) GEMINI_API_KEY=your_api_key_here SUPABASE_URL=your_supabase_url SUPABASE_ANON_KEY=your_anon_key // 載入環境變數 import 'package:flutter_dotenv/flutter_dotenv.dart'; Future main() async { await dotenv.load(); runApp(MyApp()); } // 使用 final apiKey = dotenv.env['GEMINI_API_KEY']!; ``` ### 9.2 資料驗證 ```dart // lib/core/utils/validators.dart class Validators { static String? email(String? value) { if (value == null || value.isEmpty) { return '請輸入 Email'; } final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!regex.hasMatch(value)) { return 'Email 格式不正確'; } return null; } static String? password(String? value) { if (value == null || value.isEmpty) { return '請輸入密碼'; } if (value.length < 8) { return '密碼至少需要 8 個字元'; } return null; } static String? sentence(String? value) { if (value == null || value.isEmpty) { return '請輸入句子'; } if (value.length > 200) { return '句子不能超過 200 個字元'; } return null; } } ``` ## 10. 測試策略 ### 10.1 單元測試 ```dart // test/algorithms/sm2_algorithm_test.dart void main() { group('SM2Algorithm', () { test('should reset interval when quality < 3', () { final result = SM2Algorithm.calculate( quality: 2, currentEF: 2.5, currentInterval: 10, repetitions: 5, ); expect(result.interval, equals(1)); expect(result.repetitions, equals(0)); }); test('should increase interval for good performance', () { final result = SM2Algorithm.calculate( quality: 4, currentEF: 2.5, currentInterval: 6, repetitions: 2, ); expect(result.interval, greaterThan(6)); expect(result.repetitions, equals(3)); }); }); } ``` ### 10.2 Widget 測試 ```dart // test/widgets/card_item_test.dart void main() { testWidgets('CardItem displays word and definition', (tester) async { final card = CardModel( id: '1', word: 'test', definition: '測試', ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: CardItem(card: card), ), ), ); expect(find.text('test'), findsOneWidget); expect(find.text('測試'), findsOneWidget); }); } ``` ## 11. 部署配置 ### 11.1 環境配置 ```dart // lib/config/environment.dart class Environment { static const String development = 'development'; static const String production = 'production'; static String get current => const String.fromEnvironment('ENV', defaultValue: development); static bool get isDevelopment => current == development; static bool get isProduction => current == production; static String get apiUrl { switch (current) { case production: return 'https://api.linguaforge.com'; default: return 'https://dev-api.linguaforge.com'; } } } ``` ### 11.2 建置腳本 ```bash # scripts/build.sh #!/bin/bash # 開發版 flutter build apk --debug --dart-define=ENV=development # 生產版 flutter build apk --release --dart-define=ENV=production flutter build ios --release --dart-define=ENV=production # 混淆 flutter build apk --release --obfuscate --split-debug-info=./debug-info ``` ## 12. 監控與分析 ### 12.1 Firebase 設置 ```dart // lib/main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Firebase 初始化 await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); // Crashlytics 設置 FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; // 捕捉非同步錯誤 PlatformDispatcher.instance.onError = (error, stack) { FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); return true; }; runApp(MyApp()); } ``` ### 12.2 效能追蹤 ```dart // 追蹤 API 呼叫 Future trackPerformance( String name, Future Function() operation, ) async { final trace = FirebasePerformance.instance.newTrace(name); await trace.start(); try { final result = await operation(); trace.setMetric('success', 1); return result; } catch (e) { trace.setMetric('error', 1); rethrow; } finally { await trace.stop(); } } // 使用 final card = await trackPerformance( 'generate_card', () => _geminiService.generateCard(sentence: sentence, targetWord: word), ); ```