Flutter Provider 3.0實戰教程

Efox發表於2019-07-15

背景

provider是Google I/O 2019大會宣佈的現在官方推薦的狀態管理方式, provider,語法糖是InheritedWidget,它允許在小部件樹中傳遞資料,允許我們更加靈活地處理資料型別和資料。

專案地址

flutter_provider 本教程的專案原始碼,歡迎star

為什麼需要狀態管理

在進行專案的開發時,我們往往需要管理不同頁面之間的資料共享,在頁面功能複雜,狀態達到幾十個上百個的時候,我們會難以清楚的維護我們的資料狀態,本文將以簡單計數器功能使用狀態管理來講解如何在Flutter中使用provider這個狀態管理框架

為什麼選擇Provider

上次為大家介紹了provide,然後provide就被棄用了,不過要從provide轉provider學習成本也不高,要了解provide可以轉Flutter UI使用Provide實現主題切換

使用Provider訪問資料有兩種方式

  • 使用Provider.of(context),簡單易用,但是要資料發生變化時,會進行頁面級別rebuild,相當於stfulWidget
  • 使用Consumer,Consumer比Provider.of(context)複雜一點,但是對於app效能的提高卻有些很好的作用,當狀態發生變化時,widget樹會更新指定的節點,極小程度進行控制元件重新整理,不會進行整顆widget樹的更新,詳細看下文分析。
  • Provider有泛型的優勢,相當於namespace的特性,使用過vuex的應該知道namespace的重要性,它將我們的狀態分離開來

專案地址

flutter-provider, 可參考專案中使用provider方法

效果

FistPage

SecondPage

如何使用

新增依賴

檢視 pub-install

  • 在pubspec.yaml中引入依賴
dependencies:
      provider: 3.0.0+1 #資料管理層
複製程式碼
  • 執行
flutter packages get
複製程式碼
  • 在需要使用的頁面中引入
import 'package:provider/provider.dart'
複製程式碼

建立model (這才第一步)

新建 lib/store/object/CounterInfo.dart 檔案

新建 lib/store/object/UserInfo.dart 檔案

資料模型,就不貼出程式碼了

新建 lib/store/model/CounterModel.dart 檔案

import 'package:flutter/foundation.dart' show ChangeNotifier;
import '../object/CounterInfo.dart';
export '../object/CounterInfo.dart';

class Counter extends CounterInfo with ChangeNotifier {
  CounterInfo _counterInfo = CounterInfo(count: 0, totalInfo: TotalInfo(total: 2));

  int get count => _counterInfo.count;
  TotalInfo get totalInfo => _counterInfo.totalInfo;

  void increment () {
    _counterInfo.count++;
    notifyListeners();
  }

  void decrement () {
    _counterInfo.count--;
    notifyListeners();
  }
}
複製程式碼

新建 lib/store/model/UserModelModel.dart 檔案

import 'package:flutter/foundation.dart' show ChangeNotifier;
import '../object/UserInfo.dart';
export '../object/UserInfo.dart';

class UserModel extends UserInfo with ChangeNotifier {
  UserInfo _userInfo = UserInfo(name: '咕嚕貓不吃貓糧不吃魚');

  String get name => _userInfo.name;

  void setName (name) {
    _userInfo.name = name;
    notifyListeners();
  }
}
複製程式碼

通過mixin混入ChangeNotifier,通過notifyListeners通知聽眾重新整理

封裝Store (沒錯,到這裡已經要快完成所有步驟了)

新建 lib/store/index.dart 檔案

import 'package:flutter/material.dart' show BuildContext;
import 'package:provider/provider.dart'
  show ChangeNotifierProvider, MultiProvider, Consumer, Provider;
import 'model/index.dart' show Counter, UserModel;
export 'model/index.dart';
export 'package:provider/provider.dart';

class Store {
  static BuildContext context;
  static BuildContext widgetCtx;

  //  我們將會在main.dart中runAPP例項化init
  static init({context, child}) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(builder: (_) => Counter()),
        ChangeNotifierProvider(builder: (_) => UserModel(),)
      ],
      child: child,
    );
  }

  //  通過Provider.value<T>(context)獲取狀態資料
  static T value<T>(context) {
    return Provider.of(context);
  }

  //  通過Consumer獲取狀態資料
  static Consumer connect<T>({builder, child}) {
    return Consumer<T>(builder: builder, child: child);
  }
}

複製程式碼

需要管理多個狀態只需要在providers新增對應的狀態

providers: [ ChangeNotifierProvider(builder: () => Counter()), ChangeNotifierProvider(builder: () => UserModel(),) ],

定義全域性的Provide (倒數第二)

lib/main.dart 檔案

import 'package:flutter/material.dart';
import 'package:flutter_provider/store/index.dart' show Store;
import 'package:flutter_provider/page/firstPage.dart' show FirstPage;

