1. 程式人生 > 實用技巧 >Flutter 狀態管理之Provider

Flutter 狀態管理之Provider

flutter中狀態管理是重中之重,每當談這個話題,總有說不完的話。

在正式介紹 Provider 為什麼我們需要狀態管理。如果你已經對此十分清楚,那麼建議直接跳過這一節。
如果我們的應用足夠簡單,Flutter 作為一個宣告式框架,你或許只需要將 資料 對映成 檢視 就可以了。你可能並不需要狀態管理,就像下面這樣。


但是隨著功能的增加,你的應用程式將會有幾十個甚至上百個狀態。這個時候你的應用應該會是這樣。

這又是什麼鬼。我們很難再清楚的測試維護我們的狀態,因為它看上去實在是太複雜了!而且還會有多個頁面共享同一個狀態,例如當你進入一個文章點贊,退出到外部縮略展示的時候,外部也需要顯示點贊數,這時候就需要同步這兩個狀態。

Flutter 實際上在一開始就為我們提供了一種狀態管理方式,那就是 StatefulWidget。但是我們很快發現,它正是造成上述原因的罪魁禍首。
State 屬於某一個特定的 Widget,在多個 Widget 之間進行交流的時候,雖然你可以使用 callback 解決,但是當巢狀足夠深的話,我們增加非常多可怕的垃圾程式碼。
這時候,我們便迫切的需要一個架構來幫助我們理清這些關係,狀態管理框架應運而生。

Provider 是什麼

通過使用Provider而不用手動編寫InhertedWidget,您將獲取諮詢分配、延遲載入、打打減少每次建立新類的程式碼。

首先在yaml中新增,具體版本號參考:

官方Provider pub,當前版本號是4.1.3.

Provider: ^4.1.3

  

然後執行

flutter pub get

獲取到最新的包到本地,在需要的資料夾內匯入
import 'package:provider/provider.dart';

簡單例子

我們還用點選按鈕新增數字的例子

首先建立儲存資料的Model

class ProviderModel extends ChangeNotifier {

    int _count=0;
    ProviderModel();
   void plus() {
   /// 在資料變動的時候通知監聽者重新整理UI
_count = _count + 1; notifyListeners(); } }

構造view

/// 使用Consumer來監聽全域性重新整理UI
Consumer<ProviderModel>(
        builder:
            (BuildContext context, ProviderModel value, Widget child) {
          print('Consumer 0 重新整理');
          _string += 'c0 ';
          return _Row(
            value: value._count.toString(),
            callback: () {
              context.read<ProviderModel>().plus();
            },
          );
        },
        child: _Row(
          value: '0',
          callback: () {
            context.read<ProviderModel>().plus();
          },
        ),
      )

測試下看下效果:

單個Model多個小部件分別重新整理(區域性重新整理)

單個model實現單個頁面多個小部件分別重新整理,是使用Selector<Model,int>來實現,首先看下建構函式:

class Selector<A, S> extends Selector0<S> {
  /// {@macro provider.selector}
  Selector({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector != null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(context, Provider.of(context)),
          child: child,
        );
}

可以看到Selector繼承了Selector0,再看Selector關鍵build程式碼:
class _Selector0State<T> extends SingleChildState<Selector0<T>> {
  T value;
  Widget cache;
  Widget oldWidget;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    final selected = widget.selector(context);

    var shouldInvalidateCache = oldWidget != widget ||
        (widget._shouldRebuild != null &&
            widget._shouldRebuild.call(value, selected)) ||
        (widget._shouldRebuild == null &&
            !const DeepCollectionEquality().equals(value, selected));
    if (shouldInvalidateCache) {
      value = selected;
      oldWidget = widget;
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    return cache;
  }
}

根據我們傳入的_shouldRebuild來判斷是否需要更新,如果需要更新則執行widget.build(context,selected,child),否則返回已經快取的cache.當沒有_shouldRebuild引數時則根據widget.selector(ctx)的返回值判斷是否和舊值相等,不等則更新UI

所以我們不寫shouldRebuild也是可以的。

區域性重新整理用法

