Flutter GetX 三件套开发规范(Skill)

概述

总结一份目前公司在用的flutter结合gex开发的AI skill吧。

一、核心原则

  1. 视图与逻辑严格分离:View 只负责渲染和触发事件,不写业务逻辑,不实例化 Controller。
  2. 依赖注入走 Binding :Controller 的创建交给路由 Binding,View 通过 GetView<T> 自动获取。
  3. 路由统一命名 :所有页面跳转走 Get.toNamed,保证 Binding 自动触发。
  4. State 独立:数据字段抽离到 State 类,Controller 只保留行为方法。
  5. 刷新用 GetBuilder,不用 .obs :所有状态字段为普通 Dart 类型,通过 update() 手动通知刷新。
  6. 文件命名后缀固定 :_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() 控制,性能和行为都可预期。

相关推荐
张小姐的猫1 小时前
【AI大模型接入SDK】 —— 前端页面 & 项目总结与拓展
前端·数据结构·数据库·c++·人工智能·chatgpt
web打印社区1 小时前
远程打印:WebSocket 与 HTTP 轮询怎么选
前端·vue.js·websocket·网络协议·http·electron·pdf
吴声子夜歌2 小时前
HTML——庞杂的表单控件元素(一)
前端·html
天天喝旺仔2 小时前
浏览器渲染原理:从解析 HTML/CSS、构建渲染树到重排重绘与首屏优化
前端·javascript·css·性能优化·html
明月_清风2 小时前
前端已死?别急,这可能只是所有行业的开始
前端·ai编程
明月_清风2 小时前
干了 6 年前端,我是怎么一步步转型到 AI 的?
前端·后端·ai编程
乘风gg2 小时前
花了 100 亿 Token 后,我发现 Code is cheap 是最大的谎言
前端·ai编程·claude
IMPYLH2 小时前
HTML 的 <textarea> 元素
前端·html
吴声子夜歌2 小时前
HTML——庞杂的表单控件元素(二)
前端·html