上篇文章講的是狀態(tài)管理,提到了 Flutter BLoC ,相比與原生的 setState() 及Provider等有哪些優(yōu)缺點(diǎn),并結(jié)合實(shí)際項(xiàng)目寫了一個(gè)簡單的使用,接下來本篇文章來講 Flutter 大型項(xiàng)目是如何進(jìn)行分層設(shè)計(jì)的,費(fèi)話不多說,直接進(jìn)入正題哈。
為啥需要分層設(shè)計(jì)
其實(shí)這個(gè)沒有啥固定答案,也許只是因?yàn)槟骋惶炜吹绞掷锏拇a如同屎山一樣,如下圖,而隨著業(yè)務(wù)功能的增加,不停的往這上面堆,這個(gè)屎山也會(huì)愈發(fā)龐大和混亂,如果這樣繼續(xù)下去,直到某一天因?yàn)橐粋€(gè)小小的Bug,你需要花半天的時(shí)間來排查問題出在哪里,最后當(dāng)你覺得問題終于改好了的時(shí)候,卻不料碰了不該碰的地方,結(jié)果就是 fixing 1 bug will create 10 new bugs,甚至程序的崩潰。

隨著這種問題的凸顯,于是團(tuán)隊(duì)里的顯眼包A提出了要求團(tuán)隊(duì)里的每個(gè)人都必須負(fù)責(zé)完成給自己寫的代碼添加注釋和文檔,規(guī)范命名等措施,一段時(shí)間后,發(fā)現(xiàn)代碼是規(guī)范了,但問題依然存在,這時(shí)候才發(fā)現(xiàn)如果工程的架構(gòu)分層沒有做好,再規(guī)范的代碼和注釋也只是在屎山上雕花,治標(biāo)不治本而已。

請(qǐng)?jiān)徫掖蛄艘粋€(gè)這么俗的比方,但話糙理不糙,那么啥是應(yīng)用的分層設(shè)計(jì)呢?
簡單的來說,應(yīng)用的分層設(shè)計(jì)是一種將應(yīng)用程序劃分為不同層級(jí)的方法,每個(gè)層級(jí)負(fù)責(zé)特定的功能或責(zé)任。其中表示層(Presentation Layer)負(fù)責(zé)用戶界面和用戶交互,將數(shù)據(jù)呈現(xiàn)給用戶并接收用戶輸入;業(yè)務(wù)邏輯層(Business Logic Layer)處理應(yīng)用程序的業(yè)務(wù)邏輯,包括數(shù)據(jù)驗(yàn)證、處理和轉(zhuǎn)換;數(shù)據(jù)訪問層(Data Access Layer)負(fù)責(zé)與數(shù)據(jù)存儲(chǔ)交互,包括數(shù)據(jù)庫或文件系統(tǒng)的讀取和寫入操作。

這樣做有什么好處呢?一句話總結(jié)就是為了讓代碼層級(jí)責(zé)任清晰,維護(hù)、擴(kuò)展和重用方便,每個(gè)模塊能獨(dú)立開發(fā)、測試和修改。
原生 App 開發(fā)的分層設(shè)計(jì)
說到 iOS、Android 的分層設(shè)計(jì),就會(huì)想到如 MVC、MVVM 等,它們主要是圍繞著控制器層(Controller)、視圖層(View)、和數(shù)據(jù)層(Model),還有連接 View 和 Model 之間的模型視圖層(ViewModel)這些來講的。

