專案資料夾結構說明
lib/
├── 1. core/ # 核心邏輯 - 應用啟動與生命週期管理
├── 2. config/ # 全域設定 - 色彩、主題、事件匯流排
├── 3. localization/ # 多語言 - 國際化翻譯資源
├── 4. presentation/ # 頁面層 - 路由、佈局、畫面組件
│ ├── layout/ # 佈局組件 - 應用程式整體佈局結構
│ ├── router/ # 路由系統 - Auto Route 路由管理與守衛
│ └── screens/ # 畫面組件 - 各功能頁面的具體實現
├── 5. services/ # 服務層 - 外部系統交互核心
├── 6. modal/ # 純資料模型 - API響應結構定義
├── 7. modalView/ # UI資料模型 - 畫面狀態與顯示邏輯
├── 8. adapters/ # 資料轉換層 - SDK與UI格式橋接
├── 9. utils/ # 工具函數 - 通用輔助功能
├── 10. components/ # 基礎UI組件 - 純視覺元件,無業務邏輯
└── 11. widgets/ # 業務組件 - 高內聚功能組件 (專案特色)
1. core/ - 核心應用
包裝用途?
"讓複雜度向下沈淪,高維護性事務向上揭露(開放)"
- 在開發過程中,有一段時間在整理 main.dart 程式碼當時非常凌亂,其實改善做法只是把 100 行的程式,依照職責盡量分類抽象,包裝複雜度,向上(開發者)提供高維護情況與事件,才造就目前的 main.dart。
CoreApplication: 應用程式啟動流程封裝,包含所有 init 動作新套件引入
如果引入新套件,且需要 init 相關動作,到
CoreApplication::initLibrary這邊添加、調整。CoreApplicationHooks: 應用程式生命週期事件處理 (背景/前台切換等)Application Hooks
裝置生命週期集中區,可自定義邏輯:
whenResumed- 應用程式從背景回到前台(重新連接聊天、更新資料)whenHidden- 應用程式進入背景時(登出聊天連接)whenInactive- 應用程式變為非活躍狀態whenPaused- 應用程式暫停時whenDetached- 應用程式完全關閉時whenDeepLink- 處理深度連結(忘記密碼、群組分享等) :::
2. config/ - Valo 的共用設定
env.dart: Valo 的專案變數,依照不同的環境,有不同的設定.env.dev、.ent.test,env.prodAGORA_APP_ID: AGORA APP KEY,有兩個不同環境(正式、測試)。API_URL: 前台 API 網址。TERMS_URL: 隱私權政策網址。GROUP_SHARE_URL: 加入群組打開 APP 用的網址。
style.dart:ValoColor: Valo 的色彩集中區,對照 Figma 的 color。Color 提醒
盡量直接使用
primary、neutral...這類設計師定義的色名。(之前定義很多 component 的 color,發現後來很常忘記名稱、調整修改上要想很久。)
ValoIcons: Valo 的 ICON 集中區ICON 提醒
盡量直接使用 SvgPicture,從 Figma 下載 SVG 引入到專案用 SvgPictue
(過往尚未引入 SvgPicture 套件時,都必須將 Svg 轉成 font icon,過程很麻煩還會有 Icon 壓壞的情況,引入 SvgPicture
未來新 Icon 盡量直接使用 Svg 就好,之前的 IconData 就封存吧!)ValoSize、ValoShadow、ValoWidget...就不細說,都是共用的外觀設定集中。
valo.dat: 共用抽象設定,目前主要topic_layout.dart的抽象設定。ValoTopic: 大廳的底部導覽,有三頁。- 依照
ValoTopicEntity抽象設定出新對象,下方導覽會自動多一區。
- 依照
ValoHeaderActions: 目前大廳 header 右上,會有兩個 '加入好友'、'加入群組'。- 能添加出新的
ValoHeaderActions在ValoTopicEntity::getHeaders添加新的ValoHeaderActions。
- 能添加出新的
3. localization/ - 多語言
使用 easy_localization 套件,支援語言中文(zh.json)、英文(en.json)、越南文(vi.json)
在 Dart 中使用:
// 基本用法 Text('common.error.login_fail'.tr()) // 帶參數的翻譯 Text('common.error.login_fail'.tr(namedArgs: {'name': '使用者名稱'})) // context.tr (如果使用端有立馬切換語系,畫面會立即響應) Text(context.tr('common.error.login_fail', namedArgs: {'name': '使用者名稱'}))提醒
通常 bottom sheet、popup,這種一次性的功能/頁面,使用簡單的 ''.tr 即可。
- 因為使用情境沒機會讓它切換語系。
但如果該頁是,比較共用的 screen 區,盡量使用 context.tr,因為有可能被切換語系,
Example:
settings_screen.dart有語系的 item,當頁的文字需要 context.tr 立即響應切換。Example:
login_screen.dart左上有語系切換,當頁的文字需要 context.tr 立即響應切換。 :::
4. presentation/ - 核心展示
處理應用程式的頁面展示邏輯,是用戶直接互動的界面層。
4-1. router/ - Valo 頁面路由,基於 Auto Route 路由管理套件。
app_router.dart:- 所有頁面路由,每個 route 都會對應到
/screen/*。 app_router.gr.dart: Auto Route 自動生成的檔案。如何快速添加新的頁面?
- 定義好新的
new_xxx_screen.dart, 頭掛上@RoutePage - 在
app_router.dart的底部import '...new_xxx_screen.dart'; - Terminal 執行
flutter packages pub run build_runner build --delete-conflicting-outputs - 完成
還有很多功能, ex: 子路由、導覽式路由...等,到 Auto Route 了解。
- 定義好新的
- 所有頁面路由,每個 route 都會對應到
路由守衛系統:
conversation_guard.dart: 對話頁面-進入前守衛。- 在進入對話前,有些前置作業需要先做?不然容易在進入後,發生其他錯誤。
- 對話進入前,先確認 Agora SDK 是否有該則,沒有先創建對話資料。
- 對話進入前,都標示為已讀。
- 群組對話進入前,先啟動
取得群組成員的匿名資料。
- 在進入對話前,有些前置作業需要先做?不然容易在進入後,發生其他錯誤。
topic_guard.dart: 大廳頁面(topic_screen.dart)-進入前守衛。- 該守衛做的事情,進入大廳前
Valo共用資料湖全部重新更新。- 開始監聽
Agora新消息,間接更新Valo共用資料湖。
- 通常會觸發該守衛的時機為以下:
- 登入成功後,準備進入
topic_contacts.screen。 - APP 重新開啟,自動登入成功,準備進入
topic_contacts.screen。
- 登入成功後,準備進入
- 該守衛做的事情,進入大廳前
4-2. layout/ - 佈局樣式 共用型組件。
app_layout.dart: 提供給進入大廳前topic_screen.dart的佈局外觀。- Example: 登入、忘記密碼、註冊,大多都有一個共同點是,右上要掛著
語言切換。
- Example: 登入、忘記密碼、註冊,大多都有一個共同點是,右上要掛著
topic_layout.dart: 提供進入大廳後topic_screen.dart的佈局外觀。- Example: 三大主題,底部
導覽 Navbar,header右邊 title + 左邊-搜尋好友&創建群組。
- Example: 三大主題,底部
setting_layout.dart: 提供給設定底下的子功能路由的佈局外觀。- Example: 他們都有個共通點, header 右方-back,底藍色,不要 Navbar 導覽。
4-3. screens/ - 各項功能畫面
welcome_screen.dart: 歡迎引導頁面。- 全新 APP 會進來此。
- 曾登入過,但過非常長時間(30 日)未使用 APP,重新打開會被導回此。
- 相關業務流程: 詳見 登入前業務單元 - 歡迎業務單元
login_screen.dart: 登入主頁面- 相關業務流程: 詳見 登入前業務單元 - 登入業務單元
topic/: 大廳 使用嵌套路由 (Nested Routes)contacts/: 聯絡人 (兩種分類,對應兩種friends,groups)chats/: 聊天 (三種分類,對應三種all,friends,groups)settings/: 設定
conversation/: 從topic/chats聊天列表,點入某一個聊天後的對話聊天模式- 核心對話功能:
conversation_screen.dart: 核心對話畫面 (聊天訊息列表與輸入區)
- 好友對話相關:
conversation_friend_info_screen.dart: 好友對話資訊頁 (好友資料、設定)
- 群組對話相關:
conversation_group_info_screen.dart: 群組對話資訊頁 (群組資料、成員列表)conversation_group_settings_screen.dart: 群組對話設定頁 (個人匿名編輯、管理員的管理功能)
- 共用功能:
conversation_info_files_screen.dart: 對話檔案列表 (對話歷史檔案清單)conversation_info_media_screen.dart: 對話媒體列表 (對話歷史圖片、影片清單)conversation_info_links_screen.dart: 對話連結列表 (查看歷史連結)
- 核心對話功能:
common/: 共用頁面。 (注意:這裡的元件,並沒有使用@RoutePage)- Example: 對話中有圖片、檔案、影片訊息,點擊後需要
全螢幕模式的功能畫面。
- Example: 對話中有圖片、檔案、影片訊息,點擊後需要
forgot/: 忘記密碼流程頁面forgot_password_email_screen.dart: 忘記密碼 - 輸入電子郵件頁面forgot_passowrd_email_verify_screen.dart: 忘記密碼 - 驗證碼驗證頁- 相關業務流程: 詳見 登入前業務單元 - 忘記密碼業務單元
register/: 註冊相關頁面群組register_screen.dart: 註冊 - 註冊佈局樣式-第一步填寫表單,第二部驗證碼驗證。register_form_screen.dart: 註冊 - 表單填寫頁面register_verify_screen.dart: 註冊 - 電子郵件驗證頁- 相關業務流程: 詳見 登入前業務單元 - 註冊業務單元
5. services/ - 功能服務層
ChatService: Agora Chat SDK 使用的方法,這邊多墊一層,因為有些繁瑣事情,會將複雜度再包裝進來。為什麼多包裝這些靜態方法?
Agora SDK 原生呼叫的方法
fetchContacsfetchGroupsfetchtConverstaiongetMessages...等,有一個特性是,只讓開發者 20 筆分頁式的取得方法,但某些情況,我希望直接取得所有的 Groups 回來,因為無時無刻都有機會會有新的 Converstaion 過來,我如果沒有將所有的 Groups 先 ready,曾經就一直有空白的 Bug 出現。- ex:
ChatService::fetchAllMessagesAgoraFetchService有意識的刻意包裝,給業務層方便。
所以這邊我都會墊一層 while loop search 包裝成一個方法,讓其他畫面元件使用。
瑕疵
不過有些 method 確實好像不用包裝,只用短短的一行 (= . =|||) 早期開發上的過度設計(ex:
ChatService::getConversationsByFriends這類的情況)- ex:
ApiService: REST API 客戶端,所有前台 API 調用- API 其實集中在
api_client.dart定義中,ApiService其實只是一個使用介面(getInstance)如何添加新的前台 API?
參考過往
api_client.dart的定義,API 類型、Response、Request更新
api_client.g.dart,執行flutter packages pub run build_runner build --delete-conflicting-output
如何使用?
- async function
try { // refreshTokenResult 已經是查完的 Result ApiServicefinal refreshTokenResult = await ApiService.getInstance.refreshToken(); } on DioException catch(ex) { // DioException 錯誤處理 } catch(ex) { // 未知的錯誤處理 }- Promise
ApiService.getInstance.refreshToken() .then(response { // 成功後做什麼 }) .catch((ex) { // DioException 錯誤處理 }, ex => ex is DioException) .catch((ex) { // 未知的錯誤處理 })細節
ApiService::_initApiClient我偷將後端需要的 JWT Auth Token,這邊偷定義進去 interceptor(攔截者),未來如果有什麼需要共同定義的可以從這邊下手。
- API 其實集中在
AuthService: Valo 的雙重認證業務 (Agora + API JWT),詳情 認證架構文檔提示
主要處理
登入、自動記憶登入、refresh token、即將到期...等,這些是在表面上的意義,但實際每件事情,可能會牽扯 2~3 件服務ex:登入頁的 LoginButton,點擊下去時,會先跟
- login api 請求
- agora client login
- 記憶帳戶方便下次自動登入
- 取得個人資料
而有這些流程事務複雜度,而不希望暴露在畫面、元件層,會造成閱讀負擔。
MediaService: Valo 設備媒體的操作服務- 打開
文件區、媒體區、相機模式、驗證檔案類型、大小等的服務方法。
注意
這邊大概是本專案最凌亂、最難搞的一部分。
- 打開
文件區、下載檔案到文件區
- IOS 有統一的規範比較少問題。
- Android,新舊版的沒有一個統一的共用文件路徑,
- 至今目前我還是不太清楚新舊版,在幾版以上&以下,該用哪個文件目錄。
- 嘗試部分套件 file_selector、file_picker...等,都無法共同解決方案,
未來這段需要花一些時間,深入嘗試多方媒體套件。
- 打開
AppBadgeManager: APP 數字徽章管理- IOS 沒問題
- Android 大部分平台沒有支援數字徽章,除了很少部分的機型。
6. modal/ - 資料模型
user.dart:- 該設備登入的使用者、角色類別(Guest、Member、Admin),目前大部分只用到
Member。
- 該設備登入的使用者、角色類別(Guest、Member、Admin),目前大部分只用到
user_profile.dart:user.dart的延伸Profile資料,ex: Member 需要 agora fetchProfile
contact.dart:- 跟 contacts 這邊有關的資料模型,ex: 好友(Friend)、群組(Group)、搜尋好友(SearchMember)、
chat.dart: 聊天相關資料模型ValoConversationType- 聊天對話類別 (Friend, Group)ValoConversationMessage- 一則 Valo 聊天訊息,多了一些方法封裝
CreateGroupFormData- 創建群組的資料
備註
chat.dart ValoConversationMessage 後來因為在整合 valo_chat.dart 時,已經有新的方式實作,ValoConversationMessage的使用上,變得有點雞肋,而目前是在 adapters/ 這邊實作轉換,當時實驗新版的 valo_chat.dart,為了要銜接 新舊版本的 chat 資料模型,而才使用 adapter 去做中間轉換。
contact.dart 跟 chat.dart 其實有在想是不是整合一起就好? CreateGroupFormData 放在 chat.dart 內好像也怪怪的
7. modalView/ - Valo UI 資料湖
其實就是 Provider 的資料模型、資料業務,Valo資料湖 共用所有資料狀態。
- 在開發過程中,遇到非常多 資料不同步 Bug、功能場景的資料依賴,這種情況而設計,例如:
- 資料重複創建問題: 如果不使用 Provider 共用
- 開發者容易遺忘
最源頭的資料源是否為同一個創立,造成以下 - 各 Widget 組件會各自創建獨立的資料物件,造成記憶體浪費且資料不一致
- 開發者容易遺忘
- 狀態同步困難: A 組件修改了好友資料,B 組件無法即時得知變更,導致顯示過時資料
- 資料重複創建問題: 如果不使用 Provider 共用
因此統一的資料湖,讓所有組件共享同一份資料,主確保資料一致性。
注意
modalView 是我最早期開發的目錄名稱,目前意義上可能已經不太合適,當時想使用 mvvc 的概念,不過經過後面的開發改善,目前以 Provider 共用資料的方式,開發狀況才比較穩定下來。
8. adapters/ - 資料轉換層
ChatUIAdapter: 將 Agora ChatMessage 轉換為 flutter_chat_ui 所需的 Message 格式- 用途: 解決不同資料格式間的相容性問題,讓 UI 層無需了解 Agora SDK 的內部結構
9. utils/ - 工具函數
提供與業務邏輯無關的通用工具函數,特點是 職責單一、低異動,大部分都有包裝 單元測試,如果有異動方法,建議要跑過單元測試。
核心工具檔案
time_util.dart: 處理timestamp、DateTime、格式化時間文字的工具。toast_util.dart: 顯示通知的工具。bottom_sheet_util.dart: 顯示由下滑上的內容的工具。popup_util.dart: 顯示彈窗工具。notice_util.dart: 舊的彈窗的工具 (遺棄中,減少此工具的使用,toast_util替代它)
資料處理工具
text_util.dart: 文字處理和格式化工具clipboard_util.dart: 剪貼簿操作工具secure_storage_util.dart: 安全儲存工具- ex: 登入成功時,保存用戶資訊與 Token,需要使用 secure_storage
擴展功能
agora_extension.dart: Agora SDK 相關擴展方法context_extension.dart: Flutter Context 擴展方法throw_util.dart: 錯誤處理和異常工具
備註
throw_util 、 context_extension 目前比較少使用
throw_util: 是一個包裝錯誤處理的工具,專案中時常寫 try catch,但在 catch 中很容易無處理,也沒辦法集中管理錯誤處理,為了這件事情而生,但忙著其他需求後來沒什麼使用。
10. components/ - 通用元件
提供無業務邏輯的純 UI 組件,專注視覺展示,可在整個專案中重複使用。
buttons/ - 按鈕組件
action_button.dart:一般任務按鈕組件(無載入狀態)worker_button.dart:異步任務按鈕 (有載入狀態)countdown_worker_button.dart:倒數計時按鈕 (驗證碼場景)
inputs/ - 輸入框組件
valo_text_field.dart: Valo 文字輸入框valo_password_field.dart: 密碼輸入框 (驗證規則已內涵在裡頭)valo_phone_field.dart: 電話號碼輸入框email_text_field.dart: 電子郵件輸入框
popup/ & sheet/ - 彈窗組件
base_popup.dart: 基礎彈窗組件base_bottom_sheet.dart: 基礎底部彈出組件BaseBottomSheet: 搭配BottomSheetUtil::show,BaseBottomSheet 是提供一個底表原型,開放 header, content 參數。- 內建常用的 Header 樣式:
BaseBottomSheetHeader: 自己客製右中左的 WidgetOnlyCloseBottomSheetHeader: 右邊無東西、中間標題、左邊關閉DoneCloseBottomSheetHeader: 右邊無東西、中間標題、左邊 Done
11. widgets/ - 業務&功能組件
依照 業務類別 切分的目錄群。
add_friend/ - 好友添加功能
add_friend_button.dart: 添加好友按鈕組件open_search_member_bottom_sheet_button.dart: 打開搜尋成員底部表單按鈕search_member_bottom_sheet.dart: 搜尋成員底部表單
auth/ - 認證相關組件
auth_register_prompt.dart: 註冊提示組件create_account_prompt.dart: 創建帳號提示login_bottom_section.dart: 登入頁底部區塊- 相關業務流程: 詳見 登入前業務單元
chat/ - 聊天功能核心
基於 flutter_chat_ui 的完整聊天系統,支援多種訊息類型、回覆機制、搜尋功能等。
詳細架構請參閱 聊天系統架構文檔。
message/- 訊息組件群: 各種訊息類型與功能組件reply/- 回覆子系統: 完整的回覆訊息機制
search/- 聊天搜尋: 編輯器與標題列搜尋功能- 聊天核心組件: 主要聊天組件、編輯器、狀態管理等
common/ - 通用業務組件
base_loading_view.dart: 基礎載入視圖settings_list_tile.dart: 設定列表項目upload_image_bottom_sheet.dart: 圖片上傳底部表單
contacts/ - 聯絡人功能
contacts_category.dart: 聯絡人分類標籤contacts_friend_item.dart: 好友項目顯示contacts_header.dart: 聯絡人頁面標題
conversation/ - 對話管理
appbar/- 對話頁標題列:conversation_appbar.dart: 對話標題列conversation2_appbar.dart: 對話標題列變體
conversation_board_item.dart: 對話列表項目conversation_pin_slidable_button.dart: 對話置頂滑動按鈕
create_group/ - 群組創建流程
buttons/- 創建按鈕群:create_group_button.dart: 創建群組按鈕create_group_bottom_sheet_button.dart: 底部表單觸發按鈕
sheets/- 創建流程表單:create_group_step_1.dart: 步驟 1 - 選擇成員create_group_step_2.dart: 步驟 2 - 群組資訊create_group_step_3.dart: 步驟 3 - 完成創建
friend/ - 好友功能
buttons/- 好友操作按鈕:friend_chat_button.dart: 開始聊天按鈕friend_block_button.dart: 封鎖好友按鈕friend_mute_button.dart: 靜音好友按鈕
friend_avatar.dart: 好友頭像組件friend_display_name.dart: 好友顯示名稱friend_profile_card.dart: 好友資料卡片sheets/friend_profile_bottom_sheet.dart: 好友資料底部表單
group/ - 群組功能
完整的匿名群聊系統,支援角色管理、匿名身份、即時通訊等功能。
詳細架構請參閱 群組系統架構文檔。
avatars/- 群組頭像系統: 群組與成員頭像顯示buttons/- 群組操作按鈕: 聊天、加入、管理等功能按鈕settings/- 群組設定項目: 管理員、編輯、刪除等設定sections/- 群組區塊: 匿名資料設定區塊sheets/- 群組底部表單: 資料查看、邀請等表單popups/- 群組彈窗: 分享、離開確認彈窗- 群組核心組件: 顯示名稱、資料卡片、邀請功能等
language/ - 語言切換
language_selector.dart: 語言選擇器language_sheet.dart: 語言選擇底部表單
login/ - 登入功能
login_form.dart: 登入表單組件- 相關業務流程: 詳見 登入前業務單元 - 登入業務單元
register/ - 註冊功能
register_button.dart: 註冊按鈕register_confirm_button.dart: 註冊確認按鈕register_verify_code_input.dart: 驗證碼輸入框- 相關業務流程: 詳見 登入前業務單元 - 註冊業務單元
topic/ - 主題頁面組件
valo_topic_appbar.dart: 主題頁標題列valo_topic_navigation.dart: 主題頁導航valo_topic_tabbar.dart: 主題頁標籤欄
user/ - 使用者功能
profile/- 使用者資料:user_profile_nickname_item.dart: 暱稱編輯項目user_profile_birthday_item.dart: 生日編輯項目
user_avatar.dart: 使用者頭像user_profile.dart: 使用者資料組件delete_user_button.dart: 刪除帳號按鈕
welcome/ - 歡迎頁組件
welcome_background.dart: 歡迎頁背景welcome_login_section.dart: 歡迎頁登入區塊- 相關業務流程: 詳見 登入前業務單元 - 歡迎業務單元
根目錄組件
change_password_bottom_sheet.dart: 修改密碼底部表單delete_account_bottom_sheet.dart: 刪除帳號底部表單logout_popup.dart: 登出確認彈窗valo_empty_view.dart: 空狀態顯示組件valo_empty_navigation.dart: 空導航組件