Flutter Modular - Module 完全指南
Flutter Modular - Module 完全指南
本文档详细介绍 flutter_modular 中 Module 的用法、依赖注入机制和作用域规则。
📋 目录
- [一、Module 基础概念](#一、Module 基础概念)
- [二、Module 的核心方法](#二、Module 的核心方法)
- 三、依赖注入详解
- [四、Module 作用域规则](#四、Module 作用域规则)
- 五、最佳实践
- 六、常见问题
- [七、Kazumi 项目实例分析](#七、Kazumi 项目实例分析)
一、Module 基础概念
1.1 什么是 Module?
定义: Module 是 flutter_modular 的核心概念,用于组织和管理应用的功能模块。
简单理解:
- 📦 Module = 功能包(把相关的页面、控制器、服务打包在一起)
- 🏢 Module = 部门(如市场部、技术部,每个部门有自己的职责)
类比:
公司组织架构
└─ 总公司 (AppModule)
└─ 运营总部 (IndexModule)
├─ 市场部 (PopularModule) - 负责热门内容
├─ 生产部 (TimelineModule) - 负责时间表
├─ 销售部 (CollectModule) - 负责收藏管理
├─ 人事部 (MyModule) - 负责用户设置
├─ 技术部 (VideoModule) - 负责视频播放
└─ 客服部 (InfoModule) - 负责番剧详情1.2 为什么使用 Module?
传统方式的问题:
// ❌ 所有代码堆在一起
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/': (_) => HomePage(),
'/popular': (_) => PopularPage(),
'/timeline': (_) => TimelinePage(),
'/collect': (_) => CollectPage(),
'/video': (_) => VideoPage(),
// ... 50 个页面
},
);
}
}问题:
- ❌ 难以维护:所有路由在一个文件里
- ❌ 代码耦合:页面之间直接调用
- ❌ 无法复用:想提取某个功能很困难
- ❌ 测试困难:无法单独测试某个功能
使用 Module 的优势:
// ✓ 模块化设计
class AppModule extends Module {
@override
void routes(r) {
r.module("/", module: IndexModule());
r.module("/video", module: VideoModule());
r.module("/settings", module: SettingsModule());
}
}
class IndexModule extends Module {
@override
void routes(r) {
r.child("/popular", child: (_) => PopularPage());
r.child("/timeline", child: (_) => TimelinePage());
}
@override
void binds(i) {
i.addSingleton(PopularController.new);
i.addSingleton(TimelineController.new);
}
}优势:
- ✅ 模块化:每个功能独立管理
- ✅ 解耦:模块间通过路由通信
- ✅ 可复用:Module 可以在不同项目中使用
- ✅ 易测试:可以单独测试某个模块
- ✅ 懒加载:只在需要时加载模块
二、Module 的核心方法
2.1 Module 的完整结构
import 'package:flutter_modular/flutter_modular.dart';
class MyModule extends Module {
// 1️⃣ 生命周期回调(可选)
@override
void onCreate() {
print("模块创建了");
}
// 2️⃣ 导入其他模块(可选)
@override
List<Module> get imports => [];
// 3️⃣ 注册依赖(可选)
@override
void binds(i) {
// 注册 Controller、Repository 等
}
// 4️⃣ 配置路由(必须)
@override
void routes(r) {
// 定义页面路由
}
// 5️⃣ 导出依赖(可选)
@override
List<Module> get exports => [];
// 6️⃣ 生命周期回调(可选)
@override
void dispose() {
print("模块销毁了");
}
}2.2 核心方法详解
方法 1:imports - 导入其他模块
作用: 引入其他 Module,让它们的功能在当前 Module 中可用。
语法:
@override
List<Module> get imports => [OtherModule()];Kazumi 中的例子:
// index_module.dart 第 28 行
class IndexModule extends Module {
@override
List<Module> get imports => menu.moduleList;
}这行代码的意思:
- ✅ 导入
menu.moduleList包含的所有模块 - ✅ 这些模块包括:PopularModule、TimelineModule、CollectModule、MyModule
- ✅ 导入后,IndexModule 可以使用这些模块里的所有功能
实际效果:
// router.dart 定义了 menu.moduleList
final MenuRoute menu = MenuRoute([
MenuRouteItem(path: "/popular", module: PopularModule()),
MenuRouteItem(path: "/timeline", module: TimelineModule()),
MenuRouteItem(path: "/collect", module: CollectModule()),
MenuRouteItem(path: "/my", module: MyModule()),
]);
// IndexModule 导入后,相当于拥有了这些模块
class IndexModule extends Module {
@override
List<Module> get imports => [
PopularModule(), // ← 导入
TimelineModule(), // ← 导入
CollectModule(), // ← 导入
MyModule(), // ← 导入
];
}imports 的作用:
- 共享依赖:导入的模块可以访问父模块的 binds
- 路由嵌套:可以在导入的模块中继续定义子路由
- 代码复用:避免重复注册相同的依赖
方法 2:binds - 注册依赖
作用: 告诉 Modular:"我需要这些工具(Controller、Repository 等),请帮我创建好。"
语法:
@override
void binds(i) {
i.addSingleton<接口类型>(实现类);
}Kazumi 中的完整例子:
// index_module.dart 第 31-50 行
@override
void binds(i) {
// ========== Repository 层(数据管理工具)==========
i.addSingleton<ICollectRepository>(CollectRepository.new);
i.addSingleton<ISearchHistoryRepository>(SearchHistoryRepository.new);
i.addSingleton<ICollectCrudRepository>(CollectCrudRepository.new);
i.addSingleton<IHistoryRepository>(HistoryRepository.new);
i.addSingleton<IDownloadRepository>(DownloadRepository.new);
i.addSingleton<IDownloadManager>(DownloadManager.new);
// ========== Controller 层(页面管理员)==========
i.addSingleton(PopularController.new);
i.addSingleton(PluginsController.new);
i.addSingleton(VideoPageController.new);
i.addSingleton(TimelineController.new);
i.addSingleton(CollectController.new);
i.addSingleton(HistoryController.new);
i.addSingleton(MyController.new);
i.addSingleton(ShadersController.new);
i.addSingleton(DownloadController.new);
}binds 的常见写法:
// 方式 1:带接口的单例(推荐用于 Repository 层)
i.addSingleton<IService>(ServiceClass.new);
// 方式 2:不带接口的单例(最常用)
i.addSingleton(MyController.new);
// 方式 3:每次都要新的实例(不常用)
i.addInstance(MyController());
// 方式 4:工厂模式(每次创建新的)
i.addFactory(MyController.new);
// 方式 5:异步依赖
i.addAsyncSingleton<MyService>(() async {
final service = MyService();
await service.initialize();
return service;
});addSingleton vs addInstance vs addFactory 的区别:
| 方法 | 创建时机 | 实例数量 | 适用场景 |
|---|---|---|---|
| addSingleton | 第一次使用时 | 1 个(全局唯一) | Controller、Repository |
| addInstance | 立即创建 | 1 个 | 已创建好的对象 |
| addFactory | 每次调用时 | N 个(每次都新) | 需要独立状态的对象 |
如何使用注册的依赖?
在页面中获取:
// popular_page.dart 第 32 行
final PopularController popularController = Modular.get<PopularController>();执行过程:
1. 调用 Modular.get<PopularController>()
↓
2. Modular 查找当前 Module 的 binds
↓
3. 如果没找到,去父 Module 查找
↓
4. 找到了,返回实例(如果是第一次,先创建)
↓
5. 以后每次获取的都是同一个实例(因为是 singleton)方法 3:routes - 配置路由
作用: 定义 URL 路径和页面的对应关系。
两种主要方法:
r.child() - 注册单个页面
r.child("/tab", // 路径
child: (_) => IndexPage(), // 这个路径显示哪个页面
children: [...], // 子路由(可选)
transition: TransitionType.fadeIn); // 切换动画Kazumi 中的例子:
// index_module.dart 第 56-67 行
r.child("/",
child: (_) => const InitPage(),
children: [
ChildRoute(
"/error",
child: (_) => Scaffold(
appBar: AppBar(title: const Text("Kazumi")),
body: const Center(child: Text("初始化失败")),
),
),
],
transition: TransitionType.noTransition);r.module() - 注册整个模块
r.module("/video", module: VideoModule());特点:
- ✅ 把整个模块交给 Modular 管理
- ✅ 该模块有自己的路由系统
- ✅ 适合复杂的功能模块
Kazumi 中的例子:
// index_module.dart 第 87-93 行
r.module("/video", module: VideoModule());
r.module("/info", module: InfoModule());
r.module("/settings", module: SettingsModule());
r.module("/search", module: SearchModule());r.child() vs r.module() 的区别:
| 特性 | r.child() | r.module() |
|---|---|---|
| 用途 | 注册单个页面 | 注册整个模块 |
| 参数 | child: Widget | module: Module |
| 路由系统 | 简单,无子路由 | 复杂,有独立路由 |
| 适用场景 | 简单页面 | 功能模块 |
| 示例 | r.child("/about", ...) | r.module("/settings", ...) |
完整的 routes 配置示例:
@override
void routes(r) {
// 1. 根路径 - 初始化页面
r.child("/",
child: (_) => const InitPage(),
children: [
ChildRoute("/error", child: (_) => ErrorPage()),
],
transition: TransitionType.noTransition);
// 2. 主界面 - 包含底部导航
r.child("/tab",
child: (_) => const IndexPage(),
children: menu.routes, // 导入其他路由
transition: TransitionType.fadeIn,
duration: Duration(milliseconds: 70));
// 3. 子模块 - 独立功能模块
r.module("/video", module: VideoModule());
r.module("/info", module: InfoModule());
r.module("/settings", module: SettingsModule());
r.module("/search", module: SearchModule());
}2.3 可选方法
onCreate() - 模块创建时的回调
@override
void onCreate() {
print("${runtimeType} 创建了");
// 可以做一些初始化工作
}dispose() - 模块销毁时的回调
@override
void dispose() {
print("${runtimeType} 销毁了");
// 可以清理资源
}exports - 导出依赖给其他模块
@override
List<Module> get exports => [
SharedDataModule(), // 导出共享数据模块
];作用: 让其他模块可以使用本模块的依赖。
三、依赖注入详解
3.1 什么是依赖注入?
定义: 依赖注入(Dependency Injection,DI)是一种设计模式,用于将对象的创建和使用分离。
通俗理解:
- ❌ 不用 DI:自己创建需要的对象
- ✅ 使用 DI:告诉别人你需要什么,别人给你
对比:
// ❌ 传统方式 - 手动创建依赖
class PopularPage extends StatefulWidget {
final PopularController controller = PopularController(); // 自己创建
@override
_PopularPageState createState() => _PopularPageState();
}
// ✓ 使用 Modular - 依赖注入
class PopularPage extends StatefulWidget {
@override
_PopularPageState createState() => _PopularPageState();
}
class _PopularPageState extends State<PopularPage> {
final PopularController controller = Modular.get<PopularController>(); // 别人给
@override
Widget build(BuildContext context) {
return Container();
}
}3.2 依赖注入的好处
好处 1:解耦
// ❌ 耦合
class PopularPage {
final PopularController controller = PopularController();
// 如果 PopularController 依赖 HttpClient,还要继续创建
final HttpClient http = HttpClient();
}
// ✓ 解耦
class PopularPage {
final PopularController controller = Modular.get<PopularController>();
// HttpClient 由 Modular 负责创建,PopularPage 不关心
}好处 2:易于测试
// 测试时可以替换为 Mock 对象
class MockPopularController extends PopularController {
@override
Future<void> loadData() async {
// 返回测试数据,不真正请求网络
}
}
// 测试代码
Modular.get<PopularController>().loadData(); // 实际使用
// 测试时使用 Mock 对象好处 3:单例管理
// binds 中注册
i.addSingleton(PopularController.new);
// 在任何地方获取都是同一个实例
final c1 = Modular.get<PopularController>();
final c2 = Modular.get<PopularController>();
print(c1 == c2); // true
// 好处:
// - 数据同步:一个地方修改,其他地方立即生效
// - 节省内存:只创建一个实例
// - 性能提升:避免重复创建3.3 依赖的生命周期
单例(Singleton)
i.addSingleton(MyController.new);生命周期:
- ✅ 第一次使用时创建
- ✅ 整个应用生命周期内只创建一次
- ✅ 所有地方共享同一个实例
工厂模式(Factory)
i.addFactory(MyController.new);生命周期:
- ✅ 每次调用
Modular.get()都创建新实例 - ✅ 适用于需要独立状态的场景
实例(Instance)
i.addInstance(myObject);生命周期:
- ✅ 立即创建
- ✅ 只能使用传入的这个实例
3.4 依赖查找算法
当调用 Modular.get<T>() 时:
T get<T>() {
// 1. 先在当前 Module 的 binds 中找
if (currentModule.binds.containsKey(T)) {
return currentModule.binds[T];
}
// 2. 如果没找到,去父 Module 的 binds 中找(递归)
if (currentModule.parent != null) {
return currentModule.parent.get<T>();
}
// 3. 如果还没找到,报错
throw ModularError('Dependency $T not found');
}这就是为什么子模块可以访问父模块的依赖!
四、Module 作用域规则
4.1 作用域层级
Module 之间有父子关系,形成树状结构:
AppModule (根/爷爷)
│
└─ IndexModule (父)
│ binds: [PopularController, VideoController, ...]
│
├─ PopularModule (子)
│ │ binds: []
│ └─ PopularPage
│
├─ VideoModule (子)
│ │ binds: [PlayerController]
│ └─ VideoPage
│
└─ InfoModule (子)
│ binds: []
└─ InfoPage4.2 作用域规则
规则 1:向下共享
子模块可以访问父模块的 binds
// IndexModule 注册
i.addSingleton(PopularController.new);
// PopularModule(子模块)中可以使用
final controller = Modular.get<PopularController>(); // ✓ 可以找到原因: 依赖查找算法会递归查找父模块
规则 2:向上不共享
父模块不能访问子模块的 binds
// VideoModule(子模块)注册
i.addSingleton(PlayerController.new);
// IndexModule(父模块)中不能使用
final controller = Modular.get<PlayerController>(); // ✗ 找不到!原因: 依赖查找只会向上找,不会向下找
规则 3:平级不共享
兄弟模块之间不能互相访问
// VideoModule 注册
i.addSingleton(PlayerController.new);
// PopularModule(兄弟模块)中不能使用
final controller = Modular.get<PlayerController>(); // ✗ 找不到!原因: 每个模块的作用域是独立的
4.3 实际案例分析
案例 1:在 PopularPage 中获取 IndexModule 的依赖
// index_module.dart L43
i.addSingleton(PopularController.new);
// popular_page.dart 第 32 行
final controller = Modular.get<PopularController>(); // ✓ 可以解析:
IndexModule (父)
binds: [PopularController]
└─ PopularModule (子)
└─ PopularPage
↓
Modular.get<PopularController>() ✓ 找到案例 2:在 VideoPage 中获取多个依赖
// index_module.dart L45
i.addSingleton(VideoPageController.new);
// video_module.dart L13
i.addSingleton(PlayerController.new);
// video_page.dart
final v1 = Modular.get<VideoPageController>(); // ✓ 可以(来自父模块)
final v2 = Modular.get<PlayerController>(); // ✓ 可以(来自自己的模块)解析:
IndexModule (父)
binds: [VideoPageController]
└─ VideoModule (子)
│ binds: [PlayerController]
└─ VideoPage
↓
Modular.get<VideoPageController>() ✓ 找到(爷爷)
Modular.get<PlayerController>() ✓ 找到(父亲)案例 3:跨模块获取依赖
// VideoModule 注册
i.addSingleton(PlayerController.new);
// InfoModule 中
final controller = Modular.get<PlayerController>(); // ✗ 找不到!解析:
IndexModule (父/爷爷)
├─ VideoModule (子 A)
│ │ binds: [PlayerController]
│ └─ VideoPage
│
└─ InfoModule (子 B)
└─ InfoPage
↓
Modular.get<PlayerController>() ✗ 找不到!
原因:InfoModule 和 VideoModule 是兄弟关系
依赖查找路径:InfoModule → IndexModule → AppModule
不会经过 VideoModule4.4 作用域总结表
| 位置 | 能获取哪些 binds |
|---|---|
| IndexModule 中的页面 | ✅ IndexModule 的 binds ❌ 所有子模块的 binds |
| PopularModule 中的页面 | ✅ PopularModule 的 binds(如果有) ✅ IndexModule 的 binds ✅ AppModule 的 binds(如果有) ❌ 其他兄弟模块的 binds |
| VideoModule 中的页面 | ✅ VideoModule 的 binds ✅ IndexModule 的 binds ✅ AppModule 的 binds ❌ 其他兄弟模块的 binds |
| InfoModule 中的页面 | ✅ InfoModule 的 binds(如果有) ✅ IndexModule 的 binds ✅ AppModule 的 binds ❌ 其他兄弟模块的 binds |
4.5 依赖覆盖
子模块可以覆盖父模块的依赖:
// 父模块注册默认实现
class ParentModule extends Module {
void binds(i) {
i.addSingleton<IService>(DefaultService.new);
}
}
// 子模块想要特殊实现
class ChildModule extends Module {
void binds(i) {
// 覆盖父模块的实现
i.addSingleton<IService>(SpecialService.new);
}
}
// 在 ChildModule 中
Modular.get<IService>() // 得到 SpecialService,不是 DefaultService应用场景:
- ✅ 测试时使用 Mock 服务
- ✅ 不同环境使用不同实现
- ✅ 特殊需求定制
五、最佳实践
5.1 什么时候用 r.child(),什么时候用 r.module()?
使用 r.child() 的场景:
// 场景 1:简单的独立页面
r.child("/about", child: (_) => AboutPage());
// 场景 2:有状态管理的页面
r.child("/popular",
child: (_) => PopularPage(),
children: [
ChildRoute("/detail", child: (_) => DetailPage()),
],
);
// 场景 3:不需要独立路由系统的功能
r.child("/history", child: (_) => HistoryPage());特点:
- ✅ 页面简单,逻辑不复杂
- ✅ 不需要额外的依赖注入
- ✅ 没有子页面或子页面很少
使用 r.module() 的场景:
// 场景 1:复杂的功能模块
r.module("/settings", module: SettingsModule());
// SettingsModule 内部可能有多个子页面
class SettingsModule extends Module {
void routes(r) {
r.child("/", child: (_) => SettingsPage());
r.child("/player", child: (_) => PlayerSettingsPage());
r.child("/danmaku", child: (_) => DanmakuSettingsPage());
r.child("/about", child: (_) => AboutSettingsPage());
}
void binds(i) {
i.addSingleton(SettingsController.new);
}
}
// 场景 2:需要独立依赖的功能
r.module("/video", module: VideoModule());
// VideoModule 有自己的 Controller
class VideoModule extends Module {
void binds(i) {
i.addSingleton(PlayerController.new);
i.addSingleton(DanmakuManager.new);
}
}
// 场景 3:可复用的功能模块
r.module("/download", module: DownloadModule());特点:
- ✅ 功能复杂,有多个子页面
- ✅ 需要独立的依赖注入
- ✅ 可能在其他地方复用
5.2 依赖注册的最佳实践
在父模块注册共享依赖:
// index_module.dart
void binds(i) {
// ✓ 所有子模块都需要网络请求
i.addSingleton(HttpClient.new);
// ✓ 所有子模块都需要日志
i.addSingleton(Logger.new);
// ✓ 所有子模块都用同一个主题配置
i.addSingleton(ThemeConfig.new);
// ✓ 所有页面都要用的 Controller
i.addSingleton(PopularController.new);
i.addSingleton(TimelineController.new);
}在子模块注册独占依赖:
// video_module.dart
void binds(i) {
// ✓ 只有视频模块需要播放器控制器
i.addSingleton(PlayerController.new);
// ✓ 只有视频模块需要弹幕管理器
i.addSingleton(DanmakuManager.new);
// ✓ 视频专用的解码器
i.addSingleton(VideoDecoder.new);
}避免的陷阱:
// ❌ 错误 1:在子模块注册应该在父模块的依赖
class PopularModule extends Module {
void binds(i) {
i.addSingleton(HttpClient.new); // ✗ 应该放在父模块
}
}
// ❌ 错误 2:重复注册
class ModuleA extends Module {
void binds(i) {
i.addSingleton(Logger.new); // ✗ 父模块已经注册了
}
}
// ❌ 错误 3:忘记注册
class ModuleB extends Module {
void binds(i) {
// ✗ 忘记注册 Controller
}
void routes(r) {
r.child("/", child: (_) => PageB());
}
}
// PageB 中调用 Modular.get<ControllerB>() 会报错!5.3 命名规范
// ✓ 好的命名
class PopularModule extends Module {}
class TimelineModule extends Module {}
class VideoPlayerModule extends Module {}
// ✗ 不好的命名
class PM extends Module {} // 缩写难以理解
class Module1 extends Module {} // 没有意义
class MyAwesomeModule extends Module {} // 太长5.4 模块划分原则
单一职责原则:
// ✓ 每个模块只负责一个功能
class PopularModule extends Module {} // 只负责热门推荐
class TimelineModule extends Module {} // 只负责时间表
class CollectModule extends Module {} // 只负责收藏管理
// ✗ 一个大模块包含所有功能
class AllInOneModule extends Module {
void routes(r) {
r.child("/popular", ...);
r.child("/timeline", ...);
r.child("/collect", ...);
r.child("/video", ...);
// ... 100 个页面
}
}高内聚低耦合:
// ✓ 模块内部高度相关,模块之间依赖少
class VideoModule extends Module {
void binds(i) {
i.addSingleton(VideoController.new);
i.addSingleton(PlayerController.new);
i.addSingleton(DanmakuManager.new);
// 所有视频相关的依赖都在一个模块
}
}
// ✗ 模块之间互相依赖
class ModuleA extends Module {
void binds(i) {
i.addSingleton(ModuleB().binds); // ✗ 直接引用其他模块
}
}六、常见问题
Q1: imports 和 Dart 的 import 有什么区别?
答: 完全不同!
// Dart 的 import - 导入代码文件(编译时需要)
import 'package:kazumi/pages/popular_controller.dart';
// Modular 的 imports - 导入整个模块的功能(运行时)
List<Module> get imports => [PopularModule()];区别:
| 特性 | Dart import | Modular imports |
|---|---|---|
| 作用 | 让编译器知道文件存在 | 让 Modular 加载模块的路由和依赖 |
| 时机 | 编译时 | 运行时 |
| 内容 | 类、函数、常量 | Module(包含 routes + binds) |
| 必须性 | 必须 | 可选 |
Q2: 为什么要在 binds 里注册 Controller?
答: 为了实现依赖注入,方便在页面中获取。
对比:
// ✗ 不用 Modular
class PopularPage extends StatefulWidget {
final controller = PopularController(); // 每次都要手动创建
@override
_PopularPageState createState() => _PopularPageState();
}
// ✓ 使用 Modular
class PopularPage extends StatefulWidget {
@override
_PopularPageState createState() => _PopularPageState();
}
class _PopularPageState extends State<PopularPage> {
final controller = Modular.get<PopularController>(); // 自动获取
@override
Widget build(BuildContext context) {
return Container();
}
}好处:
- ✅ 不用手动创建
- ✅ 所有地方用的是同一个实例
- ✅ 易于测试和替换
Q3: 如何调试 Module 的问题?
答: 在关键位置打印日志:
class MyModule extends Module {
@override
void onCreate() {
print("【${runtimeType}】创建了");
}
@override
void binds(i) {
print("【${runtimeType}】正在注册依赖");
}
@override
void routes(r) {
print("【${runtimeType}】正在配置路由");
}
}常见错误排查:
// 错误 1:Dependency not found
// 解决:检查 binds 是否注册了这个依赖
// 错误 2:Route not found
// 解决:检查 routes 是否配置了这个路径
// 错误 3:Circular dependency
// 解决:检查是否有循环导入Q4: 如何实现懒加载?
答: Modular 默认就是懒加载的。
// 只有在第一次访问 /video 时,VideoModule 才会被加载
r.module("/video", module: VideoModule());
// 好处:
// - 加快启动速度
// - 节省内存
// - 按需加载如果想预加载:
// 在应用启动时预加载
Modular.to.navigate('/video/');Q5: Module 什么时候销毁?
答: 当 Module 不再被使用时会自动销毁。
class MyModule extends Module {
@override
void dispose() {
print("${runtimeType} 销毁了");
// 可以在这里清理资源
}
}注意:
- ✅ 单例依赖会在整个应用生命周期内保持
- ✅ 非单例依赖会随 Module 一起销毁
七、Kazumi 项目实例分析
7.1 Module 层级结构
AppModule (lib/app_module.dart)
│
└─ IndexModule (lib/pages/index_module.dart)
│ imports: menu.moduleList
│ binds: [
│ ICollectRepository,
│ ISearchHistoryRepository,
│ PopularController,
│ PluginsController,
│ VideoPageController,
│ TimelineController,
│ CollectController,
│ HistoryController,
│ MyController,
│ ShadersController,
│ DownloadController
│ ]
│
├─ routes 定义的子模块
│ ├─ VideoModule (lib/pages/video/video_module.dart)
│ │ └─ binds: [PlayerController]
│ ├─ InfoModule (lib/pages/info/info_module.dart)
│ │ └─ binds: []
│ ├─ SettingsModule (lib/pages/settings/settings_module.dart)
│ │ └─ binds: [...]
│ └─ SearchModule (lib/pages/search/search_module.dart)
│ └─ binds: [...]
│
└─ imports 导入的子模块
├─ PopularModule (lib/pages/popular/popular_module.dart)
│ └─ binds: []
├─ TimelineModule (lib/pages/timeline/timeline_module.dart)
│ └─ binds: []
├─ CollectModule (lib/pages/collect/collect_module.dart)
│ └─ binds: []
└─ MyModule (lib/pages/my/my_module.dart)
└─ binds: []7.2 典型 Module 分析
AppModule - 根模块
// lib/app_module.dart
import 'package:flutter_modular/flutter_modular.dart';
import 'package:kazumi/pages/index_module.dart';
class AppModule extends Module {
@override
void binds(i) {
// 空,不在根模块注册具体依赖
}
@override
void routes(r) {
r.module("/", module: IndexModule()); // 根路径指向 IndexModule
}
}特点:
- ✅ 作为应用的入口
- ✅ 只负责指向 IndexModule
- ✅ 不注册具体依赖(让子模块自己管理)
IndexModule - 首页模块
// lib/pages/index_module.dart
class IndexModule extends Module {
@override
List<Module> get imports => menu.moduleList; // 导入菜单模块
@override
void binds(i) {
// Repository 层
i.addSingleton<ICollectRepository>(CollectRepository.new);
i.addSingleton<ISearchHistoryRepository>(SearchHistoryRepository.new);
i.addSingleton<ICollectCrudRepository>(CollectCrudRepository.new);
i.addSingleton<IHistoryRepository>(HistoryRepository.new);
i.addSingleton<IDownloadRepository>(DownloadRepository.new);
i.addSingleton<IDownloadManager>(DownloadManager.new);
// Controller 层
i.addSingleton(PopularController.new);
i.addSingleton(PluginsController.new);
i.addSingleton(VideoPageController.new);
i.addSingleton(TimelineController.new);
i.addSingleton(CollectController.new);
i.addSingleton(HistoryController.new);
i.addSingleton(MyController.new);
i.addSingleton(ShadersController.new);
i.addSingleton(DownloadController.new);
}
@override
void routes(r) {
// 初始化页面
r.child("/",
child: (_) => const InitPage(),
children: [
ChildRoute("/error", child: (_) => ErrorPage()),
],
transition: TransitionType.noTransition);
// 主界面
r.child("/tab",
child: (_) => const IndexPage(),
children: menu.routes,
transition: TransitionType.fadeIn,
duration: Duration(milliseconds: 70));
// 子模块
r.module("/video", module: VideoModule());
r.module("/info", module: InfoModule());
r.module("/settings", module: SettingsModule());
r.module("/search", module: SearchModule());
}
}特点:
- ✅ 注册了所有共享的 Controller
- ✅ 组织了所有主要功能模块
- ✅ 是整个应用的核心模块
PopularModule - 推荐页模块
// lib/pages/popular/popular_module.dart
import 'package:kazumi/pages/popular/popular_page.dart';
import 'package:flutter_modular/flutter_modular.dart';
class PopularModule extends Module {
@override
void routes(r) {
r.child("/", child: (_) => const PopularPage());
}
}特点:
- ✅ 非常简单,只有一个页面
- ✅ 不需要自己的 binds(使用父模块的 PopularController)
- ✅ 通过 menu.routes 导入到 IndexModule
VideoModule - 视频播放模块
// lib/pages/video/video_module.dart
import 'package:kazumi/pages/video/video_page.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:kazumi/pages/player/player_controller.dart';
class VideoModule extends Module {
@override
void routes(r) {
r.child("/", child: (_) => const VideoPage());
}
@override
void binds(i) {
i.addSingleton(PlayerController.new); // 视频专用的播放器控制器
}
}特点:
- ✅ 有自己的专属依赖(PlayerController)
- ✅ 独立的路由系统
- ✅ 不与其他模块共享 PlayerController
7.3 依赖共享实例
场景:在 PopularPage 中使用依赖
// lib/pages/popular/popular_page.dart
class _PopularPageState extends State<PopularPage> {
// 从父模块(IndexModule)获取 PopularController
final PopularController popularController = Modular.get<PopularController>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Observer(
builder: (_) {
if (popularController.isLoading) {
return LoadingWidget();
}
return ContentGrid(list: popularController.bangumiList);
},
),
);
}
}依赖来源:
IndexModule (父)
binds: [PopularController] ← 从这里获取
└─ PopularModule (子)
└─ PopularPage场景:在 VideoPage 中使用多个依赖
// lib/pages/video/video_page.dart
class _VideoPageState extends State<VideoPage> {
// 从爷爷模块(IndexModule)获取 VideoPageController
final VideoPageController videoController = Modular.get<VideoPageController>();
// 从父模块(VideoModule)获取 PlayerController
final PlayerController playerController = Modular.get<PlayerController>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
VideoPlayer(controller: playerController),
VideoInfo(controller: videoController),
],
),
);
}
}依赖来源:
IndexModule (爷爷)
binds: [VideoPageController] ← 从这里获取
└─ VideoModule (父)
│ binds: [PlayerController] ← 从这里获取
└─ VideoPage7.4 路由导航实例
基本导航:
// 在任何页面中
ElevatedButton(
onPressed: () {
// 跳转到推荐页
Modular.to.pushNamed('/tab/popular/');
// 跳转到视频页
Modular.to.pushNamed('/video/');
// 跳转到详情页(带参数)
Modular.to.pushNamed('/info/', arguments: bangumiItem);
},
child: Text('跳转'),
)替换导航(不保留历史):
// 初始化完成后跳转到主界面
Modular.to.navigate('/tab/popular/');pushNamed vs navigate 的区别:
| 方法 | 是否保留历史 | 返回键行为 | 适用场景 |
|---|---|---|---|
| pushNamed | ✓ 保留 | 可以返回 | 普通跳转 |
| navigate | ✗ 不保留 | 不能返回 | 初始化、重置状态 |
八、总结
8.1 核心要点
Module 的三大核心方法:
class MyModule extends Module {
// 1️⃣ imports - 导入其他模块
@override
List<Module> get imports => [];
// 2️⃣ binds - 注册依赖
@override
void binds(i) {
i.addSingleton(MyController.new);
}
// 3️⃣ routes - 配置路由
@override
void routes(r) {
r.child("/", child: (_) => MyPage());
}
}依赖注入的作用域规则:
- ✅ 向下共享:子模块可以访问父模块的 binds
- ❌ 向上不共享:父模块不能访问子模块的 binds
- ❌ 平级不共享:兄弟模块之间不能互相访问
r.child() vs r.module():
- r.child():注册单个页面(简单功能)
- r.module():注册整个模块(复杂功能)
8.2 最佳实践清单
✅ 应该做的:
- ✅ 一个功能一个 Module
- ✅ 在 binds 中注册所有依赖
- ✅ 使用 addSingleton 保持单例
- ✅ 清晰的命名(PopularModule、TimelineModule)
- ✅ 在父模块注册共享依赖
- ✅ 在子模块注册独占依赖
❌ 不应该做的:
- ❌ 把所有页面放在一个 Module 里
- ❌ 在 binds 里创建多个实例
- ❌ Module 之间互相依赖(循环依赖)
- ❌ 在子模块注册应该在父模块的依赖
- ❌ 忘记注册 Controller 导致页面报错
8.3 学习资源
- 官方文档:https://pub.dev/packages/flutter_modular
- GitHub:https://github.com/Flutterando/modular
- 示例项目:Kazumi(本项目)