Flutter大型項(xiàng)目架構(gòu):分層設(shè)計(jì)篇

上篇文章講的是狀態(tài)管理,提到了 Flutter BLoC 敌买,相比與原生的 setState()Provider等有哪些優(yōu)缺點(diǎn),并結(jié)合實(shí)際項(xiàng)目寫了一個(gè)簡(jiǎn)單的使用,接下來本篇文章來講 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)不治本而已狈网。

image.png

請(qǐng)?jiān)徫掖蛄艘粋€(gè)這么俗的比方宙搬,但話糙理不糙,那么啥是應(yīng)用的分層設(shè)計(jì)呢拓哺?

簡(jiǎn)單的來說勇垛,應(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ā)猎醇、測(cè)試和修改窥突。

原生 App 開發(fā)的分層設(shè)計(jì)

說到 iOSAndroid 的分層設(shè)計(jì)硫嘶,就會(huì)想到如 MVC阻问、MVVM 等,它們主要是圍繞著控制器層(Controller)沦疾、視圖層(View)称近、和數(shù)據(jù)層(Model)贡蓖,還有連接 ViewModel 之間的模型視圖層(ViewModel)這些來講的。

MVVM

然而煌茬,MVCMVVM 概念還不算完整的分層架構(gòu)彻桃,它們只是關(guān)注的 App 分層設(shè)計(jì)當(dāng)中的應(yīng)用層(Applicaiton Layer)組織方式坛善,對(duì)于一個(gè)簡(jiǎn)單規(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ā)和測(cè)試冯乘,再在主工程添加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)。

分層架構(gòu)設(shè)計(jì)

Flutter App 想要實(shí)現(xiàn)分層設(shè)計(jì)洼哎,就不得不提到包管理工具烫映,如果在將所有分層組件代碼放在主工程里面沼本,那樣并不能達(dá)到每個(gè)組件單獨(dú)開發(fā)、維護(hù)和測(cè)試的目的锭沟,而如果放在新建的 Dart Package 中抽兆,沒發(fā)跨多個(gè)組件改代碼和測(cè)試,無法實(shí)現(xiàn)本地包鏈接和安裝族淮。使用 melos 就能解決這個(gè)問題辫红,類似于 iOS 包管理工具 Pod, 而 melosFlutter 項(xiàng)目的包管理工具。

組件包管理工具

  1. 安裝 Melos祝辣,將 Melos 安裝為全局包贴妻,這樣整個(gè)系統(tǒng)環(huán)境都可以使用:

    dart pub global activate melos
    
  2. 創(chuàng)建 workspace 文件夾,我這里命名為 flutter_architecture_design蝙斜,添加 melos 的配置文件melos.yamlpubspec.yaml名惩,其目錄結(jié)構(gòu)大概是這樣的:

    flutter_architecture_design
    ├── melos.yaml
    ├── pubspec.yaml
    └── README.md
    
  3. 新建組件,以開發(fā)工具 Android Studio 為例孕荠,選擇 File -> New -> New Flutter Project娩鹉,根據(jù)需要?jiǎng)?chuàng)建組件包,需要注意的是組件包存放的位置要放在 workspace 目錄中稚伍。

    新建組件

  4. 編輯 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.
    
  5. 打開命令行熙涤,切換到 workspace 目錄,也就是 flutter_architecture_design 目錄困檩,執(zhí)行命令祠挫。

    melos bootstrap
    

    出現(xiàn) SUCCESS 之后,現(xiàn)在的目錄結(jié)構(gòu)是這樣的:

    目錄結(jié)構(gòu)

  1. 點(diǎn)擊Android Studioadd configuration悼沿,將下圖中的 Shell Scripts 選中后點(diǎn)擊 OK等舔。
Add Shell Scripts

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

Shell Scripts

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:工具類組件包敷矫,包括:utilhelper汉额、enumconstants榨汤、exception蠕搜、mixins等等。
  • resources:資源類組件包收壕,有intl妓灌、公共的images
  • initializer:模塊初始化組件包。
  • widgets:公共的 UI 組件包蜜宪,如常用的:alert虫埂、buttontoast圃验、slider 等等掉伏。

它們之間的調(diào)用關(guān)系如下圖:

Flutter App Architecture Design

其中 sharedresources 作為基礎(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í)Apppubspec.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ò)誤處理等饶辙;最后一步:在blocevent中調(diào)用蹲诀。這么一趟下來,確實(shí)有些繁瑣或者說是過度設(shè)計(jì)弃揽。但是如果維度設(shè)定在大的項(xiàng)目中多人合作開發(fā)的時(shí)候脯爪,卻能規(guī)避很多問題,每個(gè)分層組件都有自己的職責(zé)互不干擾矿微,都支持單獨(dú)的開發(fā)測(cè)試披粟,盡可能的做到依賴于抽象而不是具體的實(shí)現(xiàn)。

本篇文章就到這里冷冗,源碼會(huì)在后面這個(gè)系列的文章里放出來守屉,感謝您的閱讀!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒿辙,一起剝皮案震驚了整個(gè)濱河市拇泛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌思灌,老刑警劉巖俺叭,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異泰偿,居然都是意外死亡熄守,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裕照,“玉大人攒发,你說我怎么就攤上這事〗希” “怎么了惠猿?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)负间。 經(jīng)常有香客問我偶妖,道長(zhǎng),這世上最難降的妖魔是什么政溃? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任趾访,我火速辦了婚禮,結(jié)果婚禮上董虱,老公的妹妹穿的比我還像新娘扼鞋。我一直安慰自己,他們只是感情好空扎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著润讥,像睡著了一般转锈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上楚殿,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天撮慨,我揣著相機(jī)與錄音,去河邊找鬼脆粥。 笑死砌溺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的变隔。 我是一名探鬼主播规伐,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼匣缘!你這毒婦竟也來了猖闪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤肌厨,失蹤者是張志新(化名)和其女友劉穎培慌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柑爸,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吵护,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片馅而。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡祥诽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出用爪,到底是詐尸還是另有隱情原押,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布偎血,位于F島的核電站诸衔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏颇玷。R本人自食惡果不足惜笨农,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晒来,春花似錦逃片、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狞甚。三九已至锁摔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哼审,已是汗流浹背谐腰。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涩盾,地道東北人十气。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像春霍,于是被迫代替她去往敵國(guó)和親砸西。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容