likes
comments
collection
share

[译] 一文带你学会全部Flutter的Provider

作者站长头像
站长
· 阅读数 11

原文在这里

虽然官方Flutter站点(状态管理入门app)说Provider“非常容易理解”,我(作者)可不这么认为。我想是因为Provider的种类有点多:

  • Provider
  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider
  • FutureProvider
  • MultiProvider
  • ProxyProvider
  • ChangeNotifierProxyProvider
  • 更多

我只想用最简单的方法管理我的app的状态。为什么有这么多的选择?我应该用哪一个呢?从哪里开始呢?

本文的目的就是帮你理解主要的Provider类型是怎么用的。我会给每一个Provide类型一个简单的例子。帮助你理解,之后你就可以自己决定用哪个来管理你的app的状态了。

准备

我的例子会使用这样的布局

[译] 一文带你学会全部Flutter的Provider

也就是:

  • 那个“Do something”按钮代表改变app状态的事件。
  • 那个“Show something”文本Widget代表显示app state的UI
  • 左边的绿色方框和右边的蓝色的方框代表了widget树的不同部分。用来强调一个事件和与这个事件所更改的状态相关的更新的UI。

这里是代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('My App')),
        body: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            Container(
              padding: const EdgeInsets.all(20),
              color: Colors.green[200],
              child: RaisedButton(
                child: Text('Do something'),
                onPressed: () {},
              ),
            ),

            Container(
              padding: const EdgeInsets.all(35),
              color: Colors.blue[200],
              child: Text('Show something'),
            ),

          ],
        ),
      ),
    );
  }
}

这些例子需要你安装Provider包

dependencies:
  provider: ^4.0.1

而且已经import到了需要的地方:

import 'package:provider/provider.dart';

Provider

如你所想,Provider是最基本的Provider类型。你可以用它来给widget树的任何地方提供一个值(一般是data model对象)。然而,值发生变化的时候是不会帮你更新widget树的。

假设你的app状态在一个model里:

class MyModel { 
  String someValue = 'Hello';
  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
  }
}

你可以用Provider包装顶层Widget,给widget树提供数据。使用Consume widget来获得这个数据的引用。

你可以在西面的代码找到Provider和两个Consume widget:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<MyModel>( //                                <--- Provider
      create: (context) => MyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        // We have access to the model.
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
  }
}

class MyModel { //                                               <--- MyModel
  String someValue = 'Hello';
  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
  }
}

运行代码,你会看到这个结果:

[译] 一文带你学会全部Flutter的Provider

NOTES:

  • UI上显示了mode对象的Hello
  • 点击“Do something”按钮会引发一个导致model数据改变的事件。然而,即使model的数据改变了,UI也不会发生更改。因为Provider不会监听model对象的数据变更。

ChangeNotifierProvider

与基本的Provider widget不同,ChangeNotifierProvider会监听model对象的变化。当数据发生变更,它会重绘Consumer的子widget。

在上面的代码里,把Provider换成ChangeNotifierProvider。Model类也需要使用ChangeNotifier mixin(or 扩展它)。这样你可以使用notifyListeners()方法。当你调用这个方法的时候,ChangeNotifierProvider就会收到通知,Consumer widget就会重绘它的子组件了。

下面是完整代码:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MyModel>( //      <--- ChangeNotifierProvider
      create: (context) => MyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                  padding: const EdgeInsets.all(20),
                  color: Colors.green[200],
                  child: Consumer<MyModel>( //                  <--- Consumer
                    builder: (context, myModel, child) {
                      return RaisedButton(
                        child: Text('Do something'),
                        onPressed: (){
                          myModel.doSomething();
                        },
                      );
                    },
                  )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
  }
}

class MyModel with ChangeNotifier { //                          <--- MyModel
  String someValue = 'Hello';

  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
    notifyListeners();
  }
}

现在当你点击了“Do something”按钮,文本会从“Hello”变成“Goodbye”。

[译] 一文带你学会全部Flutter的Provider

NOTE:

  • 多数的app里,model类都在各自的文件里。你需要在这些文件里import flutter/foundation.dart,这样才能用ChangeNotifier。我不太喜欢这样,因为这样的话就表明你的业务逻辑里包含了一个framework的依赖,而且这个framework还是一个和逻辑无关的“细节”。我们暂时先这样。
  • notifyListeners()方法后,Consumer widget会重绘它的子树的所有widget都会重绘。按钮没有必要更新,所以相对于Consumer你可以用Provider.of,并把listener设置为false。这样model有变化,按钮也不会更新了。这里是修改了之后的按钮widget。
