GetX框架里容易被忽略的那些小知识(五)

748 阅读2分钟

在使用 GetxController(以及它的子类,如 GetxService)时,我们通常会接触到几个常见的生命周期方法,比如 onInit、onReady、onClose 等。实际上,GetX 在 Controller 的生命周期管理上提供了更细致的钩子函数,帮助我们更好地安排业务逻辑。

一、GetxController 主要生命周期方法

  • onInit()
  • onReady()
  • onClose()

这三大方法在 GetxController 中最常用,也是官方文档重点介绍的。除此之外,在 GetX 源码中还可以找到一些其他(比较少见)的生命周期方法,例如 onStart, onDelete, 以及对于 GetxService 的一些附加逻辑。

不过在常规的业务开发场景下,主要关注 onInit(), onReady() 和 onClose() 就足以应对绝大部分需求。

让我们依次了解它们在何时被调用、适合处理哪些逻辑,以及一些最佳实践。

1. onInit()

何时被调用?

  • 当 Controller 实例被 Get.put 或 Get.find(第一次获取)时,会立即调用该 Controller 的 onInit() 方法。
  • 这意味着 onInit() 通常是这个 Controller 生命周期的开端,一般在 Widget 构建之前就会执行(或者说,在页面渲染之前)。

适合处理的逻辑

  1. 变量初始化:初始化业务变量、Rx 变量等。
  2. 监听绑定:通过 ever, debounce, interval 等 Worker,在此处集中设置对 Rx 变量的监听。
  3. 依赖注入:如果这个控制器需要依赖某些服务(也可以用 Bindings),也可以在这里尝试使用 Get.find 获取并赋值。
  4. 轻量级网络请求:如果数据的优先级比较高,需要在页面显示之前就拿到(如基础配置、用户信息),可以在 onInit()里发请求。但如果请求会阻塞页面渲染过久,可以考虑放到 onReady()。
class HomeController extends GetxController {
  var counter = 0.obs;

  @override
  void onInit() {
    super.onInit();
    print('onInit -> Controller开始初始化');

    // 设置 Worker,比如监听 counter 的变化
    ever(counter, (value) {
      print('counter changed: $value');
    });

    // 如果需要请求一些不太大的数据,比如配置文件
    fetchInitialData();
  }

  void fetchInitialData() {
    // ...
  }
}

⚠️ 注意:如果请求会比较耗时,且必须在页面完全渲染后才做,不建议放在 onInit(),否则用户可能在空白页面等待,体验不好。这时应该考虑放到 onReady()。

2. onReady()

何时被调用?

  • 当 Widget(绑定了该 Controller 的页面)渲染完成后,就会调用 onReady()。
  • 它比 onInit() 更晚,通常意味着页面已经可见,此时做一些需要依赖“页面可见”的操作会更合适。

适合处理的逻辑

  1. 网络请求 / 动画启动:页面渲染完成后,再开始长耗时操作或加载数据,可以先让用户看到页面框架,避免一直等待空白屏。
  2. 弹窗 / SnackBar:有时需要在页面刚显示时弹出引导或提示,也可以在这里调用。
  3. 启动轮询 / 计时器:页面已经加载完,可能需要周期性地更新数据。
class ProfileController extends GetxController {
  var profile = Rxn<UserProfile>(); // 可能是null

  @override
  void onInit() {
    super.onInit();
    // 这里可以做一些很轻量的初始化
    // 例如:profile.value = localCache?.readProfile();
  }

  @override
  void onReady() {
    super.onReady();
    print('onReady -> 页面已经渲染完成');

    // 进行相对耗时或需要页面已加载才执行的网络请求
    fetchUserProfile();
  }

  void fetchUserProfile() async {
    // ... 异步请求
    // profile.value = ...
  }
}

PS:有些人也喜欢把所有网络请求都放 onInit(),这样页面一进来就开始请求。并不是不可以,但若数据体量较大,则可能延迟页面的渲染,用户体验没有 onReady() 友好。

3. onClose()

何时被调用?

  • 当 Controller 所在的页面被销毁、或者我们主动调用了 Get.delete()(若此 Controller 未被其它依赖持有)时,会触发 onClose().
  • 在默认的 SmartManagement.full 模式下,离开页面并且路由不再保留时,就会自动销毁该 Controller 并调用 onClose()。

适合处理的逻辑

  1. 释放资源:例如取消订阅、关闭流、dispose Timer / Worker、断开 WebSocket 等。
  2. 保存数据:在页面关闭时,将一些状态持久化到本地或共享存储。
  3. 取消请求:如果有网络请求或异步操作在进行,可以在这里清理或取消。
class ChatController extends GetxController {
  late final StreamSubscription messageSub;

  @override
  void onInit() {
    super.onInit();
    messageSub = ChatService.onNewMessage.listen((msg) {
      // handle new message
    });
  }

  @override
  void onClose() {
    // 页面销毁时,取消监听,避免内存泄漏
    messageSub.cancel();
    super.onClose();
  }
}

⚠️ 注意:若你在 onInit() 或其他地方启动了定时器 (Timer)、stream 监听、socket 连接等,一定要在 onClose() 中主动释放,否则会出现内存泄漏或意外的后台执行。

