Flutter 知識梳理 (狀態管理) - Provider 之各種 XXProvider 的使用姿勢

澤毛發表於2019-07-31

一、前言

Provider是目前Google推薦的狀態管理方式之一,建議大家可以先看一下 Provider 的 Github 地址 瞭解基本的用法。

網上大多數介紹Provider的文章講的都是ChangeNotifierProvider,看完之後確實知道它是幹什麼的,以及怎麼用。

然而其實還有其它的Provider供我們使用,那麼它們之間的區別和聯絡是什麼呢,官方文件對它們的使用也沒有詳細的Demo,這篇文章就來總結一下它們的用法和區別。

Provider的分類有如下幾種:

  • Provider
  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider
  • FutureProvider

二、Provider

2.1 建構函式

Provider的建構函式如下:

  Provider({
    Key key,
    @required ValueBuilder<T> builder,
    Disposer<T> dispose,
    Widget child,
  })
複製程式碼
  • builderT Function(BuildContext context),返回要共享的資料Model
  • disposevoid Function(BuildContext context, T value),在回撥中釋放資源。

2.2 優點 & 缺點

Provider的優點:

  • 資料共享:通過Provider.of<T>(context)方法,可以在以Provider為根節點的子樹中獲取到T的物件。
  • 通過dispose引數,可以進行資源的釋放。

但是Provider也有一個明顯的缺點:

  • 在共享資料發生改變時,不能通知它的監聽者

2.3 計數器例子

下面我們用一個經典的計數器Demo演示一下Provider的使用,為了方便對比,後面在介紹其它Provider時,也使用該例子。

根據Provider的兩個特點,我們可以用它來實現BLocsink的獲取,以及最後資源的釋放。

  • 首先我們定義一個簡單的Bloc模型。
import 'dart:async';

class ProviderBloc {

  int _count = 0;
  var _countController = StreamController<int>.broadcast();

  Stream<int> get stream => _countController.stream;
  int get count => _count;

  increment() {
    _countController.sink.add(++_count);
  }

  dispose() {
    _countController.close();
  }
}
複製程式碼
  • 接下來編寫主介面。
    • 通過Provider.of<ProviderBloc>(context)我們可以在任意的地方獲取到ProviderBloc物件,獲得sink來更改資料。
    • 使用StreamBuilder監聽Stream中資料的變化,重新整理介面。
    • 在原始的Bloc模型上,頂層的Provider提供了dispose回撥,用於資源的釋放。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider_bloc.dart';

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

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Provider<ProviderBloc>(
      builder: (context) => ProviderBloc(),
      dispose: (context, bloc) => bloc.dispose(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('Provider Demo')),
          body: CounterLabel(),
          floatingActionButton: CounterButton(),
        ),
      ),
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: Provider.of<ProviderBloc>(context).increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: StreamBuilder<int>(
        builder: (context, snapshot) {
          return Text('you have push ${snapshot.data} times');
        },
        initialData: 0,
        stream: Provider.of<ProviderBloc>(context).stream,
      ),
    );
  }
}
複製程式碼

三、ChangeNotifierProvider

ChangeNotifierProvider應該是大家見的最多的,大多數介紹Provider的文章都是以它為例子,和 Provider 相比,它最大的優點就是解決了資料改變後無法監聽的問題

3.1 建構函式

ChangeNotifierProvider的建構函式為如下:

  ChangeNotifierProvider({
    Key key,
    @required ValueBuilder<T> builder,
    Widget child,
  })
複製程式碼
  • builderT Function(BuildContext context),返回要共享的資料Model

3.2 ChangeNotifier

使用ChangeNotifierProvider時,它要求builder返回的資料Model必須是ChangeNotifier的子類。

  • 在改變資料後,呼叫notifyListener()方法。
  • 通過重寫它的dispose方法,可以完成和Provider一樣的資源釋放工作。
class Counter with ChangeNotifier {

  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  @override
  void dispose() {
    super.dispose();
  }
}
複製程式碼

3.3 Provider.of(context) & Consumer

在介紹Provider的文章中,Provider.of<T>(context)Consumer都會被拿來對比,一般都會推薦使用Consumer因為它會將資料發生變化後,把監聽者的 Widget 重建的範圍限制地更小

專案中Provider的使用,可以分為兩個角色,資料改變的 觸發者監聽者

  • 觸發者:如果只是需要獲取到資料model,不需要監聽變化(例如點選按鈕),推薦使用Provider.of<Counter>(context, listen: false)來獲取資料model
  • 監聽者:推薦使用Consumer

3.4 例子

3.4.1 定義資料模型

  • 首先,定義資料模型:
    • 在改變資料後,呼叫notifyListeners方法。
    • 如果需要釋放資源,那麼需要重寫dispose方法。
