Flutter使用GraphQL订阅时视图resize引发后端请求重复发送的问题
问题根因补充
你遇到的重复订阅本质是build方法每次执行时,都会生成全新的SubscriptionOptions实例,graphql_flutter的Subscription组件检测到配置变更就会销毁旧订阅、创建新订阅,和窗口resize触发Widget重建的逻辑叠加就会出现大量重复请求。
方案1:最小改动修复(适合快速上线)
- 将当前的
StatelessWidget改为StatefulWidget,把SubscriptionOptions的初始化逻辑放到initState里完成,作为State的成员变量存储,不要在build方法中生成配置 - 如果该Widget是在PageView、TabView、滚动列表等可回收容器中使用,给State混入
AutomaticKeepAliveClientMixin,重写wantKeepAlive返回true,避免Widget被回收时销毁订阅
示例代码:
class PersonWidget extends StatefulWidget { const PersonWidget({required this.personId, super.key}); final int personId; @override State<PersonWidget> createState() => _PersonWidgetState(); } class _PersonWidgetState extends State<PersonWidget> with AutomaticKeepAliveClientMixin { late final SubscriptionOptions _subscriptionOptions; // 订阅文档定义为全局常量,避免重复解析 static const _personSubscription = gql(r''' subscription ($personId: Int!){ person(where: {id: {_eq: $personId}}) { id first_name last_name email } } '''); @override void initState() { super.initState(); // 初始化只执行一次,不会随build变更 _subscriptionOptions = SubscriptionOptions( document: _personSubscription, variables: <String, dynamic>{ 'personId': widget.personId, }, ); } @override Widget build(BuildContext context) { super.build(context); // 混入KeepAlive必须调用 return Subscription( options: _subscriptionOptions, builder: (result) { if (result.isLoading && result.data == null) { return const Center( child: CircularProgressIndicator(), ); } final person = result.data!['person'][0]; return WidgetStructureGoesHere( ... ); }, ); } @override bool get wantKeepAlive => true; }
方案2:业务层抽离订阅(适合长期维护的项目,推荐)
使用状态管理工具(Provider、Riverpod、Bloc均可)将订阅逻辑和视图完全解绑,订阅的生命周期由业务层控制,和Widget的build行为完全无关:
- 定义独立的业务类负责订阅的创建、数据解析、销毁,生命周期和页面/全局状态绑定
- 视图层只监听业务层抛出的数据流更新UI,不会触发订阅的重建
示例(基于Riverpod 2.0):
// 业务层:提供订阅流 final personStreamProvider = StreamProvider.family<Person, int>((ref, personId) { final client = ref.watch(graphQLClientProvider); // 订阅生命周期由Riverpod管理,默认缓存一段时间,同一个personId复用同一个流 return client.subscribe(SubscriptionOptions( document: _personSubscription, variables: {'personId': personId}, )).map((result) => Person.fromJson(result.data!['person'][0])); }); // 视图层:只做渲染 class PersonWidget extends ConsumerWidget { const PersonWidget({required this.personId, super.key}); final int personId; @override Widget build(BuildContext context, WidgetRef ref) { final personAsync = ref.watch(personStreamProvider(personId)); return personAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, st) => Center(child: Text('加载失败:$e')), data: (person) => WidgetStructureGoesHere(person: person), ); } }
方案3:全局订阅缓存(适合多页面复用同类型订阅的场景)
封装全局订阅管理单例,用订阅参数作为key缓存已创建的订阅流,新增订阅时优先复用已有流,销毁时按引用计数判断是否真正关闭后端连接,彻底避免重复请求。
内容的提问来源于stack exchange,提问作者Alex Recarey
相关产品推荐
相关产品推荐

