如何组织合理稳定的Flutter工程结构?

在软件开发中,我们不仅要在代码实现中遵守常见的设计模式,更需要在架构设计中遵从基本的设计原则。而在这其中,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.gradleandroid/app/build.gradle 中注册子项目。

  1. 打开 my_app/android/settings.gradle,末尾添加:

    php 复制代码
    include ':auth_module'
    project(':auth_module').projectDir = file('../modules/auth_module/android')
    
    include ':profile_module'
    project(':profile_module').projectDir = file('../modules/profile_module/android')
  2. 打开 my_app/android/app/build.gradle,在 dependencies 中引入:

    java 复制代码
    dependencies {
        implementation project(':auth_module')
        implementation project(':profile_module')
        // ... 其他依赖
    }
  3. 再次同步并运行:

    arduino 复制代码
    cd my_app
    flutter clean
    flutter run

六、iOS 平台配置

同理,若模块内含 iOS 原生代码,需要在主工程的 iOS Podfile 中添加路径依赖:

  1. 打开 my_app/ios/Podfile,在 target 'Runner' do 内加入:

    ini 复制代码
    pod 'auth_module', :path => '../modules/auth_module/ios'
    pod 'profile_module', :path => '../modules/profile_module/ios'
  2. 安装 Pod:

    bash 复制代码
    cd my_app/ios
    pod install
  3. 回到根目录,运行:

    arduino 复制代码
    cd ..
    flutter clean
    flutter run

七、模块化开发流程建议

  1. 代码隔离

    • 每个模块内部只关心自身功能,公用工具抽到 commoncore 模块。
  2. 版本管理

    • 模块也可以发布到私有或公有 Dart 仓库,使用版本号管理依赖。
  3. 单元测试与 CI

    • 各模块独立维护测试用例,CI Pipelines 可针对单模块或全量运行。

通过以上步骤,即可快速搭建一个基于 CLI 的 Flutter 多模块工程,实现关注点分离、可复用、易维护的组件化结构。

八、如何将一个 Flutter/Dart 模块发布到公有仓库(pub.dev),以及如何在主工程中使用语义化版本号管理依赖。


1. 在 pub.dev 上创建 API Token

  1. 打开 pub.dev ,登录你的 Google/GitHub 帐号。
  2. 点击右上角头像 → AccountAPI tokensCreate token
  3. 复制生成的 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 publishflutter 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 <you@example.com>        # 可选
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. 后续版本迭代

  1. 修复 Bug → 修改 pubspec.yaml 中的 PATCH:

    makefile 复制代码
    version: 1.0.0 → 1.0.1
  2. 新增向下兼容特性 → 修改 MINOR:

    makefile 复制代码
    version: 1.0.1 → 1.1.0
  3. 不兼容改动 → 修改 MAJOR:

    makefile 复制代码
    version: 1.1.0 → 2.0.0
  4. 重复 步骤 4步骤 5 发布新版本。

主工程只需在 pubspec.yaml 中调整版本约束(如 ^1.1.0),然后再跑一次 flutter pub get 即可升级到最新符合约束的版本。


平台化

组件是个松散的广义概念,其规模取决于我们封装的功能维度大小,而各个组件之间的关系也仅靠依赖去维持。如果组件之间的依赖关系比较复杂,就会在一定程度上造成功能耦合现象。

如下所示的组件示意图中,组件 2 和组件 3 同时被多个业务组件和基础功能组件直接引用,甚至组件 2 和组件 5、组件 3 和组件 4 之间还存在着循环依赖的情况。一旦这些组件的内部实现和外部接口发生变化,整个 App 就会陷入不稳定的状态,即所谓牵一发而动全身。

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

可以看出,经过业务与 UI 的分解之后,这些组件可以分为 4 类:

  1. 具备 UI 属性的独立业务模块;
  2. 不具备 UI 属性的基础业务功能;
  3. 不具备业务属性的 UI 控件
  4. 不具备业务属性的基础功能

这 4 类组件其实隐含着分层依赖的关系。比如,处于业务模块中的首页,依赖位于基础业务模块中的账号功能;再比如,位于 UI 控件模块中的轮播卡片,依赖位于基础功能模块中的存储管理等功能。将它们按照依赖的先后顺序从上到下进行划分,就是一个完整的 App 了

可以看到,平台化与组件化最大的差异在于增加了分层的概念,每一层的功能均基于同层和下层的功能之上,这使得各个组件之间既保持了独立性,同时也具有一定的弹性,在不越界的情况下按照功能划分各司其职。与组件化更关注组件的独立性相比,平台化更关注的是组件之间关系的合理性,而这也是在设计平台化架构时需要重点考虑的单向依赖原则。

所谓单向依赖原则,指的是组件依赖的顺序应该按照应用架构的层数从上到下依赖,不要出现下层模块依赖上层模块这样循环依赖的现象。这样可以最大限度地避免复杂的耦合,减少组件化时的困难。如果我们每个组件都只是单向依赖其他组件,各个组件之间的关系都是清晰的,代码解耦也就会变得非常轻松了。

平台化强调依赖的顺序性,除了不允许出现下层组件依赖上层组件的情况,跨层组件和同层组件之间的依赖关系也应当严格控制,因为这样的依赖关系往往会带来架构设计上的混乱。

如果下层组件确实需要调用上层组件的代码怎么办?这时,我们可以采用增加中间层的方式,比如 Event Bus、Provider 或 Router,以中间层转发的形式实现信息同步。比如,位于第 4 层的网络引擎中,会针对特定的错误码跳转到位于第 1 层的统一错误页,这时我们就可以利用 Router 提供的命名路由跳转,在不感知错误页的实现情况下来完成。又比如,位于第 2 层的账号组件中,会在用户登入登出时主动刷新位于第 1 层的首页和我的页面,这时我们就可以利用 Event Bus 来触发账号切换事件,在不需要获取页面实例的情况下通知它们更新界面。

平台化架构是目前应用最广的软件架构设计,其核心在于如何将离散的组件依照单向依赖的原则进行分层。而关于具体的分层逻辑,除了上面介绍的业务和 UI 四象限法则之外,也可以使用其他的划分策略,只要整体结构层次清晰明确,不存在难以确定归属的组件就可以了。

比如,Flutter 就采用 Embedder(操作系统适配层)、Engine(渲染引擎及 Dart VM 层)和 Framework(UI SDK 层)整体三层的划分。可以看到,Flutter 框架每一层的组件定义都有着明确的边界,其向上提供的功能和向下依赖的能力也非常明确。

相关推荐
还鮟8 分钟前
CTF Web的数组巧用
android
DemonAvenger12 分钟前
深入理解Go的网络I/O模型:优势、实践与踩坑经验
网络协议·架构·go
伍哥的传说1 小时前
鸿蒙系统(HarmonyOS)应用开发之手势锁屏密码锁(PatternLock)
前端·华为·前端框架·harmonyos·鸿蒙
yugi9878381 小时前
前端跨域问题解决Access to XMLHttpRequest at xxx from has been blocked by CORS policy
前端
小蜜蜂嗡嗡1 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi002 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
浪裡遊2 小时前
Sass详解:功能特性、常用方法与最佳实践
开发语言·前端·javascript·css·vue.js·rust·sass
旧曲重听12 小时前
最快实现的前端灰度方案
前端·程序人生·状态模式
默默coding的程序猿3 小时前
3.前端和后端参数不一致,后端接不到数据的解决方案
java·前端·spring·ssm·springboot·idea·springcloud
夏梦春蝉3 小时前
ES6从入门到精通:常用知识点
前端·javascript·es6