class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final myModel = Provider.of<MyModel>(context, listen: false);
    return RaisedButton(
      child: Text('Do something'),
      onPressed: () {
        myModel.doSomething();
      },
    );
  }
}

下面的代码还是老样子,使用Consumer组件。

FutureProvider

FutureProvider基本上是FutureBuilder的一个封装widget。你可以设置一些UI显示的初始数据,然后可以给一个Future值。FutureProvider会在Future完成的时候通知Consumer重绘它的widget子树。

在下面的代码里我给UI提供了一个空的model。我也加了一个方法,会在3秒钟后返回一个新的model对象。这也是FutureProvider在等待的。

和基本的Provider一样,FutureProvider不会监听model本身的任何更改。在下面的代码里会表明这一点。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureProvider<MyModel>( //                      <--- FutureProvider
      initialData: MyModel(someValue: 'default value'),
      create: (context) => someAsyncFunctionToGetMyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
    
  }
}

Future<MyModel> someAsyncFunctionToGetMyModel() async { //  <--- async function
  await Future.delayed(Duration(seconds: 3));
  return MyModel(someValue: 'new data');
}

class MyModel { //                                               <--- MyModel
  MyModel({this.someValue});
  String someValue = 'Hello';
  Future<void> doSomething() async {
    await Future.delayed(Duration(seconds: 2));
    someValue = 'Goodbye';
    print(someValue);
  }
}

[译] 一文带你学会全部Flutter的Provider

NOTE:

  • FutureProvider会在Futuer<MyModel>完成后通知Consumer重绘。
  • 点击Hot restart重置app
  • 注意,点击“Do something”按钮不会更新UI,即使是在Future完成了之后。如果你想让UI可以重绘,使用ChangeNotifierProvider
  • FutureProvider的使用情况一般是在处理网络或者文本读取数据的时候。但是,也可以使用FutureBuilder来达到同样的目的。以我(作者)不成熟的看法,FutureProvider没有比FutureBuilder更有用。如果我需要一个provider,我基本上会用ChangeNotifierProvider,如果我不需要provider的话可能会用FutureProvider

StreamProvider

StreamProviderStreamBuilder的一个封装。你提供一个stream,然后Consumer会在stream收到一个事件的时候更新它的widget子树。这和上面的FutureProvider非常相似。

你可以把stream发送的值当做不可修改的。也就是说,StreamProvider不会监听model的变化。只会监听stream里的新事件。

代码如下:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamProvider<MyModel>( //                       <--- StreamProvider
      initialData: MyModel(someValue: 'default value'),
      create: (context) => getStreamOfMyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
    
  }
}

Stream<MyModel> getStreamOfMyModel() { //                        <--- Stream
  return Stream<MyModel>.periodic(Duration(seconds: 1),
          (x) => MyModel(someValue: '$x'))
      .take(10);
}

class MyModel { //                                               <--- MyModel
  MyModel({this.someValue});
  String someValue = 'Hello';
  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
  }
}

NOTE:

  • StreamProvider在收到新的事件之后通知Consumer重绘
  • 使用hot restart重置app
  • 点击“Do something”不会重绘UI。如果你想要UI可以更新,那么使用ChangeNotifierProvider。事实上,你可以在model里加一个stream,然后调用notifyListeners()。这样的话完全不需要StreamProvider了。
  • 你可以使用StreamProvider实现BLoC模式

ValueListenableProvider

你可以略过这一节,它和ChangeNotifierProvider基本一样,只是更加复杂一点,而且没有什么额外的好处。。。

如果你有一个这样的ValueNotifier

class MyModel {
  ValueNotifier<String> someValue = ValueNotifier('Hello');
  void doSomething() {
    someValue.value = 'Goodbye';
  }
}

那么你可以用ValueListenableProvider来监听这个值的变化。但是,如果你想在UI里面调用model的方法,那么你也需要在provider里provide这个model对象。因此,你会在下面的代码里看到一个Provider提供了一个MyModel对象给Consumer,而这个provider同时也给了ValueListenableProvider它需要的包含在MyModelValueNotifier属性someValue。也就是如果只是监听一个model对象的属性值只需要ValueListenableProvider,但是你想在UI里调用这个model的方法,那么还要额外写一个Provider

