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

Flutter RiverPod:如何为FutureProvider添加每次执行的后置处理?

解决RiverPod结合flutter_tts的朗读时机问题

你遇到的问题核心在于RiverPod的缓存机制和Widget重建触发逻辑的冲突:第一种方法里FutureProvider对相同参数的请求会缓存结果,导致后续调用不再执行朗读;第二种方法在build的data回调里执行朗读,又会因为Widget重建重复触发。下面给你几个针对性的解决方案:


方案1:用ref.listen精准监听状态变化,仅首次加载完成时朗读

这种方法可以让你只在Provider的状态从「加载中/错误」变为「成功获取数据」时触发朗读,完全避开Widget重建带来的重复执行。

修改MyHomePage的build逻辑:

class MyHomePage extends HookWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final list = useProvider(asyncListProvider.state);
    return ListView(
      children: [
        RaisedButton(
          child: Text("Add data"),
          onPressed: () {
            context.read(asyncListProvider).add("item id");
          },
        ),
        for (final item in list)
          HookConsumer(
            builder: (context, ref, child) {
              // 监听Provider的状态变化
              ref.listen<AsyncValue<String>>(item, (previous, next) {
                // 仅当状态从非data变为data时,触发朗读
                if (next.hasData && previous?.hasData != true) {
                  SpeakResult(next.data!);
                }
              });

              // 正常渲染UI
              return ref.watch(item).when(
                    loading: () => ListTile(
                      title: Center(child: CircularProgressIndicator()),
                    ),
                    error: (error, stackTrace) => Text("$error"),
                    data: (value) => Text("$value"),
                  );
            },
          ),
      ],
    );
  }
}

方案2:给请求添加唯一标识,强制FutureProvider重新执行

如果你的需求是即使itemId相同,每次点击Add都要重新请求并朗读,可以给family参数加上唯一标识(比如UUID),让RiverPod认为这是新的Provider实例,从而重新执行请求和朗读逻辑。

第一步:修改FutureProvider

// 用(id, uniqueKey)作为family参数
final futureProvider = FutureProvider.autoDispose.family<String, (String, String)>((ref, params) async {
  final (id, _) = params;
  ref.onDispose(() => print("disposed"));
  // 模拟网络请求
  final result = await Future.delayed(Duration(seconds: 1)).then((value) => "$id");
  // 在这里执行朗读,每个新请求都会触发
  await SpeakResult(result);
  return result;
});

第二步:修改AsyncList的add方法

import 'package:uuid/uuid.dart';

final asyncListProvider = StateNotifierProvider((_) => AsyncList());

class AsyncList extends StateNotifier<List<AutoDisposeFutureProvider<String>>> {
  final _uuid = Uuid();
  AsyncList() : super([]);

  void add(String id) {
    // 生成唯一标识
    final uniqueKey = _uuid.v4();
    final asyncValue = futureProvider((id, uniqueKey));
    state = [asyncValue, ...state];
  }
}

这样每次点击Add按钮,都会创建一个全新的Provider实例,请求和朗读逻辑都会重新执行,同时autoDispose会在item从列表移除时自动销毁Provider,避免内存泄漏。


方案3:在StateNotifier中完全掌控请求和朗读逻辑

如果你想彻底摆脱Provider缓存的限制,可以把请求、朗读、状态管理都放到StateNotifier里,完全自定义生命周期:

第一步:定义请求状态类和StateNotifier

final asyncListProvider = StateNotifierProvider((_) => AsyncList());

// 封装每个请求的状态
class RequestState {
  final String requestId; // 唯一标识每个请求
  final AsyncValue<String> value;
  RequestState({required this.requestId, required this.value});
}

class AsyncList extends StateNotifier<List<RequestState>> {
  AsyncList() : super([]);

  Future<void> add(String id) async {
    // 生成请求唯一标识
    final requestId = DateTime.now().microsecondsSinceEpoch.toString();
    // 先添加加载中状态
    state = [
      RequestState(requestId: requestId, value: const AsyncLoading()),
      ...state,
    ];
    try {
      // 模拟网络请求
      final result = await Future.delayed(Duration(seconds: 1)).then((value) => "$id");
      // 请求完成后立即朗读
      await SpeakResult(result);
      // 更新为成功状态
      state = state.map((item) {
        if (item.requestId == requestId) {
          return RequestState(requestId: requestId, value: AsyncData(result));
        }
        return item;
      }).toList();
    } catch (e, stack) {
      // 更新为错误状态
      state = state.map((item) {
        if (item.requestId == requestId) {
          return RequestState(requestId: requestId, value: AsyncError(e, stack));
        }
        return item;
      }).toList();
    }
  }
}

第二步:修改MyHomePage的UI渲染

class MyHomePage extends HookWidget {
  const MyHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final list = useProvider(asyncListProvider.state);
    return ListView(
      children: [
        RaisedButton(
          child: Text("Add data"),
          onPressed: () {
            context.read(asyncListProvider).add("item id");
          },
        ),
        for (final item in list)
          item.value.when(
            loading: () => ListTile(
              title: Center(child: CircularProgressIndicator()),
            ),
            error: (error, stackTrace) => Text("$error"),
            data: (value) => Text("$value"),
          ),
      ],
    );
  }
}

这种方法完全掌控了每个请求的生命周期,不用担心Provider缓存或Widget重建的干扰,适合复杂的业务场景。


方案选择建议

  • 若仅需相同itemId的数据首次加载时朗读一次:选方案1
  • 若需每次点击Add都重新请求并朗读(即使itemId相同):选方案2
  • 若想完全自定义请求和状态逻辑:选方案3

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 11:17:37