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

Flutter Riverpod疑问:为何需热重载才更新UI?我犯了什么错误?

问题:Flutter Riverpod状态更新后UI不刷新,仅热重载生效

我使用Flutter Riverpod进行状态管理,点击时通过ref.read(titleBlurProvider.notifier).lightUpPlayTitle()执行函数,控制台能看到打印输出,但UI仅在热重载后才更新。请问为何必须热重载才能更新UI?我哪里出错了?

完整代码:

class StartPage extends StatelessWidget {
  const StartPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Image.asset('assets/matches_background.jpeg'),
          const TitlesWidget()
        ],
      ),
    );
  }
}

class TitlesWidget extends StatelessWidget {
  const TitlesWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          TitleWidget(
            text: 'Play',
            index: 0,
            callOnTap: () {
              Navigator.pushReplacement(
                  context,
                  MaterialPageRoute(
                      builder: ((context) => const PositionedPage())));
            },
          ),
          TitleWidget(
            text: 'Leads',
            index: 1,
            callOnTap: () {},
          )
        ],
      ),
    );
  }
}

class TitleWidget extends ConsumerWidget {
  TitleWidget(
      {super.key,
      required this.text,
      required this.callOnTap,
      required this.index});
  String text;
  Function callOnTap;

  int index = 0;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final double opacity = ref.watch(titleBlurProvider)[index];
    return Consumer(
      builder: (context, ref, child) => Stack(
        children: [
          Opacity(
            opacity: opacity,
            child: ImageFiltered(
              imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
              child: Text(
                text,
                style: const TextStyle(
                    fontSize: 80,
                    fontWeight: FontWeight.bold,
                    color: Colors.white),
                textAlign: TextAlign.center,
              ),
            ),
          ),
          GestureDetector(
            onLongPress: () => debugPrint(opacity.toString()),
            onTap: () =>
                ref.read(titleBlurProvider.notifier).lightUpPlayTitle(),
            child: Opacity(
              opacity: 0.8,
              child: Text(
                text,
                style: const TextStyle(
                    fontSize: 80,
                    fontWeight: FontWeight.bold,
                    color: Colors.white),
                textAlign: TextAlign.center,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

final titleBlurProvider = StateNotifierProvider<TitleBlur, List<double>>(
  (ref) => TitleBlur(),
);

class TitleBlur extends StateNotifier<List<double>> {
  TitleBlur() : super([0, 0]);

  startRegularBlur() {
    Timer.periodic(const Duration(), (timer) {});
  }

  lightUpPlayTitle() {
    debugPrint('light up');

    while (state[0] < 1) {
      Future.delayed(const Duration(milliseconds: 500));
      state[0] = state[0] + 0.1;
      if (state[0] > 1) state[0] = 1;
      debugPrint(state[0].toString());
    }
  }
}

我尝试将Consumer移到组件树顶部,但没有效果。


问题原因与修复方案

核心错误点

  1. 直接修改StateNotifier的state列表元素,未触发状态更新
    Riverpod的StateNotifier依赖state引用的变化来通知监听者刷新UI。你直接修改state[0] = state[0] + 0.1时,列表的引用并未改变,所以UI无法感知到状态变化,只有热重载时重新读取状态才会更新。

  2. 同步while循环+无await的Future.delayed阻塞主线程
    Future.delayed是异步操作,但你在同步while循环中调用它却没有用await,导致循环瞬间执行完毕,直接将state[0]加到1,同时阻塞主线程,UI完全没有机会刷新中间状态。

修复步骤

1. 正确更新StateNotifier的状态

修改lightUpPlayTitle方法,每次更新时创建新的列表(改变引用),并使用异步循环避免阻塞:

Future<void> lightUpPlayTitle() async {
  debugPrint('light up');
  // 用异步循环替代同步while,配合await等待延迟
  while (state[0] < 1) {
    await Future.delayed(const Duration(milliseconds: 500));
    // 创建新列表,替换旧state,触发状态更新
    state = [
      state[0] + 0.1 > 1 ? 1.0 : state[0] + 0.1,
      state[1],
    ];
    debugPrint(state[0].toString());
  }
}

2. 简化Widget代码(可选)

TitleWidget已经是ConsumerWidget,内部的Consumer是多余的,可以移除,同时优化类型定义:

class TitleWidget extends ConsumerWidget {
  TitleWidget({
    super.key,
    required this.text,
    required this.callOnTap,
    required this.index,
  });
  final String text;
  final VoidCallback callOnTap; // 用类型更安全的VoidCallback替代Function
  final int index;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final double opacity = ref.watch(titleBlurProvider)[index];
    return Stack(
      children: [
        Opacity(
          opacity: opacity,
          child: ImageFiltered(
            imageFilter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
            child: Text(
              text,
              style: const TextStyle(
                fontSize: 80,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ),
        GestureDetector(
          onLongPress: () => debugPrint(opacity.toString()),
          onTap: () => ref.read(titleBlurProvider.notifier).lightUpPlayTitle(),
          child: Opacity(
            opacity: 0.8,
            child: Text(
              text,
              style: const TextStyle(
                fontSize: 80,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ),
      ],
    );
  }
}

关键注意事项

  • StateNotifier的state必须是不可变对象,或者每次更新都创建新的引用(比如列表、自定义对象要重新实例化),才能触发UI刷新。
  • 异步操作必须配合await使用,否则会导致逻辑异常或主线程阻塞。
  • 优先使用类型明确的回调(如VoidCallback)替代泛型Function,提升代码安全性。

内容的提问来源于stack exchange,提问作者im-not-a-robot

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 17:40:22