Valo Documentation
首頁
架構
認證
開發設置
代碼規範
開發流程
故障排除
GitHub
首頁
架構
認證
開發設置
代碼規範
開發流程
故障排除
GitHub
  • 快速開始

    • /
    • 🚀 開發環境設置指南
    • 最初的那些痛
  • 核心概念

    • 🏗️ 專案概述
    • 認證架構文檔
    • 📐 代碼規範
  • 開發指南

    • ⚡ 開發工作流程
    • 🔧 常見問題排解
  • 專案故事

    • 專案故事

📐 代碼規範

本文檔定義了 Valo 專案的編碼標準、風格指南和最佳實踐,確保代碼品質和團隊協作效率。

🎯 總體原則

核心理念

  1. 可讀性優先: 代碼應該易於理解和維護
  2. 一致性: 遵循統一的風格和模式
  3. 簡潔性: 避免不必要的複雜度
  4. 效能意識: 考慮性能影響
  5. 安全性: 遵循安全最佳實踐

專案特色

  • 高內聚組件: 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;
  
  @override
  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 {
  @override
  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 {
  @override
  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;

  @override
  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);
      }
    }
  }

  @override
  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 {
  @override
  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

最後更新: 2025/8/7 中午12:19
貢獻者: boheng
Prev
認證架構文檔