[譯] 一文帶你學會全部Flutter的Provider

小紅星閃啊閃發表於2022-06-09

原文在這裡

雖然官方Flutter站點(狀態管理入門app)說Provider“非常容易理解”,我(作者)可不這麼認為。我想是因為Provider的種類有點多:

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

我只想用最簡單的方法管理我的app的狀態。為什麼有這麼多的選擇?我應該用哪一個呢?從哪裡開始呢?

本文的目的就是幫你理解主要的Provider型別是怎麼用的。我會給每一個Provide型別一個簡單的例子。幫助你理解,之後你就可以自己決定用哪個來管理你的app的狀態了。

準備

我的例子會使用這樣的佈局
image.png

也就是:

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

執行程式碼,你會看到這個結果:

image.png

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”。
image.png

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

image.png

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

image.png

不過作者本人對以上的看法後來發生了改變,他推薦大家看這篇文章

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

image.png

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

image.png

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的教程希望對你有幫助。

相關文章