[Flutter翻译]Flutter:对比GetIt,Provider和Riverpod

2,059 阅读5分钟

本文由 简悦SimpRead 转码,原文地址 blog.gskinner.com

今天我们要看一下Flutter中比较流行的3个基本状态管理的库。G......

浏览器中的 "状态管理 "是指 "状态管理",而不是 "状态管理"。

今天我们来看看Flutter中基本状态管理的3个比较流行的库。GetItProviderriverpod

对于每一个库,我们将看看你如何执行数据绑定、数据注入,以及如何模拟它们进行测试。

配置...

在Flutter中,有许多类型的数据可以共享。期货、流、ValueNotifiers等。在这个例子中,我们将使用一个经典的基于 "ChangeNotifier "的管理器,它有两个可变的属性。它看起来就像这样。

class AppManager extends ChangeNotifier {
    ...
    int _count1 = 0;
    int get count => _count;
    set count1(int value){
      _count = value;
      notifyListener();
    }

    int _count2 = 0;
    set count2(int value){
      _count2 = value;
      notifyListener();
    }
}

Provider

为了举例说明,设想我们还有两个对象需要传递:SettingsManagerFileService。另外,让我们假设设置和应用程序,都需要使用文件服务(这将让我们展示共享对象相互访问)。为了跟上下面的例子,查看github上的代码

Provider在widget树中使用父:子关系传递数据。一个MultiProvider小组件可以用来传递多个对象到下面的小组件树中。

class ProviderExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider(create: (_) => FileService()),
        ChangeNotifierProvider(create: (context) {
          return SettingsManager(() => context.read<FileService>());
        }),
        ChangeNotifierProvider(create: (context) {
          return AppManager(() => context.read<FileService>());
        }),
      ],
      child: ...,
    );
  }
}

在视图层中,context.read扩展可以用来访问数据,context.watchcontext.select可以用来与之绑定。

class View1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    void handleTap() => context.read<AppManager>().count1++;
    int count = context.select((AppManager m) => m.count1);
    ...
  }
}

这里的select调用将我们的部件与ChangeNotifier绑定,现在只要.count1属性发生变化,它就会重建,而忽略count2的变化。

使用提供者进行测试

要用Provider进行测试,你可以扩展你的数据对象来创建一个模拟,并在你想测试的widget上面提供这个模拟。

class MockFileService extends FileService { ... }

return MultiProvider(
  providers: [
    Provider(create: (_) => MockFileService()),
    ...
  ],
  child: ...,
);

不幸的是,Provider没有任何系统允许你从 "外部 "覆盖依赖关系,这可能会使事情更难测试。

提供者的局限性

如前所述,Provider的可测试性是有限的,因为它没有内置的覆盖系统。它也很难在widget树之外工作,因为每次查找都需要context。最后,当它试图提供同一类型的多个对象时,会出现问题。

GetIt + GetItMixin

GetIt是经典的服务定位器 模式的实现,提供各种静态方法来注册单体或工厂。

final sl = GetIt.instance;
sl.registerSingleton(FileService());
sl.registerSingleton(SettingsManager(() => sl.get<FileService>()));
sl.registerSingleton(AppManager(() => sl.get<FileService>()));

get方法可以用来读取数据,GetItMixin包提供了各种方法来在你的视图中绑定它。在本例中,为了绑定到 "ChangeNotifier "的一个属性,我们可以使用 "watchOnly",我们从mixin中获得。

class View1 extends StatelessWidget with GetItMixin {
  @override
  Widget build(BuildContext context) {
    void handleTap() => GetIt.I.get<AppManager>().count1++;
    int count = watchOnly((AppManager m) => m.count1);
    ...
  }
}

使用GetIt进行测试

使用GetIt进行测试,你可以使用unregisterregister方法,随时随地提供一个模拟类,这使得测试变得轻而易举。

GetIt.I.unregister(GetIt.I.get<FileService>());
GetIt.I.register(MockFileService());

你也可以使用pushNewScopepopScope来轻松设置和拆除一组注册对象。

GetIt.I.pushNewScope();
GetIt.I.register(MockFileService());
// do tests
GetIt.I.popScope();

GetIt的局限性

GetIt也有提供相同类型的实例的问题,然而它在注册时暴露了一个name字段,这为这个问题提供了一个合理的解决方法。另一个潜在的弱点是,你可能不得不手动处置或取消注册项目,因为它们不是widget树的内在组成部分。最后,一些开发者可能不喜欢 "GetIt "单项的全局性,他们更喜欢 "riverpod "或 "Provider "的更严格的范围模型,而其他开发者可能认为这是个好处。

Riverpod

使用riverpod,你定义了全局可访问的 "提供者",然后你使用ref对象来读取它们。一个`提供者范围'也必须放置在widget树的周围。

final fileServiceProvider = Provider((_) => FileService());
final settingsManagerProvider = ChangeNotifierProvider((ref) {
  return SettingsManager(() => ref.read(fileServiceProvider));
});
final appManagerProvider = ChangeNotifierProvider((ref) {
  return AppManager(() => ref.read(fileServiceProvider));
});

class RiverpodExample extends StatelessWidget {
  const RiverpodExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ProviderScope(
      child: ...,
    );
  }
}

在声明了你的提供者后,你可以对ConsumerWidget进行子类化,并声明一个稍加修改的build方法,以获得一个ref,然后允许你读取提供者。

class View1 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    void handleTap() => ref.read(appManagerProvider).count1++;
    int count = ref.watch(appManagerProvider.select((p) => p.count1));
    ...
  }
}

使用riverpod测试

riverpod提供了一个简单的覆盖机制,可以用来强制模拟到任何提供者。

ProviderScope  overrides: [
    fileServiceProvider.overrideWithValue(MockFileService())
  ],
  child: ...,
);

riverpod的局限性

从功能上来说,riverpod没有太多的限制,它有很强的可测试性,并且允许轻松注入相同类型的对象。它最大的缺点可能是使用起来略显笨拙。它需要一个自定义的widget,一个修改过的构建方法签名,而且所有的读取都需要一个ref。虽然这在开始时感觉有点烦,但一旦你习惯了,就不是问题了。为了进一步缓解这个问题,riverpod还附带了一套用于Android StudioVS Code的代码片段。

尾思...

真的,你用这些状态管理库中的任何一个都不会出错。你选择哪一个主要是个人品味的问题。GetIt采用了最简单的方法,并提供了逻辑对象和widget树的硬脱钩。riverpod在服务定位器公式上引入了一个强大的转折,它的 "许多定位器 "+范围方法,但不像其他的那样容易学习。最后但并非最不重要(好吧,基本上是最不重要)的是,Provider为您提供了不加修饰的、传统的 "继承小部件 "风格的方法,其API非常简单,在小部件树中非常有效,但在树外就不那么好了。

如果您有任何问题(或更正!),请在下面告诉我们。


www.deepl.com 翻译