关于Flutter的提供程序包的详细信息

哈Ha!

我们的中期计划包括发行Flutter书。对于Dart语言作为主题,我们仍然持较为谨慎的立场,因此我们将尝试根据本文的结果评估其相关性。它将侧重于提供程序,因此,将侧重于Flutter中的状态管理。

Provider是由Remy Rusle编写并由Google和Flutter社区采用的状态管理程序包但是什么是状态管理?对于初学者来说,条件是什么?让我提醒您,状态仅仅是代表您的应用程序中的UI的数据。状态管理是一种创建,访问,处理和处置数据的方法。为了更好地理解Provider包,我们简要概述了Flutter中状态管理的历史。

1. StatefulWidget


StatelessWidget是一个简单的UI组件,仅在有数据时才显示。有StatelessWidget没有“记忆“;它是根据需要创建和销毁的。 Flutter还具有一个StatefulWidget,在其中有一个内存,这要归功于它长寿的卫星-State对象。此类有一个方法setState(),当调用该方法时,将启动一个小部件,该小部件将重建状态并以新形式显示状态。这是开箱即用提供的Flutter状态管理的最简单形式。这是一个带有按钮的示例,该按钮始终显示上次按下的时间:

class _MyWidgetState extends State<MyWidget> {
  DateTime _time = DateTime.now();  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text(_time.toString()),
      onPressed: () {
        setState(() => _time = DateTime.now());
      },
    );
  }
}

那么这种方法有什么问题呢?假设您的应用程序在根目录中存储了一些全局状态StatefulWidget它包含打算在UI的各个部分中使用的数据。该数据以参数的形式共享并传递给每个子小部件。计划更改此数据的所有事件都将以回调的形式弹出。因此,通过所有中间小部件,将传递许多参数和回调,这很快会引起混乱。更糟糕的是,对上述根目录的任何更新都将导致整个窗口小部件树的重建,效率低下。

2. InheritedWidget


InheritedWidget是一个特殊的小部件,其后代无需直接链接即可访问它。仅通过转到InheritedWidget,使用小部件即可注册自动重建,这将在重建祖先小部件时发生。此技术使您可以更有效地组织UI的更新。您可以选择仅选择那些需要重建的特定小部件,而不用响应状态的微小变化而重建应用程序的大块。InheritedWidget无论何时使用MediaQuery.of(context)或,您都已经在使用Theme.of(context)的确,您不太可能使用状态保存来实现自己的InheritedWidget。事实是正确实施它们并不容易。

3.范围模型


ScopedModel是Brian Egan于2017年创建的软件包,它易于使用InheritedWidget来存储应用程序状态。首先,您需要创建一个继承自Model的状态对象,然后notifyListeners()在其属性更改时调用它。这种情况让人联想到Java中PropertyChangeListener接口的实现

class MyModel extends Model {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

为了提供状态对象,我们将此对象包装在ScopedModel应用程序根目录中的小部件中:

ScopedModel<MyModel>(
  model: MyModel(),
  child: MyApp(...)
)

现在,任何后代小部件都将能够MyModel使用ScopedModelDescendant小部件进行访问模型实例将传递给参数builder

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<MyModel>(
      builder: (context, child, model) => Text(model.foo),
    );
  }
}

任何子代小部件也将能够更新模型,这将自动引发任何ScopedModelDescendants模型的重建(只要我们的模型正确调用notifyListeners()):

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = ScopedModel.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

ScopedModel在Flutter中作为状态管理工具而得到普及,但是它的使用仅限于提供继承类Model并使用这种更改通知模式的对象

4. BLoC


Google I / O '18 会议上,引入业务逻辑组件(BLoC)模式,该模式是从小部件提取状态的又一工具。 BLoC类是长期存在的非UI组件,可保留状态并将其公开为流和接收者。除了UI之外,还可以StatelessWidget使用状态和业务逻辑,您可以简单地实现小部件,并使用StreamBuilder进行自动重建。结果,小部件“变得愚蠢”,并且变得更容易测试。

BLoC类示例:

class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    //  
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
 ,   BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    //  
 });
}

BLoC模式的问题在于,如何创建和销毁BLoC对象并不明显。myBloc在上面的示例中如何创建实例?我们该如何打电话dispose()摆脱他?流需要使用StreamController,应该closed尽快使用它-这样做是为了防止内存泄漏。(在Dart中没有类析构器;只有类StateStatefulWidget有一个方法dispose())。此外,尚不清楚如何在多个小部件之间共享此BLoC。开发人员通常很难掌握BLoC。有几种软件包试图简化这一过程。

