Flutter大型项目架构:分层设计篇

上篇文章讲的是状态管理,提到了 Flutter BLoC ,相比与原生的 setState()Provider等有哪些优缺点,并结合实际项目写了一个简单的使用,接下来本篇文章来讲 Flutter 大型项目是如何进行分层设计的,费话不多说,直接进入正题哈。

为啥需要分层设计

其实这个没有啥固定答案,也许只是因为某一天看到手里的代码如同屎山一样,如下图,而随着业务功能的增加,不停的往这上面堆,这个屎山也会愈发庞大和混乱,如果这样继续下去,直到某一天因为一个小小的Bug,你需要花半天的时间来排查问题出在哪里,最后当你觉得问题终于改好了的时候,却不料碰了不该碰的地方,结果就是 fixing 1 bug will create 10 new bugs,甚至程序的崩溃。

随着这种问题的凸显,于是团队里的显眼包A提出了要求团队里的每个人都必须负责完成给自己写的代码添加注释和文档,规范命名等措施,一段时间后,发现代码是规范了,但问题依然存在,这时候才发现如果工程的架构分层没有做好,再规范的代码和注释也只是在屎山上雕花,治标不治本而已。

image.png

请原谅我打了一个这么俗的比方,但话糙理不糙,那么啥是应用的分层设计呢?

简单的来说,应用的分层设计是一种将应用程序划分为不同层级的方法,每个层级负责特定的功能或责任。其中表示层(Presentation Layer)负责用户界面和用户交互,将数据呈现给用户并接收用户输入;业务逻辑层(Business Logic Layer)处理应用程序的业务逻辑,包括数据验证、处理和转换;数据访问层(Data Access Layer)负责与数据存储交互,包括数据库或文件系统的读取和写入操作。

这样做有什么好处呢?一句话总结就是为了让代码层级责任清晰,维护、扩展和重用方便,每个模块能独立开发、测试和修改。

原生 App 开发的分层设计

说到 iOSAndroid 的分层设计,就会想到如 MVCMVVM 等,它们主要是围绕着控制器层(Controller)、视图层(View)、和数据层(Model),还有连接 ViewModel 之间的模型视图层(ViewModel)这些来讲的。

MVVM

然而,MVCMVVM 概念还不算完整的分层架构,它们只是关注的 App 分层设计当中的应用层(Applicaiton Layer)组织方式,对于一个简单规模较小的App来说,可能单单一个应用层就能搞定,不用担心业务增量和复杂度上升对后期开发的压力,而一旦 App 上了规模之后就有点应付不过来了。

App 有了一定规模之后,必然会涉及到分层的设计,还有模块化、Hybrid 机制、数据库、跨项目开发等等,拿 iOS 的原生分层设计落地实践来说,通常会将工程拆分成多个Pod私有库组件,拆分的标准视情况而定,每一个分层组件是独立的开发和测试,再在主工程添加Pod 私有库依赖来做分层设计开发。

此处应该有 Pod 分层组件化设计的配图,但是太懒了,就没有一个个的去搭建新项目和 Pod 私有库,不过 iOS 原生分层设计不是本篇文章的重点,本篇主要谈论的是 Flutter App 的分层设计。

Flutter 的分层设计

分层架构设计的理念其实是相通的,差别在于语言的特性和具体项目实施上,Flutter 项目也是如此。试想一下,当各种逻辑混合在一次的时候,即便是选择了像 Bloc 这样的状态管理框架来隔离视图层和逻辑实现层,也很难轻松的增强代码的拓展性,这时候选择采用一个干净的分层架构就显得尤为重要,怎样做到这一点呢,就需要将代码分成独立的层,并依赖于抽象而不是具体的实现。

分层架构设计

Flutter App 想要实现分层设计,就不得不提到包管理工具,如果在将所有分层组件代码放在主工程里面,那样并不能达到每个组件单独开发、维护和测试的目的,而如果放在新建的 Dart Package 中,没发跨多个组件改代码和测试,无法实现本地包链接和安装。使用 melos 就能解决这个问题,类似于 iOS 包管理工具 Pod, 而 melosFlutter 项目的包管理工具。