二、其他较少见的生命周期/方法

onStart()

在部分早期版本或某些扩展中,可能会看到 onStart(),但在官方的最新控制器中并没有默认实现。onStart() 更类似于一个“刚开始启动 Controller”的时刻,一般你可以把它当作是比 onInit() 更早的一层(但通常我们不使用它)。

目前常见的场景会使用 onInit() 即可满足需求。若你要在生成 Controller 之后,还没正式初始化前执行某些事情,可以考虑直接在构造函数或 onInit() 里处理。

onDelete()

当使用 Get.delete(force: true) 时,可能会触发到 onDelete。它在 GetX 的内部封装里用来清理一些辅助资源。大多数时候你只需要在 onClose() 中进行资源释放即可。

三、在不同生命周期做什么业务最合适

下面做一个小结,可以作为一个通用的“对照表”:

生命周期方法何时调用 & 适用场景建议做的事不建议做的事
onInit()1. 当 Controller 被注入(或 Get.find 首次被调用)时触发
2. 页面构建前执行,属于 Controller 的初始化阶段 
1. 绑定 Worker(ever, debounce, interval 等)
2. 依赖注入或 Get.find()
3. 轻量级数据准备(如同步读取、少量网络请求)
4. 做一些初步逻辑(设置默认值、读取缓存等)
1. 不要进行非常耗时的操作(会阻塞页面首帧渲染)
2. 不要在此处弹出对话框或执行大量 UI 操作(此时页面尚未渲染)
onReady()1. 当页面(Widget)已完成渲染后触发
2. 比 onInit() 更晚,可确保用户已能看到页面主体
1. 加载大数据或较长耗时异步请求(确保页面不白屏)
2. 启动动画、弹出对话框或引导
3. 获取用户真正所需的核心数据(如详情、列表)
4. 启动定时器、轮询等(页面已可见,用户体验更好)
1. 不适合过早执行的操作(应放在 onInit() 中)
2. 如果某操作必须在页面显示前完成,放在 onInit() 但要注意影响首屏渲染,需平衡用户体验
onClose()1. 当 Controller 被销毁或页面结束路由栈时触发
2. 在默认 SmartManagement.full 模式下,离开页面会销毁对应 Controller     
1. 释放资源(如 StreamTimer、WebSocket、Controller)
2. 保存页面状态或数据(写入本地或全局服务)
3. 取消未完成的网络请求(避免后台无用操作)
1. 避免过多耗时操作(用户已离开页面)
2. 不建议执行过于复杂的逻辑,可考虑改为后台异步执行或交给全局服务处理

四、完整实例

以下示例演示一个带有 Worker、网络请求、资源释放的 Controller,并在不同生命周期执行不同逻辑。

class ProductController extends GetxController {
  // 产品列表
  final products = <Product>[].obs;

  // 搜索关键字
  final searchQuery = ''.obs;

  // 定时轮询的 timer
  Timer? pollingTimer;

  @override
  void onInit() {
    super.onInit();
    print('ProductController -> onInit');

    // 1. Worker - 监听搜索关键字变化,触发搜索
    debounce(searchQuery, (_) {
      fetchProducts(searchQuery.value);
    }, time: Duration(milliseconds: 500));

    // 2. 可以进行一些轻量级初始化操作,或者尝试用局部缓存
    final cachedData = LocalCache.get('product_list');
    if (cachedData != null) {
      products.assignAll(cachedData);
    }
  }

  @override
  void onReady() {
    super.onReady();
    print('ProductController -> onReady');

    // 页面已渲染,可以进行真正的网络请求
    fetchProducts();

    // 每 30 秒轮询一次数据
    pollingTimer = Timer.periodic(Duration(seconds: 30), (timer) {
      fetchProducts();
    });
  }

  @override
  void onClose() {
    print('ProductController -> onClose');
    // 1. 释放 Timer
    pollingTimer?.cancel();
    // 2. 可能需要保存最后的产品列表
    LocalCache.save('product_list', products.toList());
    // 3. 执行父类 onClose
    super.onClose();
  }

  void fetchProducts([String? query]) async {
    // ... 调接口获取产品列表
    // products.value = ...
    print('Fetching products with query: $query');
  }
}
  • 在 onInit() 中,设置了 searchQuery 的去抖监听,并尝试加载本地缓存数据。
  • 在 onReady() 中,页面渲染完成后进行网络请求、启动定时轮询。
  • 在 onClose() 中,销毁 Timer 并保存数据到本地。

五、总结

onInit():

  • 适合做初始绑定、轻量初始化、Worker 监听
  • 时间点:Controller 被创建、注入后立即执行

onReady():

  • 适合在页面渲染后再做更多数据加载、动画启动、提示弹窗
  • 时间点:Widget build 完成后

onClose():

  • 适合资源释放、销毁定时器或保存数据
  • 时间点:页面或 Controller 被销毁前

合理利用 GetX Controller 的这些生命周期方法,可以让你的业务逻辑分层更加清晰、页面渲染和数据请求的时机更可控,也更便于管理资源和避免内存泄漏。