然而,MVC、MVVM 概念還不算完整的分層架構(gòu),它們只是關(guān)注的 App 分層設(shè)計(jì)當(dāng)中的應(yīng)用層(Applicaiton Layer)組織方式,對(duì)于一個(gè)簡單規(guī)模較小的App來說,可能單單一個(gè)應(yīng)用層就能搞定,不用擔(dān)心業(yè)務(wù)增量和復(fù)雜度上升對(duì)后期開發(fā)的壓力,而一旦 App 上了規(guī)模之后就有點(diǎn)應(yīng)付不過來了。
當(dāng) App 有了一定規(guī)模之后,必然會(huì)涉及到分層的設(shè)計(jì),還有模塊化、Hybrid 機(jī)制、數(shù)據(jù)庫、跨項(xiàng)目開發(fā)等等,拿 iOS 的原生分層設(shè)計(jì)落地實(shí)踐來說,通常會(huì)將工程拆分成多個(gè)Pod私有庫組件,拆分的標(biāo)準(zhǔn)視情況而定,每一個(gè)分層組件是獨(dú)立的開發(fā)和測試,再在主工程添加Pod 私有庫依賴來做分層設(shè)計(jì)開發(fā)。
此處應(yīng)該有 Pod 分層組件化設(shè)計(jì)的配圖,但是太懶了,就沒有一個(gè)個(gè)的去搭建新項(xiàng)目和 Pod 私有庫,不過 iOS 原生分層設(shè)計(jì)不是本篇文章的重點(diǎn),本篇主要談?wù)摰氖?Flutter App 的分層設(shè)計(jì)。
Flutter 的分層設(shè)計(jì)
分層架構(gòu)設(shè)計(jì)的理念其實(shí)是相通的,差別在于語言的特性和具體項(xiàng)目實(shí)施上,Flutter 項(xiàng)目也是如此。試想一下,當(dāng)各種邏輯混合在一次的時(shí)候,即便是選擇了像 Bloc 這樣的狀態(tài)管理框架來隔離視圖層和邏輯實(shí)現(xiàn)層,也很難輕松的增強(qiáng)代碼的拓展性,這時(shí)候選擇采用一個(gè)干凈的分層架構(gòu)就顯得尤為重要,怎樣做到這一點(diǎn)呢,就需要將代碼分成獨(dú)立的層,并依賴于抽象而不是具體的實(shí)現(xiàn)。