void main () {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('根部重建: $context');
    return Store.init(
      context: context,
      child: MaterialApp(
        title: 'Provider',
        home: Builder(
          builder: (context) {
            Store.widgetCtx = context;
            print('widgetCtx: $context');
            return FirstPage();
          },
        ),
      )
    );
  }
}
複製程式碼

建立頁面 (完成)

新建 lib/page/firstPage.dart 檔案

import 'package:flutter/material.dart';
import 'package:flutter_provider/store/index.dart' show Store, Counter, UserModel;
import 'package:flutter_provider/page/secondPage.dart' show SecondPage;

class FirstPage extends StatelessWidget {
  TextEditingController controller = TextEditingController();
  @override
  Widget build(BuildContext context) {
    print('first page rebuild');
    return Scaffold(
      appBar: AppBar(title: Text('FirstPage'),),
      body: Center(
        child: Column(
          children: <Widget>[
            Store.connect<Counter>(
              builder: (context, snapshot, child) {
                return RaisedButton(
                  child: Text('+'),
                  onPressed: () {
                    snapshot.increment();
                  },
                );
              }
            ),
            Store.connect<Counter>(
              builder: (context, snapshot, child) {
                print('first page counter widget rebuild');
                return Text(
                  '${snapshot.count}'
                );
              }
            ),
            Store.connect<Counter>(
              builder: (context, snapshot, child) {
                return RaisedButton(
                  child: Text('-'),
                  onPressed: () {
                    snapshot.decrement();
                  },
                );
              }
            ),
            Store.connect<UserModel>(
              builder: (context, snapshot, child) {
                print('first page name Widget rebuild');
                return Text(
                  '${Store.value<UserModel>(context).name}'
                );
              }
            ),
            TextField(
              controller: controller,
            ),
            Store.connect<UserModel>(
              builder: (context, snapshot, child) {
                return RaisedButton(
                  child: Text('change name'),
                  onPressed: () {
                    snapshot.setName(controller.text);
                  },
                );
              }
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Center(
          child: Icon(Icons.group_work)
        ),
        onPressed: () {
          Navigator.of(context)
            .push(MaterialPageRoute(builder: (BuildContext context) {
              return SecondPage();
          }));
          // Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) {
          //   return SecondPage();
          // }));
        },
      ),
    );
  }
}
複製程式碼

新建 lib/page/secondPage.dart 檔案

import 'package:flutter/material.dart';
import 'package:flutter_provider/store/index.dart' show Store, Counter, UserModel;

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('second page rebuild');
    return Scaffold(
      appBar: AppBar(title: Text('SecondPage'),),
      body: Center(
        child: Column(
          children: <Widget>[
            RaisedButton(
              child: Text('+'),
              onPressed: () {
                Store.value<Counter>(context).increment();
              },
            ),
            Builder(
              builder: (context) {
                print('second page counter widget rebuild');
                return Text(
                  'second page: ${Store.value<Counter>(context).count}'
                );
              },
            ),
            RaisedButton(
              child: Text('-'),
              onPressed: () {
                Store.value<Counter>(context).decrement();
              },
            ),
          ],
        ),
      ),
    );
  }
}
複製程式碼

細心的同學可以發現我在firstPage中使用獲取資料狀態全部都是通過Consumer來獲取的,在firstPage中使用了兩個store(Counter和UserModel)繫結了兩個不同的weiget,好處就在於:

  • 我通過+或-進行資料修改時,只會對使用Counter資料模型的widget進行更新,通過點選change name按鈕時修改了UserModel中的name,也只會對使用了UserModel的weiget進行更新
    • firstPage中在build中進行了print('first page rebuild');
    • 在顯示數量的weiget中進行了print('first page counter widget rebuild');
    • 在顯示暱稱的weiget中進行了print('first page name Widget rebuild');

結果是first page rebuild只會在頁面初始化的時候進行列印,而運算元據增減和name修改只會重新渲染對應的weiget,下圖分別為單獨進行一次資料修改和name修改後的控制檯輸出

firstPage的print

  • 在secondPage中對於資料的操作我通過Provider.value(context)獲取,使用較為方便簡單,但是資料改變時,會發生頁面級別重新整理
    • secondPage中build進行了print('second page rebuild');
    • 在顯示數量的weiget中進行了print('second page counter widget rebuild');

結果是second page rebuild會在頁面初始化的時候進行列印,但每次資料修改時同樣也會進行print

secondPage的print

綜上,使用Provider.value(context)會導致頁面重新整理,雖然flutter會自動優化重新整理,但還是建議大家儘量使用Consumer去獲取資料,可以獲取最好app的效能提升

最後

歡迎更多學習flutter的小夥伴加入QQ群 Flutter UI: 798874340

敬請關注我們正在開發的:efoxTeam/flutter-ui

作者

相關文章