阅读视图

发现新文章,点击刷新页面。

Flutist - Flutter 模块化架构管理框架

简单而系统地管理你的 Flutter 模块化结构


简介

随着我们的项目增长,模块化似乎势在必行,而我最近发现了一个比较新的Flutter模块化管理框架——Flutist。它是一个专为 Flutter 应用设计的强大项目管理框架,灵感来源于 iOS 开发生态中的 Tuist[1]。它为管理大型 Flutter 项目提供了一套结构化的方法,具备模块化架构、集中式依赖管理和代码生成能力。

为什么选择 Flutist?

模块化是大型 Flutter 项目的标准方案——独立构建、并行开发、测试隔离,优势显而易见。但随着模块数量增长,管理开销也随之攀升。Flutist 通过自动化消除了这些开销。

类型安全的依赖管理

当模块超过 10 个时,包版本不一致的问题极易出现。在 package.dart 中声明一次版本,flutist generate 会自动生成 flutist_gen.dart,所有模块都能通过 IDE 自动补全和类型检查安全地引用依赖。


    
    
    
  // 1. package.dart — 只声明一次版本
Dependency(name: 'dio', version: '^5.3.0'),
Dependency(name: 'flutter_bloc', version: '^8.1.6'),

// 2. flutist_gen.dart — 由 flutist generate 自动生成
Dependency get dio         => dependencies.firstWhere((d) => d.name == 'dio');
Dependency get flutterBloc => dependencies.firstWhere((d) => d.name == 'flutter_bloc');
Module     get authDomain   => modules.firstWhere((m) => m.name == 'auth_domain');

// 3. project.dart — 类型安全引用(IDE 自动补全 ✅)
Module(
  name: 'auth_data',
  dependencies: [package.dependencies.dio, package.dependencies.flutterBloc],
  modules: [package.modules.authDomain],
),

集中式 pubspec.yaml 管理

每增加一个模块就多一个 pubspec.yaml。升级一个包的版本意味着要手动编辑每个引用它的文件。

Flutist 根据 project.dart 的声明自动同步所有 pubspec.yaml 文件。开发者只需编辑一个文件——project.dart


    
    
    
  $ flutist generate
✓ pubspec.yaml synced: app, auth_domain, auth_data, auth_presentation,
                      product_interface, product_implementation ... (24 total)
✓ all architecture rules passed
✓ done (0.8s)

架构规则自动化

仅靠文档和代码审查很难持续保持架构规则的一致性。在开发压力下,domain 层最终会导入 http,或者一个功能模块直接引用了另一个功能模块的实现。这些违规很难被发现,等到发现时往往已经扩散开来。

Flutist 将架构规则转化为可执行代码。在 strictMode: true(默认值)下,任何违规都会立即终止 generate。曾经只存在于文档中的原则,现在变成了构建关卡。


    
    
    
  $ flutist generate
✗ [B4] auth_domain → auth_data: 检测到反向依赖 — domain 不应依赖 data
   → 请从 auth_domain 中移除 Dio 导入,仅声明 Repository 接口
