You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Riverpod中FutureProvider刷新时不触发loading状态的问题

问题

在Flutter开发中使用Riverpod的FutureProvider实现API调用更新UI,首次加载时能正常显示CircularProgressIndicator,加载完成后主页展示数据。但后续从其他页面调用ref.refresh(calendarResponseProvider)刷新该Provider时,主页UI能成功更新数据,却不会再次显示加载指示器,而是等待请求完成后直接更新。请问为何第二次刷新时不会进入loading状态?


相关代码

Provider定义

final getCalendarApiProvider = Provider<GetCalendarApi>(
  (ref) => GetCalendarApi(),
);

final calendarResponseProvider = FutureProvider<List<CalendarResponse>>((ref) async {
  return ref.read(getCalendarApiProvider).getCalendar(ref.read(startDateProvider), ref.read(endDateProvider));
});

主页面代码

class MainMenuScreen extends ConsumerStatefulWidget {
  const MainMenuScreen({
    Key? key,
  }) : super(key: key);

  @override
  ConsumerState createState() => _MainMenuScreenState();
}

class _MainMenuScreenState extends ConsumerState<MainMenuScreen> {
  String _selectedDate = '';


  @override
  Widget build(BuildContext context) {

    print('Main Build Called');

    AsyncValue<List<CalendarResponse>> calendarResponse = ref.watch(calendarResponseProvider);

    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: true,
        title: const Text(
          "My Sessions",
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.normal),
        ),
        actions: [
          IconButton(
            icon: Icon(
              Icons.settings,
              color: Colors.white,
            ),
            onPressed: () {
              ref.refresh(calendarResponseProvider);
            },
          )
        ],
        centerTitle: true,
      ),
      drawer: const DrawerWidget(),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          StartEndDateWidget(),
          calendarResponse.when(data: (data) {
            print('DATA length: ${data.length}');
            return Expanded(
              child: ListView.builder(
                  scrollDirection: Axis.vertical,
                  shrinkWrap: true,
                  itemCount: data.length,
                  itemBuilder: ((context, index) {
                    return Text(data[index].studentNames ?? '-');
                  })),
            );
          }, error: ((error, stackTrace) {
            return Text('Error: ${error.toString()}');
          }), loading: (() {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }))
        ],
      ),
    );
  }
}

API调用类

class GetCalendarApi {

  Future<List<CalendarResponse>> getCalendar(DateTime startTime, DateTime endTime) async {

    log('startTime $startTime', name: logGetCalendarApi);
    log('endTime $endTime', name: logGetCalendarApi);

    String token = await SharedPrefsMethods.getStringFromPrefs(PREF_TOKEN_KEY);

    String url = BASE_URL + SESSNOTE_URL + GET_CALENDAR_URL;

    List<CalendarResponse> calendarResponseList;

    Map<String, String> queryParameters = {
      "startTime": "${formatDate(startTime)}",
      "endTime": "${formatDate(endTime)}",
    };

    Uri uri = Uri.parse(url).replace(queryParameters: queryParameters);

    log('Uri: (logGetCalendarApi) $uri', name: logGetCalendarApi);

    return http.get(uri, headers: {
      HttpHeaders.contentTypeHeader: "application/json",
      HttpHeaders.authorizationHeader: "Bearer $token",
      'accept': 'text/plain'
    }).then((http.Response response) {

      int statusCode = response.statusCode;
      log('Status Code: $statusCode', name: logGetCalendarApi);
      log('RESPONSE BODY (get students and mandates): ${response.body}', name: logGetCalendarApi);

      dynamic data = json.decode(response.body);

      if (statusCode < 200 || statusCode >= 400) {
        log('Error: ${response.body}', name: logGetCalendarApi);
        Fluttertoast.showToast(
            msg: data['Message'] ??
                'An internal error has occurred.',
            toastLength: Toast.LENGTH_LONG,
            timeInSecForIosWeb: 3);
        throw Exception(data['Message'] ?? 'An internal error has occurred.');
      }

      calendarResponseList = data
          .map<CalendarResponse>(
              (json) => CalendarResponse.fromJson(json))
          .toList();

      log('Calendar result List Size: ${calendarResponseList.length}', name: logGetCalendarApi);

      return calendarResponseList;

    });
  }
}

其他页面刷新触发代码

ref.refresh(calendarResponseProvider);

原因分析

  • Riverpod的FutureProvider在调用ref.refresh()时,默认会保留之前缓存的数据状态,直到新的Future请求完成。这是框架为了避免UI闪烁、提升用户体验设计的行为。
  • 首次加载时,Provider没有缓存数据,直接进入loading状态;后续刷新时,Provider会先返回旧的data状态,直到新请求完成才更新为新数据,因此不会触发loading分支。

解决方案

方法1:利用AsyncValue.isRefreshing显示加载状态

在when方法的data分支中,通过calendarResponse.isRefreshing判断是否正在刷新,在原有数据上叠加加载指示器:

calendarResponse.when(
  data: (data) {
    return Stack(
      children: [
        Expanded(
          child: ListView.builder(
            scrollDirection: Axis.vertical,
            shrinkWrap: true,
            itemCount: data.length,
            itemBuilder: ((context, index) {
              return Text(data[index].studentNames ?? '-');
            }),
          ),
        ),
        // 刷新时显示加载指示器
        if (calendarResponse.isRefreshing)
          const Center(child: CircularProgressIndicator()),
      ],
    );
  },
  error: ((error, stackTrace) {
    return Text('Error: ${error.toString()}');
  }),
  loading: (() {
    return const Center(child: CircularProgressIndicator());
  }),
)

方法2:用family修饰Provider强制触发loading

如果需要刷新时完全替换为加载状态(不保留旧数据),可以给FutureProvider添加family修饰,每次刷新传入唯一标识(如时间戳),让Provider创建新实例:

  1. 修改Provider定义:
final calendarResponseProvider = FutureProvider.family<List<CalendarResponse>, int>((ref, refreshKey) async {
  return ref.read(getCalendarApiProvider).getCalendar(ref.read(startDateProvider), ref.read(endDateProvider));
});
  1. 主页面监听与刷新:
class _MainMenuScreenState extends ConsumerState<MainMenuScreen> {
  int _refreshKey = 0;

  @override
  Widget build(BuildContext context) {
    AsyncValue<List<CalendarResponse>> calendarResponse = ref.watch(calendarResponseProvider(_refreshKey));

    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            icon: const Icon(Icons.settings, color: Colors.white),
            onPressed: () {
              setState(() {
                _refreshKey++; // 更新key触发Provider重建
              });
            },
          )
        ],
        // 其他代码...
      ),
      // 其他代码...
    );
  }
}

方法3:手动管理刷新状态

新增一个StateProvider标记刷新状态,在调用refresh前后更新状态:

  1. 定义状态Provider:
final isCalendarRefreshingProvider = StateProvider<bool>((ref) => false);
  1. 触发刷新时更新状态:
// 其他页面刷新逻辑
ref.read(isCalendarRefreshingProvider.notifier).state = true;
try {
  await ref.refresh(calendarResponseProvider.future);
} finally {
  ref.read(isCalendarRefreshingProvider.notifier).state = false;
}
  1. 主页面监听并显示加载:
final isRefreshing = ref.watch(isCalendarRefreshingProvider);
AsyncValue<List<CalendarResponse>> calendarResponse = ref.watch(calendarResponseProvider);

// 在data分支中根据isRefreshing显示加载指示器

内容的提问来源于stack exchange,提问作者RJB

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 19:18:31