 Widget build(BuildContext context) {
    print('page 1');
    _string += 'page ';
    return Scaffold(
      appBar: AppBar(
        title: Text('Provider 全域性與區域性重新整理'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text('全域性重新整理<Consumer>'),
            Consumer<ProviderModel>(
              builder:
                  (BuildContext context, ProviderModel value, Widget child) {
                print('Consumer 0 重新整理');
                _string += 'c0 ';
                return _Row(
                  value: value._count.toString(),
                  callback: () {
                    context.read<ProviderModel>().plus();
                  },
                );
              },
              child: _Row(
                value: '0',
                callback: () {
                  context.read<ProviderModel>().plus();
                },
              ),
            ),
            SizedBox(
              height: 40,
            ),
            Text('區域性重新整理<Selector>'),
            Selector<ProviderModel, int>(
              builder: (ctx, value, child) {
                print('Selector 1 重新整理');
                _string += 's1 ';
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Selector<Model,int>次數:' + value.toString()),
                    OutlineButton(
                      onPressed: () {
                        context.read<ProviderModel>().plus2();
                      },
                      child: Icon(Icons.add),
                    )
                  ],
                );
              },
              selector: (ctx, model) => model._count2,
              shouldRebuild: (m1, m2) {
                print('s1:$m1 $m2 ${m1 != m2 ? '不相等,本次重新整理' : '資料相等,本次不重新整理'}');
                return m1 != m2;
              },
            ),
            SizedBox(
              height: 40,
            ),
            Text('區域性重新整理<Selector>'),
            Selector<ProviderModel, int>(
              selector: (context, model) => model._count3,
              shouldRebuild: (m1, m2) {
                print('s2:$m1 $m2 ${m1 != m2 ? '不相等,本次重新整理' : '資料相等,本次不重新整理'}');
                return m1 != m2;
              },
              builder: (ctx, value, child) {
                print('selector 2 重新整理');
                _string += 's2 ';
                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Selector<Model,int>次數:' + value.toString()),
                    OutlineButton(
                      onPressed: () {
                        ctx.read<ProviderModel>().plus3();
                      },
                      child: Icon(Icons.add),
                    )
                  ],
                );
              },
            ),
            SizedBox(
              height: 40,
            ),
            Text('重新整理次數和順序:↓'),
            Text(_string),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                OutlineButton(
                  child: Icon(Icons.refresh),
                  onPressed: () {
                    setState(() {
                      _string += '\n';
                    });
                  },
                ),
                OutlineButton(
                  child: Icon(Icons.close),
                  onPressed: () {
                    setState(() {
                      _string = '';
                    });
                  },
                )
              ],
            )
          ],
        ),
      ),
    );
  }

效果:

當我們點選區域性重新整理s1,執行s1builds1不相等,s2相等不重新整理。輸出:

flutter: s2:5 5 資料相等,本次不重新整理
flutter: s1:6 7 不相等,本次重新整理
flutter: Selector 1 重新整理
flutter: Consumer 0 重新整理

當點選s2,s2的值不相等重新整理UI,s1資料相等,不重新整理UI.

flutter: s2:2 3 不相等,本次重新整理
flutter: selector 2 重新整理
flutter: s1:0 0 資料相等,本次不重新整理
flutter: Consumer 0 重新整理

可以看到上邊2次Consumer每次都重新整理了,我們探究下原因。

Consumer 全域性重新整理

Consumer繼承了SingleCHildStatelessWidget,當我們在ViewModel中呼叫notification則當前widget被標記為dirty,然後在build中執行傳入的builder函式,在下幀則會重新整理UI

Selector<T,S>則被標記dirty時執行_Selector0State中的buildWithChild(ctx,child)函式時,根據selected_shouldRebuild來判斷是否需要執行widget.builder(ctx,selected,child)(重新整理UI).

其他用法

多model寫法

只需要在所有需要model的上級包裹即可,當我們一個page需要2model的時候,我麼通常這樣子寫:

class BaseProviderRoute extends StatelessWidget {
  BaseProviderRoute({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<ProviderModel>(
          create: (_) => ProviderModel(),
        ),
        ChangeNotifierProvider<ProviderModel2>(create: (_) => ProviderModel2()),
      ],
      child: BaseProvider(),
    );
  }
}

當然是用的時候和單一model一致的。

Selector<ProviderModel2, int>(
    selector: (context, model) => model.value,
    builder: (ctx, value, child) {
      print('model2 s1 重新整理');
      _string += 'm2s1 ';
      return Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Selector<Model2,int>次數:' + value.toString()),
          OutlineButton(
            onPressed: () {
              ctx.read<ProviderModel2>().add(2);
            },
            child: Icon(Icons.add),
          )
        ],
      );
    },
  ),

watch && read

watch原始碼是Provider.of<T>(this),預設Provider.of<T>(this)listen=true.

static T of<T>(BuildContext context, {bool listen = true}){
final inheritedElement = _inheritedElementOf<T>(context);

    if (listen) {
      context.dependOnInheritedElement(inheritedElement);
    }

    return inheritedElement.value;
}

read原始碼是Provider.of<T>(this, listen: false),watch/read只是寫法簡單一點,並無高深結構。

當我們想要監聽值的變化則是用watch,當想呼叫model的函式時則使用read

參考

  • 程式碼倉庫: https://github.com/ifgyong/flutter-example
  • 官方Provider: https://github.com/rrousselGit/provider