背景
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方法
效果
如何使用
新增依賴
檢視 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修改後的控制檯輸出
- 在secondPage中對於資料的操作我通過Provider.value(context)獲取,使用較為方便簡單,但是資料改變時,會發生頁面級別重新整理
- secondPage中build進行了print('second page rebuild');
- 在顯示數量的weiget中進行了print('second page counter widget rebuild');
結果是second page rebuild會在頁面初始化的時候進行列印,但每次資料修改時同樣也會進行print
綜上,使用Provider.value(context)會導致頁面重新整理,雖然flutter會自動優化重新整理,但還是建議大家儘量使用Consumer去獲取資料,可以獲取最好app的效能提升
最後
歡迎更多學習flutter的小夥伴加入QQ群 Flutter UI: 798874340
敬請關注我們正在開發的:efoxTeam/flutter-ui