import 'package:flutter/foundation.dart';

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
  
  @override
  void dispose() {
    super.dispose();
  }
}
複製程式碼

3.4.2 主檔案

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

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

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Counter>(
      builder: (context) => Counter(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('Provider Demo')),
          body: CounterLabel(),
          floatingActionButton: CounterButton(),
        ),
      )
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: Provider.of<Counter>(context, listen : false).increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<Counter>(
          builder: (BuildContext context, Counter counter, Widget child) {
            return Text('you have push ${counter.count} times');
          }),
    );
  }
}
複製程式碼

四、ListenableProvider

4.1 建構函式

ListenableProvider的建構函式為:

  ListenableProvider({
    Key key,
    @required ValueBuilder<T> builder,
    Disposer<T> dispose,
    Widget child,
  })
複製程式碼
  • builderT Function(BuildContext context),返回要共享的資料Model
  • disposevoid Function(BuildContext context, T value),在回撥中釋放資源。

4.2 和 ChangeNotifierProvider 對比

先來一下ChangeNotifierProvider的原始碼:

class ChangeNotifierProvider<T extends ChangeNotifier>
    extends ListenableProvider<T> implements SingleChildCloneableWidget {
  
  static void _disposer(BuildContext context, ChangeNotifier notifier) =>
      notifier?.dispose();

  ChangeNotifierProvider({
    Key key,
    @required ValueBuilder<T> builder,
    Widget child,
  }) : super(key: key, builder: builder, dispose: _disposer, child: child);

}
複製程式碼

從原始碼上可以看出,ListenableProviderChangeNotifierProvider其實是 父與子的關係ChangeNotifierProvider在它的基礎上:

  • 限制資料model的上限,要求必須是ChangeNotifier的子類。
  • 通過重寫ChangeNotifier.dispose()來完成資源的釋放,不需要傳入dispose引數給ChangeNotifierProvider

4.3 例子

使用ListenableProvider時,假如我們沒有將資料模型定義成ChangeNotifier的子類,那麼需要自己進行監聽者的管理,為了方便,我們還是繼續使用ChangeNotifier,其它地方的使用和ChangeNotifierProvider都是一樣的。

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

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

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ListenableProvider<Counter>(
        builder: (context) => Counter(),
        dispose: (context, counter) => counter.dispose(),
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: CounterLabel(),
            floatingActionButton: CounterButton(),
          ),
        )
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: Provider.of<Counter>(context, listen: false).increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<Counter>(
          builder: (BuildContext context, Counter counter, Widget child) {
            return Text('you have push ${counter.count} times');
          }),
    );
  }
}
複製程式碼

五、ValueListenableProvider

5.1 建構函式

  ValueListenableProvider({
    Key key,
    @required ValueBuilder<ValueNotifier<T>> builder,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
複製程式碼
  • builderT Function(BuildContext context),返回要共享的資料Model
  • disposevoid Function(BuildContext context, T value),在回撥中釋放資源。

5.2 ValueNotifier

ValueListenableProvider要求builder返回的物件必須是ValueNotifier<T>的子類,T是需要共享的資料型別。

ValueNotifier的定義如下:

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {

  ValueNotifier(this._value);

  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}
複製程式碼

ValueNotifier是我們前面多次提到的ChangeNotifier的子類,在改變_value時,自動呼叫了notifyListeners方法,那麼就參照之前的方法來使用它。

5.3 例子

5.3.1 定義 ValueNotifier 的子類

import 'package:flutter/foundation.dart';

class CounterModel {

  int count;
  CounterNotifier wrapper;

  CounterModel(this.count);

}

class CounterNotifier extends ValueNotifier<CounterModel> {

  CounterNotifier(CounterModel value) : super(value) {
    value.wrapper = this;
  }

  @override
  void dispose() {
    super.dispose();
  }

}
複製程式碼

5.3.2 主檔案

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

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

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ValueListenableProvider<CounterModel>(
        builder: (context) => CounterNotifier(CounterModel(0)),
        updateShouldNotify: (model1, model2) {
          print('updateShouldNotify');
          return model1.count != model2.count;
        },
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: _CounterLabel(),
            floatingActionButton: _CounterButton(),
          ),
        )
    );
  }
}

class _CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () {
        CounterModel oldModel = Provider.of<CounterModel>(context, listen: false);
        CounterModel newModel = CounterModel(oldModel.count + 1);
        newModel.notifier = oldModel.notifier;
        oldModel.notifier.value = newModel;
        return;
      },
      child: const Icon(Icons.add),
    );
  }

}

