在软件开发中,我们不仅要在代码实现中遵守常见的设计模式,更需要在架构设计中遵从基本的设计原则。而在这其中,DRY(即 Don't Repeat Yourself)原则可以算是最重要的一个。通俗来讲,DRY 原则就是"不要重复"。这是一个很朴素的概念,因为即使是最初级的开发者,在写了一段时间代码后,也会不自觉地把一些常用的重复代码抽取出来,放到公用的函数、类或是独立的组件库中,从而实现代码复用。在软件开发中,我们通常从架构设计中就要考虑如何去管理重复性(即代码复用),即如何将功能进行分治,将大问题分解为多个较为独立的小问题。而在这其中,组件化和平台化就是客户端开发中最流行的分治手段。
组件化
组件化又叫模块化,即基于可重用的目的,将一个大型软件系统(App)按照关注点分离的方式,拆分成多个独立的组件或模块。每个独立的组件都是一个单独的系统,可以单独维护、升级甚至直接替换,也可以依赖于别的独立组件,只要组件提供的功能不发生变化,就不会影响其他组件和软件系统的整体功能。

组件化的中心思想是将独立的功能进行拆分,而在拆分粒度上,组件化的约束则较为松散。一个独立的组件可以是一个软件包(Package)、页面、UI 控件,甚至可能是封装了一些函数的模块。
组件的粒度可大可小,那我们如何才能做好组件的封装重用呢?哪些代码应该被放到一个组件中?这里有一些基本原则,包括单一性原则、抽象化原则、稳定性原则和自完备性原则。
单一性原则指的是,每个组件仅提供一个功能。分而治之是组件化的中心思想,每个组件都有自己固定的职责和清晰的边界,专注地做一件事儿,这样这个组件才能良性发展。
一个反例是 Common 或 Util 组件,这类组件往往是因为在开发中出现了定义不明确、归属边界不清晰的代码:"哎呀,这段代码放哪儿好像都不合适,那就放 Common(Util)吧"。久而久之,这类组件就变成了无人问津的垃圾堆。所以,遇到不知道该放哪儿的代码时,就需要重新思考组件的设计和职责了。
抽象化原则指的是,组件提供的功能抽象应该尽量稳定,具有高复用度。而稳定的直观表现就是对外暴露的接口很少发生变化,要做到这一点,需要我们提升对功能的抽象总结能力,在组件封装时做好功能抽象和接口设计,将所有可能发生变化的因子都在组件内部做好适配,不要暴露给它的调用方。
稳定性原则指的是,不要让稳定的组件依赖不稳定的组件。比如组件 1 依赖了组件 5,如果组件 1 很稳定,但是组件 5 经常变化,那么组件 1 也就会变得不稳定了,需要经常适配。如果组件 5 里确实有组件 1 不可或缺的代码,我们可以考虑把这段代码拆出来单独做成一个新的组件 X,或是直接在组件 1 中拷贝一份依赖的代码。
自完备性,即组件需要尽可能地做到自给自足,尽量减少对其他底层组件的依赖,达到代码可复用的目的。比如,组件 1 只是依赖某个大组件 5 中的某个方法,这时更好的处理方法是,剥离掉组件 1 对组件 5 的依赖,直接把这个方法拷贝到组件 1 中。这样一来组件 1 就能够更好地应对后续的外部变更了。
我们再来看看组件化的具体实施步骤
首先,我们需要剥离应用中与业务无关的基础功能,比如网络请求、组件中间件、第三方库封装、UI 组件等,将它们封装为独立的基础库;然后,我们在项目里用 pub 进行管理。如果是第三方库,考虑到后续的维护适配成本,我们最好再封装一层,使项目不直接依赖外部代码,方便后续更新或替换。
基础功能已经封装成了定义更清晰的组件,接下来就可以按照业务维度,比如首页、详情页、搜索页等,去拆分独立的模块了。拆分的粒度可以先粗后细,只要能将大体划分清晰的业务组件进行拆分,后续就可以通过分布迭代、局部微调,最终实现整个业务项目的组件化。
在业务组件和基础组件都完成拆分封装后,应用的组件化架构就基本成型了,最后就可以按照刚才我们说的 4 个原则,去修正各个组件向下的依赖,以及最小化对外暴露的能力了。
Flutter 项目组件化(模块化)实现
下面以命令行(CLI)为主,演示如何在 Flutter 中快速搭建一个多模块工程。
一、目录规划
建议项目结构如下:
bash
/my_app # 主工程
├── pubspec.yaml
└── lib
/modules # 存放各功能模块
├── auth_module
└── profile_module
二、创建主工程
bash
# 在任意工作目录下
flutter create my_app
cd my_app
三、创建 Dart 功能包模块
我们使用 --template=package
来生成纯 Dart 包
ini
# 回到项目根目录
cd ..
# 创建存放模块的文件夹
mkdir modules
cd modules
# 创建两个功能模块:auth_module、profile_module
flutter create --template=package --org com.example auth_module
flutter create --template=package --org com.example profile_module
此时,modules/auth_module/lib/
下已有一个示例 Dart 文件。你可以在各模块内按需组织代码、资源、依赖。
四、在主工程中引入模块
编辑 my_app/pubspec.yaml
,在 dependencies:
下加入本地路径依赖:
yaml
dependencies:
flutter:
sdk: flutter
# 本地模块依赖
auth_module:
path: ../modules/auth_module
profile_module:
path: ../modules/profile_module
执行:
arduino
cd my_app
flutter pub get
这样,主工程就可以:
arduino
import 'package:auth_module/auth_module.dart';
import 'package:profile_module/profile_module.dart';
五、Android 平台配置
若模块中含有 Android 原生代码(如使用 --template=plugin
创建的模块),需在主工程的 android/settings.gradle
和 android/app/build.gradle
中注册子项目。
-
打开
my_app/android/settings.gradle
,末尾添加:phpinclude ':auth_module' project(':auth_module').projectDir = file('../modules/auth_module/android') include ':profile_module' project(':profile_module').projectDir = file('../modules/profile_module/android')
-
打开
my_app/android/app/build.gradle
,在dependencies
中引入:javadependencies { implementation project(':auth_module') implementation project(':profile_module') // ... 其他依赖 }
-
再次同步并运行:
arduinocd my_app flutter clean flutter run
六、iOS 平台配置
同理,若模块内含 iOS 原生代码,需要在主工程的 iOS Podfile 中添加路径依赖:
-
打开
my_app/ios/Podfile
,在target 'Runner' do
内加入:inipod 'auth_module', :path => '../modules/auth_module/ios' pod 'profile_module', :path => '../modules/profile_module/ios'
-
安装 Pod:
bashcd my_app/ios pod install
-
回到根目录,运行:
arduinocd .. flutter clean flutter run
七、模块化开发流程建议
-
代码隔离
- 每个模块内部只关心自身功能,公用工具抽到
common
或core
模块。
- 每个模块内部只关心自身功能,公用工具抽到
-
版本管理
- 模块也可以发布到私有或公有 Dart 仓库,使用版本号管理依赖。
-
单元测试与 CI
- 各模块独立维护测试用例,CI Pipelines 可针对单模块或全量运行。
通过以上步骤,即可快速搭建一个基于 CLI 的 Flutter 多模块工程,实现关注点分离、可复用、易维护的组件化结构。
八、如何将一个 Flutter/Dart 模块发布到公有仓库(pub.dev),以及如何在主工程中使用语义化版本号管理依赖。
1. 在 pub.dev 上创建 API Token
- 打开 pub.dev ,登录你的 Google/GitHub 帐号。
- 点击右上角头像 → Account → API tokens → Create token。
- 复制生成的 token(形如
xxxxxxxxxxxxxxxxxxxxxxxx
),后续在本地配置使用。
2. 在本地添加认证凭据
方法 A:使用 dart pub token
(推荐)
macOS 终端执行:
csharp
# 将 <YOUR_TOKEN> 替换为上一步获得的 token
dart pub token add --server=https://pub.dev <YOUR_TOKEN>
- 该命令会把凭据写入
~/.config/dart/pub-credentials.json
,以后dart pub publish
或flutter pub publish
都能自动生效。
方法 B:环境变量(不太常用)
ini
export PUB_TOKEN=<YOUR_TOKEN>
# 可选:把这一行加到 ~/.zshrc 或 ~/.bash_profile 以便每次登录都生效
3. 配置模块的 pubspec.yaml
在你的模块目录(如 modules/auth_module
)下,确保 pubspec.yaml
包含:
yaml
name: auth_module
description: A reusable authentication module for Flutter apps.
version: 1.0.0 # 遵循 SemVer
homepage: https://example.com/auth_module # 可选
author: Your Name <[email protected]> # 可选
publish_to: https://pub.dev # 发布到公有仓库(也可删掉此行,默认为 pub.dev)
-
version
:MAJOR.MINOR.PATCH- MAJOR:不兼容的 API 变更
- MINOR:向下兼容的新功能
- PATCH:向下兼容的 Bug 修复
4. 预检发布(dry run)
在模块根目录运行:
bash
cd modules/auth_module
dart pub publish --dry-run
- 修正
pubspec.yaml
警告(如缺少描述、缺少 LICENSE 等)。 - 确保所有示例代码、README、CHANGELOG 都准备齐全。
5. 正式发布
rust
dart pub publish
- 如果使用 Flutter 项目,也可运行
flutter pub publish
,效果一样。 - 按提示确认,一旦发布成功,你的包就能在 pub.dev/ 上找到。
6. 在主工程中使用版本号管理依赖
编辑主工程 pubspec.yaml
,在 dependencies:
下加入:
yaml
dependencies:
flutter:
sdk: flutter
# 使用 caret 语法,接受 1.0.x 的所有版本,但 <2.0.0
auth_module: ^1.0.0
^1.0.0
等同于>=1.0.0 <2.0.0
- 如果你发布了
1.1.0
,^1.0.0
会自动拉取1.1.0
;在2.0.0
发布前都有效。
运行:
arduino
flutter pub get
会在项目根目录生成或更新 pubspec.lock
,锁定具体版本。
7. 后续版本迭代
-
修复 Bug → 修改
pubspec.yaml
中的 PATCH:makefileversion: 1.0.0 → 1.0.1
-
新增向下兼容特性 → 修改 MINOR:
makefileversion: 1.0.1 → 1.1.0
-
不兼容改动 → 修改 MAJOR:
makefileversion: 1.1.0 → 2.0.0
-
重复 步骤 4 、步骤 5 发布新版本。
主工程只需在 pubspec.yaml
中调整版本约束(如 ^1.1.0
),然后再跑一次 flutter pub get
即可升级到最新符合约束的版本。
平台化
组件是个松散的广义概念,其规模取决于我们封装的功能维度大小,而各个组件之间的关系也仅靠依赖去维持。如果组件之间的依赖关系比较复杂,就会在一定程度上造成功能耦合现象。
如下所示的组件示意图中,组件 2 和组件 3 同时被多个业务组件和基础功能组件直接引用,甚至组件 2 和组件 5、组件 3 和组件 4 之间还存在着循环依赖的情况。一旦这些组件的内部实现和外部接口发生变化,整个 App 就会陷入不稳定的状态,即所谓牵一发而动全身。

平台化是组件化的升级,即在组件化的基础上,对它们提供的功能进行分类,增加依赖治理的概念。为了对这些功能单元在概念上进行更为统一的分类,我们按照四象限分析法,把应用程序的组件按照业务和 UI 分解为 4 个维度

可以看出,经过业务与 UI 的分解之后,这些组件可以分为 4 类:
- 具备 UI 属性的独立业务模块;
- 不具备 UI 属性的基础业务功能;
- 不具备业务属性的 UI 控件
- 不具备业务属性的基础功能
这 4 类组件其实隐含着分层依赖的关系。比如,处于业务模块中的首页,依赖位于基础业务模块中的账号功能;再比如,位于 UI 控件模块中的轮播卡片,依赖位于基础功能模块中的存储管理等功能。将它们按照依赖的先后顺序从上到下进行划分,就是一个完整的 App 了

可以看到,平台化与组件化最大的差异在于增加了分层的概念,每一层的功能均基于同层和下层的功能之上,这使得各个组件之间既保持了独立性,同时也具有一定的弹性,在不越界的情况下按照功能划分各司其职。与组件化更关注组件的独立性相比,平台化更关注的是组件之间关系的合理性,而这也是在设计平台化架构时需要重点考虑的单向依赖原则。
所谓单向依赖原则,指的是组件依赖的顺序应该按照应用架构的层数从上到下依赖,不要出现下层模块依赖上层模块这样循环依赖的现象。这样可以最大限度地避免复杂的耦合,减少组件化时的困难。如果我们每个组件都只是单向依赖其他组件,各个组件之间的关系都是清晰的,代码解耦也就会变得非常轻松了。
平台化强调依赖的顺序性,除了不允许出现下层组件依赖上层组件的情况,跨层组件和同层组件之间的依赖关系也应当严格控制,因为这样的依赖关系往往会带来架构设计上的混乱。
如果下层组件确实需要调用上层组件的代码怎么办?这时,我们可以采用增加中间层的方式,比如 Event Bus、Provider 或 Router,以中间层转发的形式实现信息同步。比如,位于第 4 层的网络引擎中,会针对特定的错误码跳转到位于第 1 层的统一错误页,这时我们就可以利用 Router 提供的命名路由跳转,在不感知错误页的实现情况下来完成。又比如,位于第 2 层的账号组件中,会在用户登入登出时主动刷新位于第 1 层的首页和我的页面,这时我们就可以利用 Event Bus 来触发账号切换事件,在不需要获取页面实例的情况下通知它们更新界面。
平台化架构是目前应用最广的软件架构设计,其核心在于如何将离散的组件依照单向依赖的原则进行分层。而关于具体的分层逻辑,除了上面介绍的业务和 UI 四象限法则之外,也可以使用其他的划分策略,只要整体结构层次清晰明确,不存在难以确定归属的组件就可以了。
比如,Flutter 就采用 Embedder(操作系统适配层)、Engine(渲染引擎及 Dart VM 层)和 Framework(UI SDK 层)整体三层的划分。可以看到,Flutter 框架每一层的组件定义都有着明确的边界,其向上提供的功能和向下依赖的能力也非常明确。
