Drawer重复调用FirstPage时FutureProvider不触发加载状态的问题
点击Drawer中的选项打开FirstPage页面,首次打开时,由于页面不在Widget Tree中,FutureProvider会进入Loading模式,获取数据后显示页面内容。但当FirstPage已显示时,再次从Drawer调用该页面,FutureProvider不会重新获取数据——推测是FirstPage仍在Widget Tree中未被自动释放。尝试在Drawer跳转时执行ref.invalidate(helloProvider),虽能重新获取数据,但不会触发Loading状态,期望每次跳转都能完整走一遍AsyncValue的loading→data流程。
最小可复现代码
main.dart
import 'package:drawer_refresh_sample/drawer.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; void main() { runApp(const ProviderScope(child: MyApp())); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key, required this.title}); final String title; @override Widget build(BuildContext context) { return Scaffold( drawer: const SampleDrawer(), appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(title), ), body: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Good Morning', ), ], ), ), ); } }
drawer.dart
import 'package:drawer_refresh_sample/first_page.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class SampleDrawer extends ConsumerWidget { const SampleDrawer({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return Drawer( child: ListView( children: <Widget>[ ListTile( title: const Text("Page 1"), trailing: const Icon(Icons.arrow_forward), onTap: () { Navigator.of(context).pop(); ref.invalidate(helloProvider); Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const FirstPage())); }, ), ], ), ); } }
first_page.dart
import 'package:drawer_refresh_sample/sample_provider.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'drawer.dart'; class FirstPage extends ConsumerWidget { const FirstPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final asyncHello = ref.watch(helloProvider); return asyncHello.when( data: (data) => Scaffold( appBar: AppBar( title: const Text("First Page"), ), drawer: const SampleDrawer(), body: SafeArea(child: Center(child: Text(data)))), error: (error, stackTrace) => Text(stackTrace.toString()), loading: () => const Center(child: CircularProgressIndicator.adaptive())); } }
sample_provider.dart
import "dart:math"; import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'sample_provider.g.dart'; @riverpod Future<String> hello(HelloRef ref) { return Future<String>.delayed(const Duration(seconds: 2), () { return "Hello${Random().nextInt(10)}"; }); }
通用解决方案
方案1:用ref.refresh替代ref.invalidate
ref.refresh会强制重新执行Provider的异步逻辑,并且主动触发Loading状态,和首次加载行为一致。修改Drawer的onTap逻辑:
onTap: () { Navigator.of(context).pop(); // 强制触发重新加载并进入loading状态 ref.refresh(helloProvider); Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const FirstPage())); },
原理:refresh会主动运行Provider创建函数,让所有监听组件立即收到loading状态;而invalidate仅标记Provider需重建,若有活跃监听会在后台更新,不会触发loading。
方案2:移除旧页面后再跳转
通过清空导航栈中旧的FirstPage实例,确保每次打开的都是全新页面,自然触发首次加载的Loading流程:
onTap: () { Navigator.of(context).pop(); // 关闭Drawer // 回到根页面,确保旧FirstPage被移除出Widget Tree Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).push(MaterialPageRoute(builder: (_) => const FirstPage())); },
注意:此方案会改变导航栈结构,需根据业务场景选择。
方案3:用唯一Key强制重建页面
通过给FirstPage传入不同的Key,让Flutter认为是新Widget,从而销毁旧实例、创建新实例,重新触发Provider监听和Loading流程:
首先修改FirstPage:
class FirstPage extends ConsumerWidget { const FirstPage({super.key}); // 新增带Key的构造方法 FirstPage.withUniqueKey() : super(key: UniqueKey()); @override Widget build(BuildContext context, WidgetRef ref) { // 原build逻辑不变 } }
再修改Drawer的onTap:
onTap: () { Navigator.of(context).pop(); // 每次传入新的UniqueKey,强制重建页面 Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (_) => FirstPage.withUniqueKey(), )); },
原理:Flutter通过Key识别Widget身份,不同Key会触发Widget实例的重建,新实例重新watch Provider时会触发Loading。
方案4:自定义刷新触发器
添加一个StateProvider作为刷新信号,让helloProvider依赖该信号,每次触发信号时重新执行异步逻辑并显示Loading:
// 新增刷新触发器 final helloRefreshTrigger = StateProvider<int>((ref) => 0); @riverpod Future<String> hello(HelloRef ref) { // 监听触发器,状态变化时重新执行 ref.watch(helloRefreshTrigger); return Future<String>.delayed(const Duration(seconds: 2), () { return "Hello${Random().nextInt(10)}"; }); }
修改Drawer的onTap:
onTap: () { Navigator.of(context).pop(); // 递增触发器状态,触发刷新 ref.read(helloRefreshTrigger.notifier).state++; Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const FirstPage())); },
优势:灵活可控,适合需要多处触发页面刷新的场景。
内容的提问来源于stack exchange,提问作者goza