完整代码如下:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<MyModel>(//                              <--- Provider
      create: (context) => MyModel(),
      child: Consumer<MyModel>( //                           <--- MyModel Consumer
          builder: (context, myModel, child) {
            return ValueListenableProvider<String>.value( // <--- ValueListenableProvider
              value: myModel.someValue,
              child: MaterialApp(
                home: Scaffold(
                  appBar: AppBar(title: Text('My App')),
                  body: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[

                      Container(
                          padding: const EdgeInsets.all(20),
                          color: Colors.green[200],
                          child: Consumer<MyModel>( //       <--- Consumer
                            builder: (context, myModel, child) {
                              return RaisedButton(
                                child: Text('Do something'),
                                onPressed: (){
                                  myModel.doSomething();
                                },
                              );
                            },
                          )
                      ),

                      Container(
                        padding: const EdgeInsets.all(35),
                        color: Colors.blue[200],
                        child: Consumer<String>(//           <--- String Consumer
                          builder: (context, myValue, child) {
                            return Text(myValue);
                          },
                        ),
                      ),

                    ],
                  ),
                ),
              ),
            );
          }),
    );
  }
}

class MyModel { //                                             <--- MyModel
  ValueNotifier<String> someValue = ValueNotifier('Hello'); // <--- ValueNotifier
  void doSomething() {
    someValue.value = 'Goodbye';
    print(someValue.value);
  }
}

[译] 一文带你学会全部Flutter的Provider

不过作者本人对以上的看法后来发生了改变,他推荐大家看这篇文章

ListenableProvider

如果你要定制一个provider,那么就需要用到这个provider类型。不过文档还是提醒,你也许只是需要一个ChangeNotifierProvider。所以,这个内容暂时忽略。之后我(作者)会更新这部分。

MultiProvider

到目前为止,我们的例子还是只用过一个model对象。如果你需要另外的一个model对象,你就要嵌套provider了(就和在讲解ValueListenableProvider的时候一样)。然而,嵌套只会造成混乱。一个更好的办法就是使用MultiProvider

在下面的例子里会使用ChangeNotifierProvider提供两种model对象。

下面是全部代码。有点长。只需要注意MultiProviderConsumer和两个model类。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider( //                                     <--- MultiProvider
      providers: [
        ChangeNotifierProvider<MyModel>(create: (context) => MyModel()),
        ChangeNotifierProvider<AnotherModel>(create: (context) => AnotherModel()),
      ],
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Column(
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[

                  Container(
                      padding: const EdgeInsets.all(20),
                      color: Colors.green[200],
                      child: Consumer<MyModel>( //            <--- MyModel Consumer
                        builder: (context, myModel, child) {
                          return RaisedButton(
                            child: Text('Do something'),
                            onPressed: (){
                              // We have access to the model.
                              myModel.doSomething();
                            },
                          );
                        },
                      )
                  ),

                  Container(
                    padding: const EdgeInsets.all(35),
                    color: Colors.blue[200],
                    child: Consumer<MyModel>( //              <--- MyModel Consumer
                      builder: (context, myModel, child) {
                        return Text(myModel.someValue);
                      },
                    ),
                  ),

                ],
              ),

             // SizedBox(height: 5),

              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[

                  Container(
                      padding: const EdgeInsets.all(20),
                      color: Colors.red[200],
                      child: Consumer<AnotherModel>( //      <--- AnotherModel Consumer
                        builder: (context, myModel, child) {
                          return RaisedButton(
                            child: Text('Do something'),
                            onPressed: (){
                              myModel.doSomething();
                            },
                          );
                        },
                      )
                  ),

                  Container(
                    padding: const EdgeInsets.all(35),
                    color: Colors.yellow[200],
                    child: Consumer<AnotherModel>( //        <--- AnotherModel Consumer
                      builder: (context, anotherModel, child) {
                        return Text('${anotherModel.someValue}');
                      },
                    ),
                  ),

                ],
              ),
            ],
          ),
        ),
      ),
    );

  }
}

class MyModel with ChangeNotifier { //                        <--- MyModel
  String someValue = 'Hello';
  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
    notifyListeners();
  }
}

class AnotherModel with ChangeNotifier { //                   <--- AnotherModel
  int someValue = 0;
  void doSomething() {
    someValue = 5;
    print(someValue);
    notifyListeners();
  }
}

[译] 一文带你学会全部Flutter的Provider

NOTES:

  • 点击第一个“Do something”按钮,文本会从Hello变成Goodbye。点击第二个“Do something”按钮,文本会从0变成5
  • 这和单个的ChangeNotifierProvider没有很大的不同。Consumer通过不同的类型参数获取到对应的model对象。

ProxyProvider

如果你有两个model对象要提供,而且这两个model对象之间还有依赖关系。在这个情况下你可以使用ProxyProvider。一个ProxyProvider从一个provider里接受对象,然后把这个对象注入另外一个provider。

一开始你会对ProxyProvider的使用方法有些困惑,这个例子可以帮你加深理解。

