如何保持短暂状态和 Riverpod 状态同步(使用计时器时)

问题描述 投票:0回答:1

我正在尝试编写一个 flutter 应用程序,它可以让您使用 HTTP 请求远程控制多个玩具。控制元素始终可见,用户可以通过从列表中选择活动玩具来在可用玩具之间自由切换。当然,这会导致控制元素具有一些本地(短暂)状态,并且所有玩具都有各自的状态(我在这里使用 Riverpod)。

现在,我遇到的问题是保持这两个状态同步。当在列表中的玩具之间快速切换时,一个玩具的状态会随机地被另一个玩具的状态覆盖。所以显然我在这里创建了某种形式的竞争条件:

我使用 Riverpods ref.listen 在当前活动的玩具发生变化时更新临时状态,并使用定期触发的计时器来从临时状态更新玩具的状态。我最好的猜测(没有完全理解 darts 执行模型)是 ref.listen 执行的 lambda 函数和计时器回调的执行方式是将旧的本地状态发送到新选择的玩具,或者反过来。

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 representing our remote controlled toy
class Toy {
  final String name;
  final double speed;
  const Toy(this.name, this.speed);
}

// Using an (Async)NotifierProvider to manage a list of toys
@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);

    // Fake some delay to simulate HTTP request
    await Future.delayed(const Duration(milliseconds: 100));

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

// Provider giving us the index of the currently selected toy index (or null)
final selectedToyIndexProvider = StateProvider<int?>((_) => null);

// Provider giving us the currently selected toy (or 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) {
    // Update ephemeral state if toy index changes and not null
    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;
      }),
    );
  }

  // Use a timer callback to periodically update toy state
  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),
          );
    }
  }
}
flutter state race-condition riverpod
1个回答
0
投票

首先,我会考虑使用冻结,我只是继续简化我的调试。我也简化了你的方法。

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:word_generator/word_generator.dart';

import 'main.dart';

part 'toy.freezed.dart';
part 'toy.g.dart';

@freezed
class Toy with _$Toy {
  const factory Toy({
    required String name,
    required double speed,
  }) = _Toy;
}

@riverpod
class Toys extends _$Toys {
  @override
  FutureOr<List<Toy>> build() async {
    return List.unmodifiable(List.generate(
      numberOfToys,
      (_) => Toy(name: WordGenerator().randomNoun(), speed: 0),
    ));
  }

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

    // Fake some delay to simulate HTTP request
    await Future.delayed(const Duration(milliseconds: 100));

    state = await AsyncValue.guard(() async => List.unmodifiable([...state.requireValue]..[index] = toy));
  }
}

然后,您不需要混合短暂状态,只需继续并完全使用 Riverpod 即可:

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final toy = ref.watch(selectedToyProvider)!;
    final index = ref.watch(selectedToyIndexProvider)!;
    return Slider(
      value: toy.speed,
      onChanged: (speed) => ref.read(toysProvider.notifier).updateToy(index, toy.copyWith(speed: speed)),
    );
  }
}

@riverpod
Toy? selectedToy(SelectedToyRef ref) {
  final toys = ref.watch(toysProvider);
  final selectedIndex = ref.watch(selectedToyIndexProvider);
  return selectedIndex != null ? toys.requireValue[selectedIndex] : null;
}

final selectedToyIndexProvider = StateProvider<int?>((_) => null);

这是整个 main.dart。希望这有帮助。

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

part 'main.g.dart';

const int numberOfToys = 4;

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) => Row(children: [
              Flexible(
                child: ListView(
                  shrinkWrap: true,
                  children: [
                    for (int i = 0; i < numberOfToys; ++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) => const Placeholder(),
          loading: () => const Placeholder(),
        ),
      ),
      debugShowCheckedModeBanner: false,
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final toy = ref.watch(selectedToyProvider)!;
    final index = ref.watch(selectedToyIndexProvider)!;
    return Slider(
      value: toy.speed,
      onChanged: (speed) => ref.read(toysProvider.notifier).updateToy(index, toy.copyWith(speed: speed)),
    );
  }
}

@riverpod
Toy? selectedToy(SelectedToyRef ref) {
  final toys = ref.watch(toysProvider);
  final selectedIndex = ref.watch(selectedToyIndexProvider);
  return selectedIndex != null ? toys.requireValue[selectedIndex] : null;
}

final selectedToyIndexProvider = StateProvider<int?>((_) => null);
© www.soinside.com 2019 - 2024. All rights reserved.