使用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), ); } } }
问题根源
- Timer回调时序问题:
updateToy有模拟HTTP延迟,切换玩具时旧的Timer回调可能仍在执行,此时回调里的_speed还是旧玩具的本地值,会覆盖新玩具的状态。 - 重复注册监听:
ref.listen放在build方法中,每次组件build都会重新注册监听,可能导致多次触发或时序混乱。 - 无状态绑定校验: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), ); } }
优化说明
- 将
ref.listen移至initState,避免组件build时重复注册监听。 - 新增
_currentSelectedIndex绑定当前操作的玩具索引,Timer回调仅处理该索引对应的玩具,避免旧请求覆盖新状态。 - 切换玩具时重启Timer,确保同步逻辑从新状态开始执行。
内容的提问来源于stack exchange,提问作者Vinci
相关产品推荐
相关产品推荐

