22 KiB
22 KiB
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 前端技術棧
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 後端服務
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 認證模組
// 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 詞卡生成服務
// 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 間隔重複演算法
// 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 本地存儲服務
// 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 架構
// 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 客戶端
// 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 例外定義
// 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 錯誤處理器
// 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 圖片快取策略
// 使用 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 列表優化
// 使用 ListView.builder 避免一次渲染所有項目
ListView.builder(
itemCount: cards.length,
itemBuilder: (context, index) {
return CardItem(card: cards[index]);
},
// 優化捲動效能
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
cacheExtent: 100,
)
8.3 防抖處理
// 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 管理
// 使用環境變數
// .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 資料驗證
// 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 單元測試
// 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 測試
// 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 環境配置
// 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 建置腳本
# 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 設置
// 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 效能追蹤
// 追蹤 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),
);