概述
总结一份目前公司在用的flutter结合gex开发的AI skill吧。
一、核心原则
- 视图与逻辑严格分离:View 只负责渲染和触发事件,不写业务逻辑,不实例化 Controller。
- 依赖注入走 Binding :Controller 的创建交给路由 Binding,View 通过
GetView<T>自动获取。 - 路由统一命名 :所有页面跳转走
Get.toNamed,保证 Binding 自动触发。 - State 独立:数据字段抽离到 State 类,Controller 只保留行为方法。
- 刷新用 GetBuilder,不用
.obs:所有状态字段为普通 Dart 类型,通过update()手动通知刷新。 - 文件命名后缀固定 :
_view.dart/_controller.dart/_binding.dart/_state.dart。
为什么不用
.obs:.obs会为每个字段创建Rx对象和 Stream,字段多时内存和性能开销明显;响应式链条隐式,容易漏掉刷新或过度刷新;调试时堆栈不直观。GetBuilder + update()是显式刷新,性能更可控,代码也更接近普通 Dart 类。
二、目录结构
按功能模块(feature-first) 组织,每个模块内部自带三件套。
bash
lib/
├── main.dart
├── app/
│ ├── routes/
│ │ ├── app_pages.dart # 所有 GetPage 注册
│ │ ├── app_routes.dart # 路由常量
│ │ └── initial_binding.dart # 全局依赖注入
│ └── theme/
│ └── app_theme.dart
├── core/
│ ├── network/
│ │ └── api_client.dart
│ └── utils/
│ └── logger.dart
└── modules/
├── counter/
│ ├── counter_view.dart
│ ├── counter_controller.dart
│ ├── counter_binding.dart
│ └── counter_state.dart
├── user/
│ ├── user_list_view.dart
│ ├── user_list_controller.dart
│ ├── user_list_binding.dart
│ └── user_list_state.dart
└── ...
三、整体流程图
3.1 页面加载与依赖注入流程
scss
┌─────────────────────────────────────────────────────────────┐
│ 应用启动 │
│ main.dart → GetMaterialApp(initialBinding: InitialBinding) │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ InitialBinding.dependencies() │
│ └─ Get.put<ApiClient>(ApiClient(), permanent: true) │
│ (全局服务注册,永不回收) │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ initialRoute: '/counter' │
│ 触发对应 GetPage │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CounterBinding.dependencies() │
│ └─ Get.lazyPut<CounterController>(() => CounterController())│
│ (仅注册工厂,尚未实例化) │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CounterView 构建(继承 GetView<CounterController>) │
│ └─ 访问 controller 属性 │
│ └─ Get.find<CounterController>() │
│ └─ 首次 find → 执行 lazyPut 的工厂 → 创建实例 │
│ └─ 调用 onInit() → 发起初始请求 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ GetBuilder<CounterController> 包裹 UI │
│ └─ builder 回调中读取 state 字段渲染 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 用户交互(点击按钮) │
│ └─ 调用 controller.xxx() │
│ └─ 修改 state 字段 │
│ └─ 调用 update() → 通知 GetBuilder 重建 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 页面销毁(Get.back / offNamed) │
│ └─ Controller.onClose() 被调用 │
│ └─ 释放资源,Controller 从内存移除 │
└─────────────────────────────────────────────────────────────┘
3.2 数据流向图(单向数据流)
scss
┌──────────────┐ 调用方法 ┌──────────────────┐ 修改字段 ┌───────────────┐
│ View │ ───────────▶ │ Controller │ ──────────▶ │ State │
│ │ │ │ │ │
│ GetBuilder │ │ increment() │ │ int count │
│ 渲染 UI │ │ fetchUsers() │ │ bool loading │
│ │ │ │ │ String error │
└──────▲───────┘ └────────┬─────────┘ └───────┬───────┘
│ │ │
│ │ update() │
│ ▼ │
│ ┌──────────────────┐ │
│ │ GetBuilder 监听 │ ◀───────────────────┘
│ │ 触发重建 │ 读取字段值
└──────────────────────┴──────────────────┘
重新渲染 UI
关键约束:
- View 不能直接改 State 字段。
- State 不能调用 Controller 方法。
- Controller 是唯一能改 State 的角色。
- 每次改完 State,必须调用
update()才能刷新 UI。
3.3 模块文件职责图
scss
┌──────────────────────────────────────────────────────────────┐
│ counter 模块 │
├──────────────────────────────────────────────────────────────┤
│ │
│ counter_state.dart counter_controller.dart │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ 纯数据类 │◀────────│ 业务逻辑 │ │
│ │ 普通 Dart 字段 │ 持有 │ 继承 GetxController │ │
│ │ 无方法 │ │ onInit / onClose │ │
│ │ 无依赖 │ │ 修改 state + update()│ │
│ └─────────────────┘ └──────────┬───────────┘ │
│ │ 被注入 │
│ ▼ │
│ counter_binding.dart counter_view.dart │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ 继承 Bindings │ │ 继承 GetView<T> │ │
│ │ Get.lazyPut │ │ 只用 controller 属性 │ │
│ │ 只做注册,无逻辑 │ │ GetBuilder 包裹渲染 │ │
│ └─────────────────┘ └──────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
四、三件套代码模板
以 counter 模块为例。
1. State ------ 只放数据(普通字段)
dart
// modules/counter/counter_state.dart
class CounterState {
/// 计数
int count = 0;
/// 最近一次操作描述
String lastAction = 'None';
/// 加载状态
bool isLoading = false;
/// 重置方法(仅用于批量恢复初始值)
void reset() {
count = 0;
lastAction = 'None';
isLoading = false;
}
}
说明:
- 全部为普通 Dart 字段,无
.obs、无Rx。 - 允许提供
reset()这类纯数据操作,但不允许调用 Controller 或发起 IO。 - 字段默认值直接初始化,避免 nullable 泛滥。
2. Controller ------ 只放逻辑
dart
// modules/counter/counter_controller.dart
import 'package:get/get.dart';
import 'counter_state.dart';
class CounterController extends GetxController {
final state = CounterState();
@override
void onInit() {
super.onInit();
_loadInitialData();
}
@override
void onClose() {
// 释放 Timer、StreamSubscription、TextEditingController 等
super.onClose();
}
void increment() {
state.count++;
state.lastAction = 'Incremented';
update(); // 通知 GetBuilder 刷新
}
void decrement() {
if (state.count == 0) return;
state.count--;
state.lastAction = 'Decremented';
update();
}
Future<void> reset() async {
state.isLoading = true;
update();
await Future.delayed(const Duration(milliseconds: 300));
state.reset();
update();
}
Future<void> _loadInitialData() async {
await Future.delayed(const Duration(milliseconds: 100));
state.lastAction = 'Ready';
update();
}
}
说明:
- 所有改字段的地方,紧跟着调用
update()。 - 异步方法里,加载态和最终态各调用一次
update()。 - 不 import
material.dart,不引用任何 Widget。
3. Binding ------ 只做依赖注入
dart
// modules/counter/counter_binding.dart
import 'package:get/get.dart';
import 'counter_controller.dart';
class CounterBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<CounterController>(() => CounterController());
}
}
带路由参数的情况:
dart
javascript
Get.lazyPut<DetailController>(
() => DetailController(Get.arguments['id'] as String),
);
4. View ------ 只做展示
dart
// modules/counter/counter_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'counter_controller.dart';
class CounterView extends GetView<CounterController> {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: GetBuilder<CounterController>(
builder: (logic) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: ${logic.state.count}',
style: const TextStyle(fontSize: 48),
),
const SizedBox(height: 8),
Text('Last: ${logic.state.lastAction}'),
const SizedBox(height: 24),
if (logic.state.isLoading)
const CircularProgressIndicator(),
],
),
);
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'inc',
onPressed: controller.increment,
child: const Icon(Icons.add),
),
const SizedBox(height: 12),
FloatingActionButton(
heroTag: 'dec',
onPressed: controller.decrement,
child: const Icon(Icons.remove),
),
const SizedBox(height: 12),
FloatingActionButton(
heroTag: 'reset',
onPressed: controller.reset,
child: const Icon(Icons.refresh),
),
],
),
);
}
}
说明:
- UI 中变化的部分用
GetBuilder<CounterController>包裹。 builder参数可命名(如logic),也可以用外层的controller,两者等价。- 不需要刷新的部分(AppBar 标题、静态文本)放在
GetBuilder外,减少重建范围。 - 事件直接绑
controller.xxx,View 中无业务判断。
GetBuilder 的两种写法:
dart
// 写法 A:只用 GetBuilder 局部包裹(推荐,可缩小重建范围)
GetBuilder<CounterController>(
builder: (logic) => Text('${logic.state.count}'),
)
// 写法 B:id 过滤,一个 Controller 有多处独立刷新时用
GetBuilder<CounterController>(
id: 'counter_text',
builder: (logic) => Text('${logic.state.count}'),
)
// 对应在 Controller 里:update(['counter_text']);
五、路由注册
dart
// app/routes/app_routes.dart
abstract class AppRoutes {
static const counter = '/counter';
static const userList = '/user-list';
}
dart
// app/routes/app_pages.dart
import 'package:get/get.dart';
import '../modules/counter/counter_binding.dart';
import '../modules/counter/counter_view.dart';
import '../modules/user/user_list_binding.dart';
import '../modules/user/user_list_view.dart';
import 'app_routes.dart';
class AppPages {
static const initial = AppRoutes.counter;
static final routes = <GetPage>[
GetPage(
name: AppRoutes.counter,
page: () => const CounterView(),
binding: CounterBinding(),
),
GetPage(
name: AppRoutes.userList,
page: () => const UserListView(),
binding: UserListBinding(),
),
];
}
dart
// app/routes/initial_binding.dart
import 'package:get/get.dart';
import '../../core/network/api_client.dart';
class InitialBinding extends Bindings {
@override
void dependencies() {
Get.put<ApiClient>(ApiClient(), permanent: true);
}
}
dart
// main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'app/routes/app_pages.dart';
import 'app/routes/initial_binding.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Skill',
initialBinding: InitialBinding(),
initialRoute: AppPages.initial,
getPages: AppPages.routes,
debugShowCheckedModeBanner: false,
);
}
}
跳转统一写法:
dart
Get.toNamed(AppRoutes.userList);
Get.toNamed(AppRoutes.userList, arguments: {'id': '123'});
Get.offNamed(AppRoutes.counter);
Get.offAllNamed(AppRoutes.counter);
六、带异步数据的完整示例(user_list)
State
dart
// modules/user/user_list_state.dart
import 'user_model.dart';
class UserListState {
List<UserModel> users = [];
bool isLoading = false;
String? errorMessage;
void reset() {
users = [];
isLoading = false;
errorMessage = null;
}
}
Controller
dart
// modules/user/user_list_controller.dart
import 'package:get/get.dart';
import 'user_list_state.dart';
import 'user_model.dart';
import '../../core/network/api_client.dart';
class UserListController extends GetxController {
final state = UserListState();
final ApiClient _api = Get.find<ApiClient>();
@override
void onInit() {
super.onInit();
fetchUsers();
}
Future<void> fetchUsers() async {
state.isLoading = true;
state.errorMessage = null;
update();
try {
final list = await _api.getUsers();
state.users = list;
} catch (e) {
state.errorMessage = e.toString();
} finally {
state.isLoading = false;
update();
}
}
void onUserTap(UserModel user) {
Get.toNamed('/user-detail', arguments: {'id': user.id});
}
}
Binding
dart
// modules/user/user_list_binding.dart
import 'package:get/get.dart';
import 'user_list_controller.dart';
class UserListBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<UserListController>(() => UserListController());
}
}
View
dart
// modules/user/user_list_view.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'user_list_controller.dart';
class UserListView extends GetView<UserListController> {
const UserListView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Users')),
body: GetBuilder<UserListController>(
builder: (logic) {
if (logic.state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
final error = logic.state.errorMessage;
if (error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败: $error'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: logic.fetchUsers,
child: const Text('重试'),
),
],
),
);
}
final users = logic.state.users;
if (users.isEmpty) {
return const Center(child: Text('暂无数据'));
}
return ListView.separated(
itemCount: users.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
onTap: () => logic.onUserTap(user),
);
},
);
},
),
);
}
}
七、检查清单(Code Review 用)
-
代码中没有任何
.obs或Obx。 -
View 文件里没有
Get.put/Get.lazyPut/Get.find。 -
View 文件里没有业务逻辑(if/循环只用于 UI 分支)。
-
Controller 文件里没有 import
material.dart/widgets.dart。 -
Controller 所有状态字段都在
State类里,无裸字段。 -
每次修改
state字段后,都有对应的update()调用。 -
初始化逻辑写在
onInit,清理逻辑写在onClose。 -
每个页面路由都在
AppPages.routes注册,并挂上对应 Binding。 -
页面跳转统一用
Get.toNamed,无Get.to(() => XxxView())。 -
全局服务在
InitialBinding里注册,permanent: true。 -
文件命名后缀统一:
_view/_controller/_binding/_state。
八、常见反模式(禁止)
| 反模式 | 问题 | 正确做法 |
|---|---|---|
使用 .obs / Obx |
性能开销大,刷新隐式 | GetBuilder + update() |
改字段后忘记 update() |
UI 不刷新 | 每次改完紧跟 update() |
View 里 Get.put(Ctrl()) |
视图耦合实例化 | 交给 Binding |
Controller 里 Text('...') |
逻辑层依赖 UI | 状态暴露给 View 渲染 |
| 一个 Controller 管多页 | 职责不清,状态污染 | 一页一 Controller |
Binding 里 permanent: true |
页面级实例变全局,泄漏 | 全局服务放 InitialBinding |
Get.to(() => XxxView()) |
Binding 不触发,报错 | 统一 Get.toNamed |
| State 里调用 IO / Controller | 数据层越界 | 方法全放 Controller |
九、新增页面操作流程
text
1. 新建 modules/xxx/ 目录
│
▼
2. 写 xxx_state.dart (普通字段 + reset())
│
▼
3. 写 xxx_controller.dart (继承 GetxController,持有 state,改字段调 update())
│
▼
4. 写 xxx_binding.dart (Get.lazyPut<XxxController>)
│
▼
5. 写 xxx_view.dart (继承 GetView<XxxController>,GetBuilder 包裹 UI)
│
▼
6. 在 app_routes.dart 加路由常量
│
▼
7. 在 app_pages.dart 注册 GetPage + Binding
│
▼
8. 用 Get.toNamed 跳转即可
按这套规范落地,View 里永远看不到实例化代码,Controller 里永远看不到 Widget,State 里永远没有业务逻辑,刷新全部通过显式 update() 控制,性能和行为都可预期。