dramaling-vocab-learning/00_starter/old/mvp-technical-spec.md

965 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<User> 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<User> signIn({
required String email,
required String password,
}) async {
// 實作...
}
// 登出
Future<void> signOut() async {
await _firebaseAuth.signOut();
}
// 取得當前用戶
Stream<User?> 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<CardModel> 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<CardModel> _cardsBox;
late Box _userBox;
late Box _settingsBox;
Future<void> init() async {
await Hive.initFlutter();
// 註冊 Adapters
Hive.registerAdapter(CardModelAdapter());
// 開啟 Boxes
_cardsBox = await Hive.openBox<CardModel>(_cardsBoxName);
_userBox = await Hive.openBox(_userBoxName);
_settingsBox = await Hive.openBox(_settingsBoxName);
}
// 詞卡操作
Future<void> saveCard(CardModel card) async {
await _cardsBox.put(card.id, card);
}
List<CardModel> getAllCards() {
return _cardsBox.values.toList();
}
List<CardModel> getTodayReviewCards() {
final today = DateTime.now();
return _cardsBox.values.where((card) {
return card.nextReviewDate.isBefore(today) ||
card.nextReviewDate.isAtSameMomentAs(today);
}).toList();
}
Future<void> updateCard(CardModel card) async {
await _cardsBox.put(card.id, card);
}
Future<void> deleteCard(String cardId) async {
await _cardsBox.delete(cardId);
}
// 設定操作
Future<void> saveSetting(String key, dynamic value) async {
await _settingsBox.put(key, value);
}
T? getSetting<T>(String key) {
return _settingsBox.get(key) as T?;
}
// 清除所有資料
Future<void> 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<CardModel> _cards = [];
List<CardModel> _todayReviewCards = [];
bool _isLoading = false;
String? _error;
CardProvider({
required CardRepository repository,
required GeminiService geminiService,
required StorageService storageService,
}) : _repository = repository,
_geminiService = geminiService,
_storageService = storageService;
List<CardModel> get cards => _cards;
List<CardModel> get todayReviewCards => _todayReviewCards;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> 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<void> 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<List<Map<String, dynamic>>> getCards(String userId) async {
final response = await _supabase
.from('cards')
.select()
.eq('user_id', userId)
.order('created_at', ascending: false);
return List<Map<String, dynamic>>.from(response);
}
Future<void> createCard(Map<String, dynamic> card) async {
await _supabase.from('cards').insert(card);
}
Future<void> updateCard(String id, Map<String, dynamic> updates) async {
await _supabase
.from('cards')
.update(updates)
.eq('id', id);
}
Future<void> 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<void> 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<T> trackPerformance<T>(
String name,
Future<T> 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),
);
```