乐趣区

关于flutter:译-一文带你学会全部Flutter的Provider

原文在这里。

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

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

我只想用最简略的办法治理我的 app 的状态。为什么有这么多的抉择?我应该用哪一个呢?从哪里开始呢?

本文的目标就是帮你了解次要的 Provider 类型是怎么用的。我会给每一个 Provide 类型一个简略的例子。帮忙你了解,之后你就能够本人决定用哪个来治理你的 app 的状态了。

筹备

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

也就是:

  • 那个“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);
  }
}

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

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”。

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);
  }
}

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);
  }
}

不过作者自己对以上的认识起初产生了扭转,他举荐大家看这篇文章。

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();}
}

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');
  }
}

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 的教程心愿对你有帮忙。

退出移动版