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

使用Timer时,如何保持Flutter临时状态与Riverpod状态同步?

问题:Flutter + Riverpod 多玩具控制的状态同步竞态问题

我正在开发一款通过HTTP请求远程控制多个玩具的Flutter应用,控制元素始终可见,用户可从列表切换激活的玩具。控制元素有本地临时状态,每个玩具的独立状态由Riverpod管理。

快速切换玩具时,会随机出现一个玩具的状态被另一个覆盖的情况,存在明显的竞态条件。

我使用ref.listen在激活玩具变更时更新临时状态,同时通过定时触发的Timer将临时状态同步到Riverpod的玩具状态。推测问题出在ref.listen的lambda函数与Timer回调的执行顺序上,导致旧的本地状态被同步到新选中的玩具,或是新状态被旧请求覆盖。


相关代码

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:word_generator/word_generator.dart';

part 'main.g.dart';

// ignore: constant_identifier_names
const int NUMBER_OF_TOYS = 4;

// 遥控玩具类
class Toy {
  final String name;
  final double speed;
  const Toy(this.name, this.speed);
}

// 用AsyncNotifierProvider管理玩具列表
@riverpod
class Toys extends _$Toys {
  @override
  FutureOr<List<Toy>> build() async {
    return List.generate(
        NUMBER_OF_TOYS, (index) => Toy(WordGenerator().randomNoun(), 0));
  }

  Future<void> updateToy(int index, Toy toy) async {
    assert(index <= NUMBER_OF_TOYS);

    // 模拟HTTP请求延迟
    await Future.delayed(const Duration(milliseconds: 100));

    // 更新状态
    state = await AsyncValue.guard(() async {
      return [
        for (var i = 0; i < state.requireValue.length; ++i)
          if (i == index) toy else state.requireValue[i],
      ];
    });
  }
}

// 当前选中玩具的索引(可为null)
final selectedToyIndexProvider = StateProvider<int?>((_) => null);