✗ generate 已终止(strictMode: true

即使是刚加入团队、对架构理解不深的新成员,在违反规则的那一刻也能获得清晰的反馈。架构违规不再依赖人工审查,工具会自动检查。

样板代码自动生成

模块化架构最大的痛点之一就是样板代码。每个新功能都需要创建 interfaceimplementationtestingtestsexample 包,每个包都有自己的 pubspec.yamllib/ 结构和 barrel 文件。

像 BLoC 这样的状态管理模式,每个功能都需要 event、state、BLoC、page 和 widget 文件。使用 flutist createflutist scaffold,一条命令就能生成所有这些内容。


    
    
    
  # 以 micro 类型创建 todos 功能 — 5 个包 + 完整结构自动化
$ flutist create --name todos --path features --options micro
✓ features/todos/todos_interface      已创建
✓ features/todos/todos_implementation 已创建
✓ features/todos/todos_testing        已创建
✓ features/todos/todos_tests          已创建
✓ features/todos/todos_example        已创建

# 使用 BLoC 脚手架生成所有文件
$ flutist scaffold --template bloc --name todos_overview --path features/todos/todos_implementation
✓ todos_overview_bloc.dart  已创建
✓ todos_overview_event.dart 已创建
✓ todos_overview_state.dart 已创建

核心特性

特性 说明
声明式 通过单一的 project.dart 文件声明整个项目结构
单一来源 所有依赖版本通过 package.dart 集中管理
规则即代码 架构违规会立即终止生成过程

安装


    
    
    
  dart pub global activate flutist

前置条件:Flutter SDK,并确保 ~/.pub-cache/bin 已添加到 PATH

快速开始

1. 初始化项目


    
    
    
  cd my_flutter_project
flutist init

Flutist 会根据上下文自动适配:

  • pubspec.yaml:询问是否创建新的 Flutter 项目
  • 存在 pubspec.yaml:询问是新建项目还是迁移现有项目
    • • 新项目:创建 app 模块,添加到工作区,生成 lib/main.dart
    • • 现有项目:仅创建配置文件,保留现有代码结构

2. 创建模块


    
    
    
  # 创建 Clean 架构模块
flutist create --name login --path features --options clean

# 创建 Microfeature 架构模块
flutist create --name network --path packages --options micro

# 创建 Lite 模块
flutist create --name auth --path packages --options lite

# 创建单一包
flutist create --name utils --path core

3. 管理依赖


    
    
    
  # 添加包(自动解析版本)
flutist pub add http bloc flutter_bloc

# 同步依赖到所有模块
flutist generate

4. 从自定义模板生成代码


    
    
    
  # 列出可用模板
flutist scaffold list

# 从模板生成
flutist scaffold feature --name login
flutist scaffold feature --name login --path lib/features

命令一览

命令 描述 用法
init 初始化新项目或现有项目 flutist init
create 创建新模块 flutist create --name <name> --path <path> [--options <type>]
generate 同步依赖并重新生成文件 flutist generate
check 检查架构规则(CI 友好,不修改文件) flutist check
test 并行运行所有模块的测试 flutist test [-m <module>]
scaffold 从模板生成代码 flutist scaffold <template> --name <name>
pub 管理依赖 flutist pub add <package>
graph 可视化模块依赖关系 flutist graph [--format <format>]
help 显示帮助信息 flutist help [command]

核心文件

文件 说明
package.dart 外部包版本和模块名称的单一真实来源,多行格式为解析必需
project.dart 声明模块依赖和模块间关系,由 flutist generate 读取
flutist_gen.dart 自动生成的类型安全访问器,提供 IDE 自动补全支持

项目结构

典型的 Flutist 项目结构:


    
    
    
  my_project/
├── project.dart              # 项目配置
├── package.dart              # 集中式依赖管理
├── pubspec.yaml              # 工作区配置
├── lib/                      # 根应用代码
│   └── main.dart
├── app/                      # 主应用模块
│   ├── lib/
│   │   └── app.dart
│   └── pubspec.yaml
├── features/                 # 功能模块
│   └── auth/
│       ├── auth_domain/
│       ├── auth_data/
│       └── auth_presentation/
├── packages/                 # 库模块
│   └── network/
│       ├── network_interface/
│       ├── network_implementation/
│       ├── network_testing/
│       ├── network_tests/
│       └── network_example/
└── flutist/
    ├── templates/            # 脚手架模板
    └── flutist_gen.dart      # 生成的代码

模块类型

flutist create 会生成层级包并自动在 project.dart 中配置依赖关系

Clean 架构 (--options clean)

3 层 Clean Architecture,最适合需要清晰关注点分离的功能模块。


    
    
    
  features/login/
├── login_domain/          # 业务规则、实体、用例(无外部依赖)
├── login_data/            # 仓库、数据源、DTO
└── login_presentation/    # UI 和状态管理

自动配置依赖presentation → domaindata → domain

规则:所有依赖箭头指向 domain,domain 不依赖任何东西。

Microfeature 架构 (--options micro)

5 层 Microfeature Architecture,最适合跨功能共享的可复用库。


    
    
    
  packages/network/
├── network_interface/         # 公共 API(抽象类、模型)
├── network_implementation/    # 具体实现
├── network_testing/           # 测试辅助、模拟对象
├── network_tests/             # 单元测试和集成测试
└── network_example/           # 模块演示应用

自动配置依赖implementation/testing → interfacetests/example → implementation + testing

规则:消费者只依赖 interface,组合根注入实现。

Lite 架构 (--options lite)

4 层 Microfeature lite(无 example),最适合内部 API。


    
    
    
  packages/auth/
├── auth_interface/
├── auth_implementation/
├── auth_testing/
└── auth_tests/

单一包(省略 --options

无层级,最适合工具类、共享模型或应用外壳。


    
    
    
  core/utils/
├── lib/
│   └── utils.dart
└── pubspec.yaml

架构验证

flutist generateflutist check 自动执行以下规则:

规则 说明
实现引用 只有组合根(默认:app)和同功能测试/example 可以引用 _implementation
测试层隔离 _testing 包被排除在生产依赖之外
Example 独立性 _example 模块不能被任何生产代码引用
方向强制 同功能层级遵循声明的依赖方向
循环依赖 通过 DFS 遍历检测,绝不允许

配置选项


    
    
    
  // project.dart
ProjectOptions(
  strictModetrue,              // true(默认):违规时终止 / false:仅警告
  compositionRoots: ['app'],     // 允许引用 _implementation 的模块
)

Scaffold 脚手架模板

将重复性工作保存为模板,通过 flutist scaffold 自动化生成代码。

模板变量

.template 文件和 path 值中使用 {{变量}} 进行替换:

变量 输入 login_feature 输出
`{{name snake_case}}` login_feature login_feature
`{{name pascal_case}}` login_feature LoginFeature
`{{name camel_case}}` login_feature loginFeature
`{{name upper_case}}` login_feature LOGIN_FEATURE

template.yaml 结构


    
    
    
  description: "BLoC Feature Template"

attributes:
  - name: name
    required: true
  - name: path
    required: false
    default: "lib/features"

items:
  - type: file
    path"{{path}}/{{name | snake_case}}/{{name | snake_case}}_bloc.dart"
    templatePath: "bloc.dart.template"

  - typestring
    path"{{path}}/{{name | snake_case}}/README.md"
    contents: |
      # {{name | pascal_case}}

Item 类型

类型 说明
file 读取 .template 文件,替换变量后生成
string 使用内联内容直接生成文件
directory 复制整个模板目录

实战示例:BLoC Feature 模板

bloc.dart.template


    
    
    
  import 'package:bloc/bloc.dart';

part '{{name | snake_case}}_event.dart';
part '{{name | snake_case}}_state.dart';

class {{name | pascal_case}}Bloc
    extends Bloc<{{name | pascal_case}}Event, {{name | pascal_case}}State> {
  {{name | pascal_case}}Bloc() : super(const {{name | pascal_case}}Initial()) {
    on<{{name | pascal_case}}Started>(_onStarted);
  }

  Future<void_onStarted(
    {{name | pascal_case}}Started event,
    Emitter<{{name | pascal_case}}State> emit,
  ) async {}
}

event.dart.template


    
    
    
  part of '{{name | snake_case}}_bloc.dart';

sealed class {{name | pascal_case}}Event {
  const {{name | pascal_case}}Event();
}

final class {{name | pascal_case}}Started extends {{name | pascal_case}}Event {
  const {{name | pascal_case}}Started();
}

state.dart.template


    
    
    
  part of '{{name | snake_case}}_bloc.dart';

sealed class {{name | pascal_case}}State {
  const {{name | pascal_case}}State();
}

final class {{name | pascal_case}}Initial extends {{name | pascal_case}}State {
  const {{name | pascal_case}}Initial();
}

final class {{name | pascal_case}}Loading extends {{name | pascal_case}}State {
  const {{name | pascal_case}}Loading();
}

final class {{name | pascal_case}}Loaded<T> extends {{name | pascal_case}}State {
  final T data;
  const {{name | pascal_case}}Loaded(this.data);
}

final class {{name | pascal_case}}Error extends {{name | pascal_case}}State {
  final String message;
  const {{name | pascal_case}}Error(this.message);
}

运行生成


    
    
    
  $ flutist scaffold bloc_feature --name login --path lib/features

✓ lib/features/login/login_bloc.dart
✓ lib/features/login/login_event.dart
✓ lib/features/login/login_state.dart

实战示例:Riverpod Notifier 模板

notifier.dart.template


    
    
    
  import 'package:riverpod_annotation/riverpod_annotation.dart';
import '{{name | snake_case}}_state.dart';

part '{{name | snake_case}}_notifier.g.dart';

@riverpod
class {{name | pascal_case}}Notifier extends _${{name | pascal_case}}Notifier {
  @override
  {{name | pascal_case}}State build() => const {{name | pascal_case}}State.initial();

  Future<void> load() async {
    state = const {{name | pascal_case}}State.loading();
    try {
      state = const {{name | pascal_case}}State.loaded(null);
    } catch (e) {
      state = {{name | pascal_case}}State.error(e.toString());
    }
  }
}

state.dart.template


    
    
    
  import 'package:freezed_annotation/freezed_annotation.dart';

part '{{name | snake_case}}_state.freezed.dart';

@freezed
class {{name | pascal_case}}State with _${{name | pascal_case}}State {
  const factory {{name | pascal_case}}State.initial()            = _Initial;
  const factory {{name | pascal_case}}State.loading()            = _Loading;
  const factory {{name | pascal_case}}State.loaded(dynamic data) = _Loaded;
  const factory {{name | pascal_case}}State.error(String msg)    = _Error;
}

示例项目

Clean Architecture 示例

flutist_clean_architecture[2]

  • • Domain、Data、Presentation 三层 Clean Architecture
  • • 集中式依赖管理
  • • 大型 Flutter 应用最佳实践

Microfeature Architecture 示例

flutist_microfeature_architecture[3]

  • • Interface、Implementation、Tests、Testing 四层 Microfeature 架构
  • • 完全隔离的可复用库模块
  • • 集中式依赖管理

相关链接

资源 链接
📦 pub.dev pub.dev/packages/fl…
📖 文档网站 deepwiki.com/seonwooke/f…
💻 GitHub github.com/seonwooke/f…

引用链接

[1] Tuist: tuist.io/
[2] flutist_clean_architecture: github.com/seonwooke/f…
[3] flutist_microfeature_architecture: github.com/seonwooke/f…

浅谈Getx删库跑库了

关注我公众号的人朋友都知道,昨天在Flutter圈子发生了一件不大不小的事情:《Getx删库跑路了》。

Screenshot 2026-04-14 at 13.39.36.png

准确地说,不是Getx删库跑路了,是连同作者一同蒸发了。这看起来十分诡异,看起不像是主动行为,个人觉得更像是被封禁了,而不是出于商业原因。至于是不是被Github误封就不好说了,毕竟Github之前也误封过ts版fastmcp。

如果是被封,那么一切看起来没那么糟糕。

大家怎么看?

Getx是一个应用广泛的Flutter框架,是一个庞大且全能的框架,涵盖了状态管理、路由管理、依赖管理等等。某种程度上可以说是“Get在手,天下我有”,但社区对Getx评价是褒贬不一的,最核心的在于:

对于有经验的可能是把利器
对于有新手可能是灾难

Getx这个框架把太多的Flutter细节隐藏起来了,这个对于新手极其不友好的。接手Getx满天飞的项目更有可能是一场灾难

我们有个群友就叫:Get劝退师

所以当Getx删库后,除了震惊、shit之外,还有人说:

这玩意就不应该存在,现在删也不晚

怎么办?

暂时不用慌

对于现在使用Get的项目,如果没有什么紧急的bug,暂时确实不用慌。首先Github上的Get跑路了,但pub.dev上的不会跑路。

image.png

甚至我们可以在pub.dev上下载到源码,实在不行到.pub-cache目录中把Getx源码给捞出来。

有了源码可以把它放到Git或者本地做为依赖。

未来

我相信在不久的将来会有社区的fork出来,毕竟这个框架还是很有热度的,不会就这么突然的沉没在历史的长河中,更何况现在是一个AI的时代。

但也如同上图所示,距4.7.3发布已经4个多月了,而4.7.2也是14个月之前的事了,而最新5.0的候选版本也是14个月前的事了,所以,个人浅显地认Get的活跃度并不能算太高,当然了这也可以和Get趋于稳定有关,但我还是建议迁移到别的库。以下是我推荐的几种方案:

  • Notifier + go_router
  • Bloc + go_router
  • riverpod + go_router
  • signals + go_router

至于哪套方案比较好,就仁者见仁智者见智了。大家可以借助AI做迁移,但也需要注意AI有很多时候会把一些边缘case遗漏掉,一定要做好回归测试

启示?

启示就是如果能不依赖第三方,就不依赖,如果非要依赖,那就做好第三方依赖的隔离

完全不用第三方看起来更像是天方夜谭。

类似话题我在之前的推文中也聊过。这其实不仅仅是Flutter的问题,是几乎软件项目都会面临的问题:跑路的跑路,API破坏掉的破坏掉。

所以,做好依赖的隔离显得尤为重要,即使现在有了强大AI,我也相信你也不太愿意在一次迁移中看到300多的文件变动吧……

所以,如果你非要我给你推荐一个Getx的迁移方案,我会首推Notifier + go_router,原因不仅是这Flutter 官方架构方案,也是对外依赖比较小的一种方案。我也有用到了provider,但是用来做DI,仅在最外层使用,如果provider有一天不维护,迁移起来不会太痛苦。

结语

嘴笨,不知道说什么。

❌