Flutter GetX 如何在所有Widget构建完成后调用Controller方法
Flutter GetX 首屏分阶段渲染:列表渲染完成后请求子数据实现方案
问题背景
在Flutter开发中使用GetX状态管理框架,现有代码逻辑如下:
控制器层代码
RxList<Outlets?> listPharmacyOutlets = <Outlets>[].obs; RxList listOutletCoordinates = [].obs; @override void onInit() async{ super.onInit(); await getPharmacyOutlets(); } Future getPharmacyOutlets() async{ Map params = { //部分请求参数 } var res = await CommonApiProvider.getPharmacyOutlets(params) var outlets = res.data; int idx = 0; listPharmacyOutlets.clear(); for(final outlet in outlets){ listPharmacyOutlets.add(Outlets( latitude: outlet.latitude, longitude: outlet.longitude, pharmacyId: outlet.id, outletName: outlet.name, address: null )); //填充坐标列表,后续用于批量获取所有门店地址 listOutletCoordinates.add({ "index": idx, "latitude": outlet.latitude, "longitude": outlet.longitude, }); idx++; } //此处不能调用getOutletAddresses(),否则会阻塞等待该方法执行完成才会渲染视图 } Future getOutletAddresses() async { await Future.forEach(listOutletCoordinates, (item) async { var result = await CommonApiProvider.getOutletAddresses(item); //根据返回结果,借助item中的index属性更新listPharmacyOutlets中对应项的address字段 }); }
视图层代码
SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), child: Column( children: List.generate(controller.listPharmacyOutlets.length, (index) { var item = controller.listPharmacyOutlets[index]; return Container( child: ....... ); }) ), );
需求与现存问题
实现目标:
- 优先调用
getPharmacyOutlets()获取门店基础列表数据,首屏优先渲染门店基础信息 - 等门店列表完成视图渲染后,再调用
getOutletAddresses()通过经纬度请求谷歌地理编码服务拿到地址 - 地址返回后无感更新列表对应项的地址字段
- 拆分请求的核心目的是优化首屏加载速度,避免用户等待全量接口返回才能看到内容
当前问题:
无法确定getOutletAddresses()的正确调用时机,无法保证列表渲染完成后再发起地址请求。曾考虑使用WidgetsBinding的帧回调实现:
WidgetsBinding.instance.addPostFrameCallback((_) { // 此处调用谷歌地理编码服务获取地址的方法 });
但项目中所有GetX页面均为Stateless无状态组件,不清楚该回调的注册位置。
解决方案
方案1:直接在控制器内注册帧回调(改动最小,无需修改视图层)
WidgetsBinding是Flutter全局的帧调度单例,不依赖StatefulWidget的上下文即可注册回调,完全可以直接在GetxController中使用,适配现有代码的成本最低。
修改getPharmacyOutlets方法,在门店列表数据填充完成后注册回调即可:
Future getPharmacyOutlets() async{ Map params = { //部分请求参数 } var res = await CommonApiProvider.getPharmacyOutlets(params) var outlets = res.data; int idx = 0; listPharmacyOutlets.clear(); for(final outlet in outlets){ listPharmacyOutlets.add(Outlets( latitude: outlet.latitude, longitude: outlet.longitude, pharmacyId: outlet.id, outletName: outlet.name, address: null )); listOutletCoordinates.add({ "index": idx, "latitude": outlet.latitude, "longitude": outlet.longitude, }); idx++; } // 列表数据写入完成后,注册帧渲染完成回调 WidgetsBinding.instance.addPostFrameCallback((_) async { // 页面已销毁时不再执行请求,避免内存泄漏和报错 if (!isClosed) { await getOutletAddresses(); } }); }
注意:
addPostFrameCallback注册的回调只会执行一次,执行后自动销毁,不会重复触发。这里注册回调的时机是列表数据全部写入listPharmacyOutlets之后,此时框架会触发列表首次重绘,回调会在这一帧完全绘制完成后才执行,不会阻塞首屏渲染。
必做适配:解决RxList内部属性修改不触发UI更新的问题
现有更新地址的逻辑默认不会触发UI刷新:RxList只会监听列表本身的长度变化、元素整体替换事件,如果直接修改列表内某个Outlets实例的address属性,框架无法感知数据变化,UI不会自动更新。
两种改法二选一即可:
- 更新地址时整体替换列表对应索引的元素(适配你现有数据结构成本最低)
Future getOutletAddresses() async { await Future.forEach(listOutletCoordinates, (item) async { var result = await CommonApiProvider.getOutletAddresses(item); int index = item['index']; Outlets oldOutlet = listPharmacyOutlets[index]!; // 整体替换对应位置元素,触发RxList更新事件 listPharmacyOutlets[index] = Outlets( latitude: oldOutlet.latitude, longitude: oldOutlet.longitude, pharmacyId: oldOutlet.pharmacyId, outletName: oldOutlet.outletName, address: result['address'], ); }); }
- 给
Outlets类需要更新的属性添加.obs响应式标记,更新时直接赋值即可,适合字段较多的场景。
可选优化:提升地址加载效率
当前使用Future.forEach是串行请求地址,门店数量较多时加载速度慢,可以根据谷歌地理编码API的QPS限制做并发控制,比如单次并发3-5个请求,能大幅缩短全量地址加载的耗时。
方案2:StatelessWidget中注册回调(适合不想在控制器中混入UI逻辑的场景)
StatelessWidget的build方法会在页面构建时执行,只需要加一个标记位保证回调只注册一次即可,不需要改造为StatefulWidget:
// 视图层build方法中 final controller = Get.find<YourController>(); bool hasRegisteredCallback = false; return Scaffold( body: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Column( children: List.generate(controller.listPharmacyOutlets.length, (index) { // 检测到列表有数据、即将渲染时,注册一次帧回调 if (!hasRegisteredCallback && controller.listPharmacyOutlets.isNotEmpty) { hasRegisteredCallback = true; WidgetsBinding.instance.addPostFrameCallback((_) async { await controller.getOutletAddresses(); }); } var item = controller.listPharmacyOutlets[index]; return Container( child: ....... ); }) ), ), );
该方案逻辑和方案1完全一致,只是把回调注册位置放在了视图层。
内容的提问来源于stack exchange,提问作者WatsMyName
相关产品推荐
相关产品推荐