组件包管理工具

  1. 安装 Melos,将 Melos 安装为全局包,这样整个系统环境都可以使用:

    dart pub global activate melos
    
  2. 创建 workspace 文件夹,我这里命名为 flutter_architecture_design,添加 melos 的配置文件melos.yamlpubspec.yaml,其目录结构大概是这样的:

    flutter_architecture_design
    ├── melos.yaml
    ├── pubspec.yaml
    └── README.md
    
  3. 新建组件,以开发工具 Android Studio 为例,选择 File -> New -> New Flutter Project,根据需要创建组件包,需要注意的是组件包存放的位置要放在 workspace 目录中。

    新建组件

  4. 编辑 melos.yaml 配置文件,将上一步新建的组件包名放在 packages 之下,添加 scripts 相关命令,其目的请看下一步:

    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 目录,执行命令。

    melos bootstrap
    

    出现 SUCCESS 之后,现在的目录结构是这样的:

    目录结构

  1. 点击Android Studioadd configuration,将下图中的 Shell Scripts 选中后点击 OK
Add Shell Scripts

以上的 Scripts 添加完后就可以在这里看到了,操作起来也很方便,不需要去命令行那里执行命令。

Shell Scripts

Flutter 分层设计实践

接下来介绍一下上面创建的几个组件库。

  • app:项目的主工程,存放业务逻辑代码、 UI 页面和 Bloc,还有 stylescolors 等等。
  • domain:实体类(entity)组件包,还有一些接口类,如 repositoryusercase等。
  • data:数据提供组件包,主要有:api_requestdatabaseshared_preference等,该组件包所有的调用实现都在 domain 中接口 repository 的实现类 repository_impl 中。
  • shared:工具类组件包,包括:utilhelperenumconstantsexceptionmixins等等。
  • resources:资源类组件包,有intl、公共的images
  • initializer:模块初始化组件包。
  • widgets:公共的 UI 组件包,如常用的:alertbuttontoastslider 等等。

它们之间的调用关系如下图:

Flutter App Architecture Design

其中 sharedresources 作为基础组件包,本身不依赖任何组件,而是给其它组件包提供支持。

作为主工程 App 也不会直接依赖 data 组件包,其调用是通过 domain 组件包中 UseCase 来实现,在 UseCase 会获取数据、处理列表数据的分页、参数校验、异常处理等等,获取数据是通过调用抽象类 repository 中相关函数,而不是直接调用具体实现类,此时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/

提供的数据组件包 data 实现了抽象类 repository 中相关函数,只负责调用 Api 接口获取数据,或者从数据库获取数据。当上层调用的时候不需要关心数据是从哪里来的,全部交给 data 组件包负责。

initializer 作为模块初始化组件包,仅有一个 AppInitializer 类,其主要目的是将其它的模块的初始化收集起来放在 AppInitializer 类中 init() 函数中,然后在主工程入口函数:main() 调用这个 init() 函数,常见的初始化如:GetIt 初始化、数据库 objectbox 初始化、SharedPreferences初始化,这些相关的初始会分布在各自的组件包中。

class AppInitializer {
  AppInitializer();

  Future<void> init() async {
    await SharedConfig.getInstance().init();
    await DataConfig.getInstance().init();
    await DomainConfig.getInstance().init();
  }
}

widgets 作为公共的 UI 组件库,不处理业务逻辑,在多项目开发时经常会使用到。上图中的 Other Plugin Module 指的的是其它组件包,特别是需要单独开发与原生交互的插件时会用到,

这种分层设计出来的架构或许在开发过程中带来一下不便,如调用一个接口,第一步:需要先在抽象类 repository 写好函数声明;第二步:然后再去Api Service 写具体请求代码,并在repository_impl 实现类中调用;第三步:还需要在 UserCase 去做业务调用,错误处理等;最后一步:在blocevent中调用。这么一趟下来,确实有些繁琐或者说是过度设计。但是如果维度设定在大的项目中多人合作开发的时候,却能规避很多问题,每个分层组件都有自己的职责互不干扰,都支持单独的开发测试,尽可能的做到依赖于抽象而不是具体的实现。

本篇文章就到这里,源码会在后面这个系列的文章里放出来,感谢您的阅读!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容