MultiProvider(
  providers: [
    ChangeNotifierProvider<MyModel>(
      create: (context) => MyModel(),
    ),
    ProxyProvider<MyModel, AnotherModel>(
      update: (context, myModel, anotherModel) => AnotherModel(myModel),
    ),
  ],

基本的ProxyProvider有两个类型参数。第二个依赖于第一个。在ProxyProviderudpate方法里,第三个参数有anotherModel存了上次的值,但是我们不在这里使用。我们只是把myModel当做参数传入AnotherModel的构造函数里。

完整代码:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider( //                              <--- MultiProvider
      providers: [
        ChangeNotifierProvider<MyModel>( //               <--- ChangeNotifierProvider
          create: (context) => MyModel(),
        ),
        ProxyProvider<MyModel, AnotherModel>( //          <--- ProxyProvider
          update: (context, myModel, anotherModel) => AnotherModel(myModel),
        ),
      ],
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Column(
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[

                  Container(
                      padding: const EdgeInsets.all(20),
                      color: Colors.green[200],
                      child: Consumer<MyModel>( //          <--- MyModel Consumer
                        builder: (context, myModel, child) {
                          return RaisedButton(
                            child: Text('Do something'),
                            onPressed: (){
                              myModel.doSomething('Goodbye');
                            },
                          );
                        },
                      )
                  ),

                  Container(
                    padding: const EdgeInsets.all(35),
                    color: Colors.blue[200],
                    child: Consumer<MyModel>( //            <--- MyModel Consumer
                      builder: (context, myModel, child) {
                        return Text(myModel.someValue);
                      },
                    ),
                  ),

                ],
              ),

              Container(
                  padding: const EdgeInsets.all(20),
                  color: Colors.red[200],
                  child: Consumer<AnotherModel>( //          <--- AnotherModel Consumer
                    builder: (context, anotherModel, child) {
                      return RaisedButton(
                        child: Text('Do something else'),
                        onPressed: (){
                          anotherModel.doSomethingElse();
                        },
                      );
                    },
                  )
              ),

            ],
          ),
        ),
      ),
    );

  }
}

class MyModel with ChangeNotifier { //                       <--- MyModel
  String someValue = 'Hello';
  void doSomething(String value) {
    someValue = value;
    print(someValue);
    notifyListeners();
  }
}

class AnotherModel { //                                      <--- AnotherModel
  MyModel _myModel;
  AnotherModel(this._myModel);
  void doSomethingElse() {
    _myModel.doSomething('See you later');
    print('doing something else');
  }
}

[译] 一文带你学会全部Flutter的Provider

NOTES:

  • 开始文本显示的是Hello
  • 当你点击“Do something”按钮,MyModel对象的文本会变成GoodbyeMyModel通知了ChangeNotifierProvider, UI也显示了新的文本。
  • 当你点击“Do something else”按钮,AnotherModel接收MyModelProxyProvider注入),之后修改文本为"See you later"。因为MyModel发生更改的时候通知了listener,所以UI发生了更新。如果AnotherModel更改了它的值,UI是不会更新的。因为ProxyProvider不会监听更改。这时候可以使用ChangeNotifierProxyProvider
  • ProxyProvider还是让我有点迷糊。ChangeNotifierProxyProvider本身就有更多的更多说明了,所以我不打算介绍更多了。
  • 我(作者)FilledStack用GetIt比用ProxyProvider实现依赖注入更好用。

Provider builder和value constructor

最后还有一件事需要解释。

大部分的provider都有两种构造函数。一个基本的接收一个create方法,用来新建你的model对象。我们在上面的例子中基本都是这么做的:

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

可以看到MyModel对象是在create方法创建的。

如果你的对象已经创建好了,你只是想要提供一个它的引用。那么你可以用命名构造方法value:

final myModel = MyModel();
...
Provider<MyModel>.value(
    value: myModel, 
    child: ...
)

这里MyModel对象提前就建好了,只提供了一个引用。如果你在initState方法已经建好了对象,你就可以这么做。

总结

基本上你可以忽略provider包里的大多数类。只记住怎么使用ChangeNotifierProviderConsumer。如果你不想要更新UI,那么就使用Provider。使用Future和Stream的场景都可以把他们放在你的Model类里面,然后通知ChangeNotifierProvider。不需要FutureProviderStreamProvdier。大多数的时候不需要MultiProvider。如果有依赖注入的时候,也可以用GetIt包来实现,不需要用ProxyProvider这篇文章有更深入的介绍。

下一步: Riverpod

你也许听说过Riverpod,这是一个构建在provider之上的状态管理库。作者也是同一个作者。如果你对provider的使用还有困惑,那么riverpod只会让你更加困惑。但是,一旦熟悉了这个库,那么它会让一切变得简单。我(作者)建议你试一试。我写了一个Riverpod的教程希望对你有帮助。