class _CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<CounterModel>(
          builder: (BuildContext context, CounterModel model, Widget child) {
            return Text('you have push ${model.count} times');
          }),
    );
  }
}
複製程式碼

5.4 疑問

這裡有一個問題困擾了我很久:就是使用ValueNotifier<T>時必須要改變_value才會觸發notifyListeners()方法,而通過Provider.of<T>(context, listen: false)拿到的物件是_value,因此還需要在它裡面儲存ValueNotifier<T>的引用(或者將ValueNotifier定義成單例的模式),再設定一次達到觸發的效果,感覺用起來很奇怪,不知道是不是我的使用方式有問題,網上也沒有找到相關的例子。

六、StreamProvider

StreamProvider用來結合Bloc使用。

6.1 建構函式

6.1.1 使用 Stream 構造

  StreamProvider({
    Key key,
    @required ValueBuilder<Stream<T>> builder,
    T initialData,
    ErrorBuilder<T> catchError,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
複製程式碼
  • builder:返回Bloc中的Stream
  • initialData:初始資料。
  • catchError:發生錯誤時候的回撥。

6.1.2 使用 StreamController 構造

  StreamProvider.controller({
    Key key,
    @required ValueBuilder<StreamController<T>> builder,
    T initialData,
    ErrorBuilder<T> catchError,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
複製程式碼
  • builder:返回Bloc中的StreamController

6.2 例子

6.2.1 定義單例模式

import 'dart:async';

class ProviderBloc {

  static ProviderBloc _instance;

  ProviderBloc._internal() {
    print("_internal");
  }

  static ProviderBloc _getInstance() {
    if (_instance == null) {
      _instance = ProviderBloc._internal();
    }
    return _instance;
  }

  factory ProviderBloc() => _getInstance();
  static ProviderBloc get instance => _getInstance();

  int _count = 0;
  var _countController = StreamController<int>.broadcast();

  Stream<int> get stream => _countController.stream;
  int get count => _count;

  increment() {
    _countController.sink.add(++_count);
  }

  dispose() {
    _countController.close();
  }

}
複製程式碼

6.2.2 主檔案

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

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

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return StreamProvider<int> (
        builder: (context) {
          return ProviderBloc().stream;
        },
        catchError: (BuildContext context, Object error) {},
        initialData: 0,
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: CounterLabel(),
            floatingActionButton: CounterButton(),
          ),
        )
    );
  }
}

class CounterButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: ProviderBloc().increment,
      child: const Icon(Icons.add),
    );
  }

}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Consumer<int>(
          builder: (BuildContext context, int value, Widget child) {
            return Text('you have push $value times');
          }),
    );
  }
}
複製程式碼

七、FutureProvider

7.1 建構函式

  FutureProvider({
    Key key,
    @required ValueBuilder<Future<T>> builder,
    T initialData,
    ErrorBuilder<T> catchError,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })
複製程式碼
  • builder:返回一個Future<T>物件,非同步任務的返回結果,在結果返回後,會觸發Consumer的重建。
  • initialData:初始資料。
  • catchError:發生錯誤的回撥。

7.2 和 FutureBuilder 的區別

FutureProvider和我們之前討論的Provider場景不太一樣,它和FutureBuilder比較類似,就是在資料返回之前載入一個元件,等待資料返回值後,重繪返回另一個元件。

它和FutureBuilder的區別在於:

  • FutureBuilder的資料請求和展示都是在一個元件當中,而FutureProvider是資料的請求者,Consumer是展示者。
  • FutureBuilder的請求者和展示者是一對一的關係,而FutureProvider可以是一對多的關係。

7.3 例子

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

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

class _ProviderApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return FutureProvider<int>(
        builder: (context) => _request(),
        initialData: 0,
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: Text('Provider Demo')),
            body: CounterLabel(),
          ),
        )
    );
  }

  Future<int> _request() async {
    return await Future<int>.delayed(Duration(milliseconds: 3000)).then((int value) {
        return 300;
    });
  }
}

class CounterLabel extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(children: <Widget>[
        Consumer<int>(
            builder: (BuildContext context, int count, Widget child) {
              return Text('Observer1=$count');
            }),
        Consumer<int>(
            builder: (BuildContext context, int count, Widget child) {
              return Text('Observer2=$count');
            }),
      ],)
    );
  }
}
複製程式碼

七、小結

對比以上幾種Provider的使用方式:還是ChangeNotifierProviderStreamProvider比較符合我們平時的使用場景。

而其它三種:

  • Provider:不能監聽資料改變,直接淘汰。
  • ListenableProviderChangeNotifierProvider的原始版本。
  • ValueListenableProviderChangeNotifierProvider的增強版本,但是怎麼感覺用起來還更麻煩了。

相關文章