Flutter App 想要實(shí)現(xiàn)分層設(shè)計(jì),就不得不提到包管理工具,如果在將所有分層組件代碼放在主工程里面,那樣并不能達(dá)到每個(gè)組件單獨(dú)開發(fā)、維護(hù)和測試的目的,而如果放在新建的 Dart Package 中,沒發(fā)跨多個(gè)組件改代碼和測試,無法實(shí)現(xiàn)本地包鏈接和安裝。使用 melos 就能解決這個(gè)問題,類似于 iOS 包管理工具 Pod, 而 melos 是 Flutter 項(xiàng)目的包管理工具。
組件包管理工具
-
安裝
Melos,將Melos安裝為全局包,這樣整個(gè)系統(tǒng)環(huán)境都可以使用:dart pub global activate melos -
創(chuàng)建
workspace文件夾,我這里命名為flutter_architecture_design,添加melos的配置文件melos.yaml和pubspec.yaml,其目錄結(jié)構(gòu)大概是這樣的:flutter_architecture_design ├── melos.yaml ├── pubspec.yaml └── README.md -
新建組件,以開發(fā)工具
Android Studio為例,選擇File->New->New Flutter Project,根據(jù)需要?jiǎng)?chuàng)建組件包,需要注意的是組件包存放的位置要放在workspace目錄中。
新建組件 -
編輯
melos.yaml配置文件,將上一步新建的組件包名放在packages之下,添加scripts相關(guān)命令,其目的請(qǐng)看下一步:name: flutter_architecture_design packages: - widgets/** - shared/** - data/** - initializer/** - domain/** - resources/** - app/** command: bootstrap: usePubspecOverrides: true scripts: analyze: run: dart pub global run melos exec --flutter "flutter analyze --no-pub --suppress-analytics" description: Run analyze. pub_get: run: dart pub global run melos exec --flutter "flutter pub get" description: pub get build_all: run: dart pub global run melos exec --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build all modules. build_data: run: dart pub global run melos exec --fail-fast --scope="*data*" --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build data module. build_domain: run: dart pub global run melos exec --fail-fast --scope="*domain*" --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build domain module. build_app: run: dart pub global run melos exec --fail-fast --scope="*app*" --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build app module. build_shared: run: dart pub global run melos exec --fail-fast --scope="*shared*" --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build shared module. build_widgets: run: dart pub global run melos exec --fail-fast --scope="*widgets*" --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build shared module. -
打開命令行,切換到
workspace目錄,也就是flutter_architecture_design目錄,執(zhí)行命令。melos bootstrap出現(xiàn)
SUCCESS之后,現(xiàn)在的目錄結(jié)構(gòu)是這樣的:
目錄結(jié)構(gòu)
- 點(diǎn)擊
Android Studio的add configuration,將下圖中的Shell Scripts選中后點(diǎn)擊OK。

以上的 Scripts 添加完后就可以在這里看到了,操作起來也很方便,不需要去命令行那里執(zhí)行命令。

Flutter 分層設(shè)計(jì)實(shí)踐
接下來介紹一下上面創(chuàng)建的幾個(gè)組件庫。
-
app:項(xiàng)目的主工程,存放業(yè)務(wù)邏輯代碼、UI頁面和Bloc,還有styles、colors等等。 -
domain:實(shí)體類(entity)組件包,還有一些接口類,如repository、usercase等。 -
data:數(shù)據(jù)提供組件包,主要有:api_request,database、shared_preference等,該組件包所有的調(diào)用實(shí)現(xiàn)都在domain中接口repository的實(shí)現(xiàn)類repository_impl中。 -
shared:工具類組件包,包括:util、helper、enum、constants、exception、mixins等等。 -
resources:資源類組件包,有intl、公共的images等 -
initializer:模塊初始化組件包。 -
widgets:公共的UI組件包,如常用的:alert、button、toast、slider等等。
它們之間的調(diào)用關(guān)系如下圖:

其中 shared 和 resources 作為基礎(chǔ)組件包,本身不依賴任何組件,而是給其它組件包提供支持。
作為主工程 App 也不會(huì)直接依賴 data 組件包,其調(diào)用是通過 domain 組件包中 UseCase 來實(shí)現(xiàn),在 UseCase 會(huì)獲取數(shù)據(jù)、處理列表數(shù)據(jù)的分頁、參數(shù)校驗(yàn)、異常處理等等,獲取數(shù)據(jù)是通過調(diào)用抽象類 repository 中相關(guān)函數(shù),而不是直接調(diào)用具體實(shí)現(xiàn)類,此時(shí)App 的 pubspec.yaml 中配置是這樣的:
name: app
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
widgets:
path: ../widgets
shared:
path: ../shared
domain:
path: ../domain
resources:
path: ../resources
initializer:
path: ../initializer
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
generate: false
assets:
- assets/images/
提供的數(shù)據(jù)組件包 data 實(shí)現(xiàn)了抽象類 repository 中相關(guān)函數(shù),只負(fù)責(zé)調(diào)用 Api 接口獲取數(shù)據(jù),或者從數(shù)據(jù)庫獲取數(shù)據(jù)。當(dāng)上層調(diào)用的時(shí)候不需要關(guān)心數(shù)據(jù)是從哪里來的,全部交給 data 組件包負(fù)責(zé)。
initializer 作為模塊初始化組件包,僅有一個(gè) AppInitializer 類,其主要目的是將其它的模塊的初始化收集起來放在 AppInitializer 類中 init() 函數(shù)中,然后在主工程入口函數(shù):main() 調(diào)用這個(gè) init() 函數(shù),常見的初始化如:GetIt 初始化、數(shù)據(jù)庫 objectbox 初始化、SharedPreferences初始化,這些相關(guān)的初始會(huì)分布在各自的組件包中。
class AppInitializer {
AppInitializer();
Future<void> init() async {
await SharedConfig.getInstance().init();
await DataConfig.getInstance().init();
await DomainConfig.getInstance().init();
}
}
widgets 作為公共的 UI 組件庫,不處理業(yè)務(wù)邏輯,在多項(xiàng)目開發(fā)時(shí)經(jīng)常會(huì)使用到。上圖中的 Other Plugin Module 指的的是其它組件包,特別是需要單獨(dú)開發(fā)與原生交互的插件時(shí)會(huì)用到,
這種分層設(shè)計(jì)出來的架構(gòu)或許在開發(fā)過程中帶來一下不便,如調(diào)用一個(gè)接口,第一步:需要先在抽象類 repository 寫好函數(shù)聲明;第二步:然后再去Api Service 寫具體請(qǐng)求代碼,并在repository_impl 實(shí)現(xiàn)類中調(diào)用;第三步:還需要在 UserCase 去做業(yè)務(wù)調(diào)用,錯(cuò)誤處理等;最后一步:在bloc的event中調(diào)用。這么一趟下來,確實(shí)有些繁瑣或者說是過度設(shè)計(jì)。但是如果維度設(shè)定在大的項(xiàng)目中多人合作開發(fā)的時(shí)候,卻能規(guī)避很多問題,每個(gè)分層組件都有自己的職責(zé)互不干擾,都支持單獨(dú)的開發(fā)測試,盡可能的做到依賴于抽象而不是具體的實(shí)現(xiàn)。
本篇文章就到這里,源碼會(huì)在后面這個(gè)系列的文章里放出來,感謝您的閱讀!

