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

上篇文章講的是狀態(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)不治本而已。

image.png

請(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ì)想到如 MVCMVVM 等,它們主要是圍繞著控制器層(Controller)、視圖層(View)、和數(shù)據(jù)層(Model),還有連接 ViewModel 之間的模型視圖層(ViewModel)這些來講的。

MVVM

然而,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)。

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

Flutter App 想要實(shí)現(xiàn)分層設(shè)計(jì),就不得不提到包管理工具,如果在將所有分層組件代碼放在主工程里面,那樣并不能達(dá)到每個(gè)組件單獨(dú)開發(fā)、維護(hù)和測試的目的,而如果放在新建的 Dart Package 中,沒發(fā)跨多個(gè)組件改代碼和測試,無法實(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_requestdatabase、shared_preference等,該組件包所有的調(diào)用實(shí)現(xiàn)都在 domain 中接口 repository 的實(shí)現(xiàn)類 repository_impl 中。
  • shared:工具類組件包,包括:util、helper、enum、constantsexception、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ā)測試,盡可能的做到依賴于抽象而不是具體的實(shí)現(xiàn)。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容