// 当前选中的玩具(可为null)
@riverpod
Toy? selectedToy(SelectedToyRef ref) {
  final toys = ref.watch(toysProvider);
  final selectedIndex = ref.watch(selectedToyIndexProvider);
  return selectedIndex != null ? toys.requireValue[selectedIndex] : null;
}

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final selectedIndex = ref.watch(selectedToyIndexProvider);
    final toys = ref.watch(toysProvider);

    return MaterialApp(
      title: 'Toy control',
      home: Scaffold(
          body: toys.when(
        data: (data) {
          return Row(children: [
            Flexible(
              child: ListView(
                shrinkWrap: true,
                children: [
                  for (int i = 0; i < NUMBER_OF_TOYS; ++i)
                    ListTile(
                      title: Text('Toy "${data[i].name}"'),
                      subtitle: Text('${data[i].speed}'),
                      onTap: () => ref
                          .read(selectedToyIndexProvider.notifier)
                          .update((state) => selectedIndex != i ? i : null),
                      selected: selectedIndex == i,
                    ),
                ],
              ),
            ),
            selectedIndex != null ? const MySlider() : const Placeholder(),
          ]);
        },
        error: (error, stackTrace) {
          return const Placeholder();
        },
        loading: () {
          return const Placeholder();
        },
      )),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MySlider extends ConsumerStatefulWidget {
  const MySlider({super.key});

  @override
  ConsumerState<MySlider> createState() => _MySliderState();
}

class _MySliderState extends ConsumerState<MySlider> {
  late final Timer _timer;
  late double _speed;

  @override
  void initState() {
    super.initState();
    final toy = ref.read(selectedToyProvider);
    _speed = toy!.speed;
    _timer = Timer.periodic(const Duration(milliseconds: 200), _timerCallack);
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 更新临时状态:玩具索引变化时
    ref.listen(selectedToyIndexProvider, (previous, next) {
      if (next == null || previous == next) return;
      final toy = ref.watch(selectedToyProvider);
      setState(() {
        _speed = toy!.speed;
      });
    });

    return Slider(
      value: _speed,
      onChanged: (speed) => setState(() {
        _speed = speed;
      }),
    );
  }

  // 定时同步临时状态到Riverpod
  void _timerCallack(_) {
    final selectedIndex = ref.watch(selectedToyIndexProvider);
    final toy = ref.read(selectedToyProvider)!;
    if (selectedIndex != null) {
      ref.read(toysProvider.notifier).updateToy(
            selectedIndex,
            Toy(toy.name, _speed),
          );
    }
  }
}

问题根源

  1. Timer回调时序问题:updateToy有模拟HTTP延迟,切换玩具时旧的Timer回调可能仍在执行,此时回调里的_speed还是旧玩具的本地值,会覆盖新玩具的状态。
  2. 重复注册监听:ref.listen放在build方法中,每次组件build都会重新注册监听,可能导致多次触发或时序混乱。
  3. 无状态绑定校验:Timer回调中没有校验当前操作的是否是最新选中的玩具,旧请求会干扰新状态。

修复方案

修改后的代码

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:word_generator/word_generator.dart';

part 'main.g.dart';

// ignore: constant_identifier_names
const int NUMBER_OF_TOYS = 4;

// 遥控玩具类
class Toy {
  final String name;
  final double speed;
  const Toy(this.name, this.speed);
}

// 用AsyncNotifierProvider管理玩具列表
@riverpod
class Toys extends _$Toys {
  @override
  FutureOr<List<Toy>> build() async {
    return List.generate(
        NUMBER_OF_TOYS, (index) => Toy(WordGenerator().randomNoun(), 0));
  }

  Future<void> updateToy(int index, Toy toy) async {
    assert(index < NUMBER_OF_TOYS);

    // 模拟HTTP请求延迟
    await Future.delayed(const Duration(milliseconds: 100));

    // 更新状态
    state = await AsyncValue.guard(() async {
      return [
        for (var i = 0; i < state.requireValue.length; ++i)
          if (i == index) toy else state.requireValue[i],
      ];
    });
  }
}

// 当前选中玩具的索引(可为null)
final selectedToyIndexProvider = StateProvider<int?>((_) => null);

// 当前选中的玩具(可为null)
@riverpod
Toy? selectedToy(SelectedToyRef ref) {
  final toys = ref.watch(toysProvider);
  final selectedIndex = ref.watch(selectedToyIndexProvider);
  return selectedIndex != null ? toys.requireValue[selectedIndex] : null;
}

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final selectedIndex = ref.watch(selectedToyIndexProvider);
    final toys = ref.watch(toysProvider);

    return MaterialApp(
      title: '玩具控制',
      home: Scaffold(
          body: toys.when(
        data: (data) {
          return Row(children: [
            Flexible(
              child: ListView(
                shrinkWrap: true,
                children: [
                  for (int i = 0; i < NUMBER_OF_TOYS; ++i)
                    ListTile(
                      title: Text('玩具 "${data[i].name}"'),
                      subtitle: Text('${data[i].speed}'),
                      onTap: () => ref
                          .read(selectedToyIndexProvider.notifier)
                          .update((state) => selectedIndex != i ? i : null),
                      selected: selectedIndex == i,
                    ),
                ],
              ),
            ),
            selectedIndex != null ? const MySlider() : const Placeholder(),
          ]);
        },
        error: (error, stackTrace) {
          return const Placeholder();
        },
        loading: () {
          return const Placeholder();
        },
      )),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MySlider extends ConsumerStatefulWidget {
  const MySlider({super.key});

  @override
  ConsumerState<MySlider> createState() => _MySliderState();
}

class _MySliderState extends ConsumerState<MySlider> {
  Timer? _timer;
  late double _speed;
  int? _currentSelectedIndex; // 绑定当前操作的玩具索引,避免竞态

  @override
  void initState() {
    super.initState();
    final selectedIndex = ref.read(selectedToyIndexProvider);
    _currentSelectedIndex = selectedIndex;
    final toy = ref.read(selectedToyProvider);
    _speed = toy!.speed;
    _startTimer();
    
    // 只注册一次选中玩具变化的监听
    ref.listen(selectedToyIndexProvider, (previous, next) {
      if (next == null || previous == next) return;
      // 切换玩具时立即更新本地状态和绑定的索引
      final toy = ref.read(selectedToyProvider)!;
      setState(() {
        _speed = toy.speed;
        _currentSelectedIndex = next;
      });
    });
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  // 重启Timer,确保切换玩具时同步逻辑重置
  void _startTimer() {
    _timer?.cancel();
    _timer = Timer.periodic(const Duration(milliseconds: 200), _timerCallback);
  }

  @override
  Widget build(BuildContext context) {
    return Slider(
      value: _speed,
      onChanged: (speed) => setState(() {
        _speed = speed;
      }),
    );
  }

  void _timerCallback(_) {
    final selectedIndex = _currentSelectedIndex;
    if (selectedIndex == null) return;
    // 仅更新绑定的当前玩具索引,避免旧回调干扰
    ref.read(toysProvider.notifier).updateToy(
          selectedIndex,
          Toy(ref.read(selectedToyProvider)!.name, _speed),
        );
  }
}

优化说明

  1. 将ref.listen移至initState,避免组件build时重复注册监听。
  2. 新增_currentSelectedIndex绑定当前操作的玩具索引,Timer回调仅处理该索引对应的玩具,避免旧请求覆盖新状态。
  3. 切换玩具时重启Timer,确保同步逻辑从新状态开始执行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 01:14:59