📐 代碼規範
本文檔定義了 Valo 專案的編碼標準、風格指南和最佳實踐,確保代碼品質和團隊協作效率。
🎯 總體原則
核心理念
- 可讀性優先: 代碼應該易於理解和維護
- 一致性: 遵循統一的風格和模式
- 簡潔性: 避免不必要的複雜度
- 效能意識: 考慮性能影響
- 安全性: 遵循安全最佳實踐
專案特色
- 高內聚組件: Widget 可包含業務邏輯
- 實用導向: 優先考慮開發效率
- 回調模式: 使用回調參數進行組件通信
📁 文件組織
目錄結構原則
lib/
├── components/ # 通用基礎組件(無業務邏輯)
├── config/ # 全域配置
├── modal/ # 純數據模型
├── modalView/ # UI相關數據模型
├── presentation/ # 屏幕和路由
├── services/ # 業務邏輯服務
├── utils/ # 工具類
└── widgets/ # 業務組件(按領域組織)
├── friend/
├── group/
├── conversation/
└── chat/
文件命名
Dart 文件
- 使用
snake_case
- 描述性且簡潔
- 例如:
friend_request_button.dart
類別命名
- 使用
PascalCase
- Widget 以用途命名,Service 以功能命名
- 例如:
FriendRequestButton
,ChatService
Import 順序
// 1. Dart 核心庫
import 'dart:async';
import 'dart:convert';
// 2. Flutter 框架
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// 3. 第三方套件(按字母順序)
import 'package:easy_localization/easy_localization.dart';
import 'package:provider/provider.dart';
// 4. 專案內部(按層級)
import '../../../config/valo.dart';
import '../../../services/chat_service.dart';
import '../../../utils/toast_util.dart';
import '../../components/buttons/primary_button.dart';
// 5. 相對路徑(最後)
import './friend_avatar.dart';
🎨 UI 組件規範
Widget 設計原則
1. 使用 const 建構函數
// ✅ 好的做法
class FriendAvatar extends StatelessWidget {
const FriendAvatar({
Key? key,
required this.userId,
this.size = 40.0,
}) : super(key: key);
final String userId;
final double size;
}
// ❌ 避免
class FriendAvatar extends StatelessWidget {
FriendAvatar({Key? key, required this.userId}) : super(key: key);
}
2. 使用 nil 代替空 Widget
// ✅ 好的做法
return isLoading
? const CircularProgressIndicator()
: nil;
// ❌ 避免
return isLoading
? CircularProgressIndicator()
: SizedBox();
3. 組件化而非私有方法
// ✅ 好的做法 - 創建獨立組件
class FriendListTile extends StatelessWidget {
const FriendListTile({Key? key, required this.friend}) : super(key: key);
final Friend friend;
Widget build(BuildContext context) {
return ListTile(
leading: FriendAvatar(userId: friend.userId),
title: FriendDisplayName(userId: friend.userId),
trailing: FriendOnlineStatus(userId: friend.userId),
);
}
}
// ❌ 避免 - 私有建構方法
class FriendList extends StatelessWidget {
Widget _buildFriendTile(Friend friend) { // 避免這種模式
return ListTile(/* ... */);
}
}
顏色系統
使用 ValoColor 靜態變量
// ✅ 好的做法
Container(
color: ValoColor.primary,
child: Text(
'Hello',
style: TextStyle(color: ValoColor.white.withAlpha(230)),
),
)
// ❌ 避免
Container(
color: Color(0xFF1976D2), // 硬編碼顏色
child: Text(
'Hello',
style: TextStyle(color: Colors.white.withOpacity(0.9)), // 使用 withOpacity
),
)
響應式設計
使用 MediaQuery 和 LayoutBuilder
// ✅ 響應式設計
class ResponsiveWidget extends StatelessWidget {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return _buildTabletLayout();
}
return _buildMobileLayout();
},
);
}
}
🔧 狀態管理
Provider 使用規範
1. 使用 Selector 優化性能
// ✅ 好的做法 - 使用 Selector
Selector<UserState, String>(
selector: (context, userState) => userState.currentUser.nickname,
builder: (context, nickname, child) {
return Text(nickname);
},
)
// ❌ 避免 - 使用 Consumer 監聽整個狀態
Consumer<UserState>(
builder: (context, userState, child) {
return Text(userState.currentUser.nickname); // 會在整個狀態變化時重建
},
)
2. 正確使用 read() 和 watch()
// ✅ 好的做法
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
// 在 build 中使用 watch
final user = context.watch<UserState>().currentUser;
return ElevatedButton(
onPressed: () {
// 在事件處理中使用 read
context.read<ChatService>().sendMessage('Hello');
},
child: Text(user.nickname),
);
}
}
業務邏輯封裝
高內聚組件設計
// ✅ 本專案推薦做法 - 業務邏輯在組件內
class FriendRequestButton extends StatefulWidget {
const FriendRequestButton({
Key? key,
required this.userId,
required this.onSuccess,
this.onError,
this.onLoading,
}) : super(key: key);
final String userId;
final VoidCallback onSuccess;
final Function(String)? onError;
final Function(bool)? onLoading;
State<FriendRequestButton> createState() => _FriendRequestButtonState();
}
class _FriendRequestButtonState extends State<FriendRequestButton> {
bool _isLoading = false;
Future<void> _sendFriendRequest() async {
setState(() => _isLoading = true);
widget.onLoading?.call(true);
try {
await context.read<ChatService>().sendFriendRequest(widget.userId);
widget.onSuccess.call();
ToastUtil.showSuccess('friend.request_sent'.tr());
} catch (e) {
widget.onError?.call(e.toString());
ToastUtil.showError('friend.request_failed'.tr());
} finally {
if (mounted) {
setState(() => _isLoading = false);
widget.onLoading?.call(false);
}
}
}
Widget build(BuildContext context) {
return PrimaryButton(
text: 'friend.send_request'.tr(),
isLoading: _isLoading,
onPressed: _sendFriendRequest,
);
}
}
📱 性能優化
循環和集合操作
1. 使用 for 循環替代 forEach/map
// ✅ 好的做法
List<Widget> buildFriendList(List<Friend> friends) {
final widgets = <Widget>[];
for (final friend in friends) {
widgets.add(FriendListTile(friend: friend));
}
return widgets;
}
// ❌ 避免(性能較差)
List<Widget> buildFriendList(List<Friend> friends) {
return friends.map((friend) => FriendListTile(friend: friend)).toList();
}
2. ListView.builder 用於大列表
// ✅ 好的做法
ListView.builder(
itemCount: friends.length,
itemBuilder: (context, index) {
return FriendListTile(friend: friends[index]);
},
)
// ❌ 避免(大列表時性能問題)
Column(
children: friends.map((friend) => FriendListTile(friend: friend)).toList(),
)
Widget 重建優化
// ✅ 好的做法 - 將不變的部分提取為 child
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
const child = Text('Static content'); // 不會重建
return AnimatedBuilder(
animation: animation,
child: child,
builder: (context, child) {
return Transform.rotate(
angle: animation.value,
child: child, // 重用靜態內容
);
},
);
}
}
🌐 國際化規範
文字本地化
1. 所有文字使用 easy_localization
// ✅ 好的做法
Text('welcome.title'.tr())
Text('friend.count'.plural(friendCount))
Text('user.greeting'.tr(namedArgs: {'name': userName}))
// ❌ 避免
Text('Welcome to Valo') // 硬編碼文字
2. 翻譯鍵命名規範
// 使用階層式命名
{
"welcome": {
"title": "歡迎使用 Valo",
"subtitle": "開始您的聊天之旅"
},
"friend": {
"request_sent": "好友邀請已發送",
"request_failed": "發送失敗,請重試",
"count": "{count, plural, =0{沒有好友} =1{1位好友} other{{count}位好友}}"
}
}
3. 多語言文件管理
- en.json: 基礎語言,所有鍵值必須存在
- zh.json: 繁體中文
- vi.json: 越南文
🛡️ 錯誤處理
統一錯誤處理
1. 使用 ToastUtil 顯示訊息
// ✅ 好的做法
try {
await apiCall();
ToastUtil.showSuccess('operation.success'.tr());
} catch (e) {
ToastUtil.showError('operation.failed'.tr());
logger.e('API call failed', e);
}
// ❌ 避免
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error occurred')),
);
2. 異常分類處理
try {
await chatService.sendMessage(message);
} on NetworkException catch (e) {
ToastUtil.showError('network.error'.tr());
} on AuthException catch (e) {
// 重新登入
context.read<AuthService>().logout();
} catch (e) {
ToastUtil.showError('general.error'.tr());
logger.e('Unexpected error', e);
}
空值安全
// ✅ 好的做法
class UserProfile {
UserProfile({required this.userId, this.nickname});
final String userId;
final String? nickname;
String get displayName => nickname ?? userId;
}
// 使用時
if (user.nickname?.isNotEmpty == true) {
// 安全檢查
}
🧪 測試規範
單元測試
1. 測試文件組織
// test/unit/services/chat_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../../../lib/services/chat_service.dart';
void main() {
group('ChatService', () {
late ChatService chatService;
setUp(() {
chatService = ChatService();
});
test('should send message successfully', () async {
// Arrange
const message = 'Hello World';
// Act
final result = await chatService.sendMessage(message);
// Assert
expect(result.success, isTrue);
});
});
}
2. Widget 測試
// test/widget/friend_request_button_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../lib/widgets/friend/friend_request_button.dart';
void main() {
testWidgets('FriendRequestButton shows loading state', (tester) async {
bool wasPressed = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: FriendRequestButton(
userId: 'test_user',
onSuccess: () => wasPressed = true,
),
),
),
);
await tester.tap(find.byType(FriendRequestButton));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
}
📝 文檔和註解
代碼註解
1. 類別和方法註解
/// 好友請求按鈕組件
///
/// 封裝了發送好友請求的完整業務邏輯,包括載入狀態管理和錯誤處理。
/// 通過回調參數向外部通報操作結果。
class FriendRequestButton extends StatefulWidget {
/// 創建好友請求按鈕
///
/// [userId] 目標用戶ID
/// [onSuccess] 請求成功回調
/// [onError] 請求失敗回調,參數為錯誤訊息
/// [onLoading] 載入狀態變化回調
const FriendRequestButton({
Key? key,
required this.userId,
required this.onSuccess,
this.onError,
this.onLoading,
}) : super(key: key);
}
2. 複雜邏輯註解
Future<void> _syncConversations() async {
// 先從本地緩存載入對話列表,提供快速顯示
final cachedConversations = await _loadCachedConversations();
if (cachedConversations.isNotEmpty) {
_updateConversationList(cachedConversations);
}
try {
// 從服務器獲取最新對話列表
final serverConversations = await chatService.getConversations();
// 合併本地和服務器數據,以服務器數據為準
final mergedConversations = _mergeConversations(
cachedConversations,
serverConversations,
);
_updateConversationList(mergedConversations);
await _cacheConversations(mergedConversations);
} catch (e) {
// 網路錯誤時保持使用緩存數據
logger.w('Failed to sync conversations, using cached data', e);
}
}
🔒 安全性規範
敏感資料處理
1. Token 和密鑰
// ✅ 好的做法
class AuthService {
static const _tokenKey = 'auth_token';
Future<void> saveToken(String token) async {
// 使用安全存儲
await SecureStorageUtil.write(_tokenKey, token);
}
Future<String?> getToken() async {
return await SecureStorageUtil.read(_tokenKey);
}
}
// ❌ 避免
SharedPreferences.getInstance().then((prefs) {
prefs.setString('token', token); // 不安全的存儲
});
2. 日誌安全
// ✅ 好的做法
logger.d('Sending request to ${api.host}'); // 不記錄敏感參數
// ❌ 避免
logger.d('Sending request with token: $token'); // 洩露敏感資訊
📋 代碼審查檢查清單
審查者檢查項目
代碼品質
- [ ] 遵循命名規範
- [ ] 適當的註解和文檔
- [ ] 沒有硬編碼值
- [ ] 錯誤處理完善
- [ ] 性能考量合理
功能性
- [ ] 符合需求規格
- [ ] 邊緣情況處理
- [ ] 用戶體驗良好
- [ ] 多語言支持
安全性
- [ ] 沒有敏感資訊洩露
- [ ] 輸入驗證充分
- [ ] 權限檢查正確
測試
- [ ] 測試覆蓋適當
- [ ] 測試案例全面
- [ ] 測試通過
最後更新: 2025-08-07