5.提供者


Provider是Remy Rusle在2018年编写的一个软件包,类似于ScopedModel,但其功能不限于提供Model的子类。这也是结束语InheritedWidget,但提供程序可以提供任何状态对象,包括BLoC,流,期货等。由于提供者是如此简单和灵活,因此Google在Google I / O '19大会上宣布,将来它将Provider成为管理状态的首选软件包。当然,也可以使用其他软件包,但是如果您有任何疑问,Google建议在停止Provider

Provider内置“带有小部件,用于小部件”。Provider允许您将带有状态的任何对象放置在小部件树中,并为其他任何小部件(子级)打开对其的访问权限。它还Provider通过用数据初始化状态对象并在将其从窗口小部件树中删除后进行清理来帮助管理状态对象的生存期。因此,它Provider甚至适合于实现BLoC组件,或者可以用作其他状态管理解决方案的基础!或简单地用于实现依赖关系 -一个花哨的术语,表示以某种方式将数据传输到小部件,从而允许您放松连接并提高代码的可测试性。最后,Provider带有一组专门的类,因此使用起来更加方便。接下来,我们将仔细研究每个类。

  • 基本提供者
  • ChangeNotifierProvider
  • 流提供者
  • 未来提供者
  • ValueListenableProvider
  • 多供应商
  • 代理提供者

安装


要使用它Provider,首先在文件中添加一个依赖项pubspec.yaml

provider: ^3.0.0

然后Provider在需要的地方导入包

import 'package:provider/provider.dart';

Base provider在应用程序的根目录中

创建base Provider;这将包含我们模型的一个实例:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

该参数builder创建一个实例MyModel如果要将现有实例传递给它,请在此处使用构造函数Provider.value

然后,您可以MyApp使用Widget 在任何地方使用此模型实例Consumer

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyModel>(
      builder: (context, value, child) => Text(value.foo),
    );
  }
}

在上面的示例中,该类使用Consumer窗口小部件MyWidget获取实例。这个小部件使我们可以将对象包含在参数中 现在,如果要更新模型中的数据该怎么办?假设我们有另一个小部件,在其中单击按钮时,应该更新属性MyModelbuildervalue

foo

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

注意用于访问实例的特定语法MyModel。从功能上讲,这等效于访问widget ConsumerConsumer在代码无法轻松获取链接的情况下,小部件很有用BuildContext

您认为MyWidget我们之前创建的原始小部件会怎样?会在其中显示新的含义bar吗?不幸的是,没有。无法收听旧的传统Dart对象中的更改(至少没有反射,这在Flutter中没有提供)。因此,Provider将无法“看到”我们已经正确更新了属性,foo并命令MyWidget响应而对小部件进行了更新。

ChangeNotifierProvider

但是有希望!您可以使我们的班级MyModel实施一个杂项ChangeNotifiernotifyListeners()每当我们的属性之一发生更改时,将需要一些时间来更改模型的实现并调用特殊方法它的工作方式大致相同ScopedModel,但是在这种情况下,您无需继承模型的特定类就很好了。实现混合就足够了ChangeNotifier看起来是这样的:

class MyModel with ChangeNotifier {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

正如你所看到的,我们更换我们的财产foogettersetter,支持由_foo私有变量。这样,我们可以“拦截”对foo属性所做的任何更改,并让我们的侦听器知道我们的对象已更改。

现在,从外部开始Provider,我们可以更改实现,以便它使用另一个名为的类 ChangeNotifierProvider

ChangeNotifierProvider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

像这样!现在,当我们OtherWidget更新foo实例中的属性MyModel,它将MyWidget自动更新以反映此更改。酷吧?

顺便说说。您可能已经注意到OtherWidget我们使用以下语法的按钮处理程序

final model = Provider.of<MyModel>(context);

默认情况下,OtherWidget一旦模型更改,此语法将自动导致实例重建MyModel也许我们不需要这个。最后,它OtherWidget仅包含一个按钮,该按钮在值更改时根本不会更改MyModel为了避免重建,您可以使用以下语法访问我们的模型而无需注册进行重建:

final model = Provider.of<MyModel>(context, listen: false);

这是包装中提供的另一种魅力Provider 乍看之下,

StreamProvider

尚不清楚为什么需要它StreamProvider最后,StreamBuilder如果需要在Flutter中使用流,则可以只使用常规方法例如,在这里我们收听onAuthStateChanged由提供的流FirebaseAuth

@override
Widget build(BuildContext context {
  return StreamBuilder(
   stream: FirebaseAuth.instance.onAuthStateChanged, 
   builder: (BuildContext context, AsyncSnapshot snapshot){ 
     ...
   });
}

为此Provider,我们可以StreamProvider在应用程序的根目录中提供流

StreamProvider<FirebaseUser>.value(
  stream: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(...),
}

然后使用子小部件,通常使用Provider

@override
Widget build(BuildContext context) {
  return Consumer<FirebaseUser>(
    builder: (context, value, child) => Text(value.displayName),
  );
}

我们的小部件代码不仅变得更加简洁,而且还抽象出了数据来自流的事实。如果我们决定将基本实现更改为FutureProvider,则无需更改小部件代码。您将看到,这适用于下面显示的所有其他提供程序。

FutureProvider

与上面的示例类似,在使用小部件时,它FutureProvider是标准的替代方法 FutureBuilder。这是一个例子:

FutureProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.currentUser(),
  child: MyApp(...),
);

消费的子控件这个值,我们使用相同的实现Consumer作为例子StreamProvider以上。

ValueListenableProvider

ValueListenable是由ValueNotifier类实现的Dart接口,该接口获取一个值并在其更改为另一个值时通知侦听器。例如,可以将整数计数器包装在简单的模型类中:

class MyModel {
  final ValueNotifier<int> counter = ValueNotifier(0);  
}

处理复杂类型时,它ValueNotifier使用==存储在其中对象的运算符来确定值是否已更改。
让我们创建最简单的一个Provider,它将包含我们的主模型,然后是一个Consumer嵌套的ValueListenableProvider侦听属性counter

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: Consumer<MyModel>(builder: (context, value, child) {
    return ValueListenableProvider<int>.value(
      value: value.counter,
      child: MyApp(...)
    }
  }
}

请注意,此嵌套提供程序的类型为int可能还有其他。如果您注册了多个相同类型的提供者,则提供者将返回“最近”(最接近的祖先)。

以下是counter从任何子窗口小部件监听属性的方法

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<int>(
      builder: (context, value, child) {
        return Text(value.toString());
      },
    );
  }
}

但是,这是counter从另一个窗口小部件更新属性的方法请注意:我们需要访问原始副本MyModel

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.counter.value++;
      },
    );
  }
}

MultiProvider

如果您使用许多WidgetProvider,那么在应用程序的根目录中,您会从许多附件中得到一个丑陋的结构:

Provider<Foo>.value( 
  value: foo, 
  child: Provider<Bar>.value( 
    value: bar, 
    child: Provider<Baz>.value( 
      value: baz , 
      child: MyApp(...)
    ) 
  ) 
)

MultiProvider允许您在同一级别上将它们全部声明。只是语法糖:在系统内级别,它们始终保持嵌套状态。

MultiProvider( 
  providers: [ 
    Provider<Foo>.value(value: foo), 
    Provider<Bar>.value(value: bar), 
    Provider<Baz>.value(value: baz), 
  ], 
  child: MyApp(...), 
)

ProxyProvider

ProxyProvider是在第三个软件包发行版中添加的一个有趣的类Provider它使您可以声明自己可能依赖于其他提供程序的提供程序,最多一对一。在此示例中,Bar类是特定于实例的Foo这在编译本身相互依赖的根服务集时很有用。

MultiProvider ( 
  providers: [ 
    Provider<Foo> ( 
      builder: (context) => Foo(),
    ), 
    ProxyProvider<Foo, Bar>(
      builder: (context, value, previous) => Bar(value),
    ), 
  ], 
  child: MyApp(...),
)

第一个泛型类型参数是您所依赖ProxyProvider的类型,第二个是它返回的类型。

如何同时收听许多提供商


如果我们希望单个窗口小部件侦听许多提供程序并在它们发生任何变化时进行重建怎么办?您可以使用小部件选项同时收听最多6个提供程序Consumer我们将收到实例作为其他方法参数builder

Consumer2<MyModel, int>(
  builder: (context, value, value2, child) {
    //value  MyModel
    //value2  int
  },
);

结论


使用时,InheritedWidget Provider它允许按照Flutter中的习惯管理状态。它允许窗口小部件以抽象基础通知机制的方式访问状态对象并收听状态对象。通过创建锚点以根据需要创建这些对象并在需要时将其删除,可以更轻松地管理状态对象的寿命。此机制可用于轻松实现依赖关系,甚至可作为更高级状态管理选项的基础。随着Google的加持和Flutter社区中越来越多的支持,它Provider已成为值得一试的软件包!

All Articles