将 Provider 的范围限定为多个实例

Scope a Provider to multiple instances

我有一个 isLoading StateNotifierProvider 可以在点击按钮时向我的按钮添加加载指示器,直到异步方法完成。

但是,当我有两个按钮同时可见时,它们都会在只有一个被点击时显示指示器。

如何将 StateNotifierProvider 的范围限定为每个按钮一个实例,而不是所有按钮一个实例?

按钮:

class AsyncSubmitButton extends ConsumerWidget {
  AsyncSubmitButton(
      {required this.text,
      required this.onPressed})
      : super(key: key);

  final String text;
  final Future<void> Function() onPressed;

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final isLoading = watch(isLoadingProvider);
    final form = ReactiveForm.of(context)!;
    return ElevatedButton(
        onPressed: form.invalid
            ? null
            : () => context.read(isLoadingProvider.notifier).pressed(onPressed),
        child: isLoading
            ? const CircularProgressIndicator()
            : Text(text),
        );
  }
}

提供商:

final isLoadingProvider =
    StateNotifierProvider<AsyncSubmitButtonLoadingStateNotifier, bool>((ref) {
  return AsyncSubmitButtonLoadingStateNotifier(isLoading: false);
});

class AsyncSubmitButtonLoadingStateNotifier extends StateNotifier<bool> {
  AsyncSubmitButtonLoadingStateNotifier({required bool isLoading})
      : super(isLoading);

  dynamic pressed(Future<void> Function() onPressedFunction) async {
    state = true;
    try {
      await onPressedFunction();
    } catch (error) {
      state = false;
    } finally {
      state = false;
    }
  }
}

作为, and also this answer suggest, use the family modifier。这个问题与链接答案中的问题重复,但我发现这个用例没有很好地记录(仅在 SO/GitHub 个答案中),我认为这个答案更完整所以想把它留在这里。

提供商的状态始终在全球范围内共享。但是您可以使用 family 修饰符通过传入的 id 访问该状态,然后在读取提供程序时比较 id 以查看它是否是相应的实例。使用family时记得使用.autoDispose以防止内存泄漏。我认为它可能会为每次读取时使用不同的传入参数创建对具有传入参数的提供者的新引用​​,并且除非使用 autoDispose 修饰符,否则永远不会处置提供者。

所以题中的代码变为:

按钮:

class AsyncSubmitButton extends ConsumerWidget {
  AsyncSubmitButton(
      {required this.text,
      this.id = 'global',
      required this.onPressed})
      : super(key: key);

  final String text;
  final Future<void> Function() onPressed;
  final String id;

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final isLoading = watch(isLoadingProvider(id)) &&
        id == context.read(isLoadingProvider(id).notifier).id;
    final form = ReactiveForm.of(context)!;
    return ElevatedButton(
        onPressed: form.invalid
            ? null
            : () => context.read(isLoadingProvider.notifier).pressed(onPressed),
        child: isLoading
            ? const CircularProgressIndicator()
            : Text(text),
        );
  }
}

提供商:

final isLoadingProvider = StateNotifierProvider.family
    .autoDispose<AsyncSubmitButtonLoadingStateNotifier, bool, String>(
        (ref, id) {
  return AsyncSubmitButtonLoadingStateNotifier(isLoading: false, id: id);
});

class AsyncSubmitButtonLoadingStateNotifier extends StateNotifier<bool> {
  AsyncSubmitButtonLoadingStateNotifier(
      {required bool isLoading, required this.id})
      : super(isLoading);

  String id;

  dynamic onPressed(Future<void> Function() onPressedFunction) async {
    state = true;
    try {
      await onPressedFunction();
    } catch (error) {
      state = false;
    } finally {
      state = false;
    }
  }
}

并且在小部件中,如果您不关心它的唯一性,只需不传递 id,它就会默认。如果您确实在意,因为您有 2 个独特的按钮使用它等,请指定 id...

child: AsyncSubmitButton(text: 'Sign Out', id: 'signOutButton', onPressed: vm.signOut)