Flutter Provider使用指南

前言

??使用一種語言編寫各種應(yīng)用的時候,橫亙在開發(fā)者面前的第一個問題就是如何進(jìn)行狀態(tài)管理。在前端領(lǐng)域渔扎,我們習(xí)慣使用框架或者各種輔助庫來進(jìn)行狀態(tài)管理。例如信轿,開發(fā)者經(jīng)常使用react自帶的context,或者mobx/redux等工具來管理組件間狀態(tài)晃痴。在大熱的跨端框架flutter中,筆者將對社區(qū)中使用廣泛的provider框架進(jìn)行介紹财忽。

準(zhǔn)備工作

安裝與引入

provider pub鏈接
官方文檔宣稱(本文基于4.0版本)倘核,provider是一個依賴注入和狀態(tài)管理的混合工具,通過組件來構(gòu)建組件即彪。
provider有以下三個特點(diǎn):

  1. 可維護(hù)性紧唱,provider強(qiáng)制使用單向數(shù)據(jù)流
  2. 易測性/可組合性,provider可以很方便地模擬或者復(fù)寫數(shù)據(jù)
  3. 魯棒性,provider會在合適的時候更新組件或者模型的狀態(tài)漏益,降低錯誤率

在pubspec.yaml文件中加入如下內(nèi)容:

dependencies:
  provider: ^4.0.0

然后執(zhí)行命令flutter pub get,安裝到本地蛹锰。
使用時只需在文件頭部加上如下內(nèi)容:

import 'package:provider/provider.dart';

暴露一個值

如果我們想讓某個變量能夠被一個widget及其子widget所引用,我們需要將其暴露出來绰疤,典型寫法如下:

Provider(
  create: (_) => new MyModel(),
  child: ...
)

讀取一個值

如果要使用先前暴露的對象铜犬,可以這樣操作

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    MyModel yourValue = Provider.of<MyModel>(context)
    return ...
  }
}

暴露和使用多個值(MultiProvider)

Provider的構(gòu)造方法可以嵌套使用

Provider<Something>(
  create: (_) => Something(),
  child: Provider<SomethingElse>(
    create: (_) => SomethingElse(),
    child: Provider<AnotherThing>(
      create: (_) => AnotherThing(),
      child: someWidget,
    ),
  ),
),

上述代碼看起來過于繁瑣,走入了嵌套地獄峦睡,好在provider給了更加優(yōu)雅的實(shí)現(xiàn)

MultiProvider(
  providers: [
    Provider<Something>(create: (_) => Something()),
    Provider<SomethingElse>(create: (_) => SomethingElse()),
    Provider<AnotherThing>(create: (_) => AnotherThing()),
  ],
  child: someWidget,
)

代理provider(ProxyProvider)

在3.0版本之后翎苫,有一種新的代理provider可供使用,ProxyProvider能夠?qū)⒉煌琾rovider中的多個值整合成一個對象榨了,并將其發(fā)送給外層provider煎谍,當(dāng)所依賴的多個provider中的任意一個發(fā)生變化時,這個新的對象都會更新龙屉。下面的例子使用ProxyProvider來構(gòu)建了一個依賴其他provider提供的計數(shù)器的例子

Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (_) => Counter()),
      ProxyProvider<Counter, Translations>(
        create: (_, counter, __) => Translations(counter.value),
      ),
    ],
    child: Foo(),
  );
}

class Translations {
  const Translations(this._value);

  final int _value;

  String get title => 'You clicked $_value times';
}

各種provider

可以通過各種不同的provider來應(yīng)對具體的需求

  • Provider 最基礎(chǔ)的provider,它會獲取一個值并將它暴露出來
  • ListenableProvider 用來暴露可監(jiān)聽的對象呐粘,該provider將會監(jiān)聽對象的改變以便及時更新組件狀態(tài)
  • ChangeNotifierProvider ListerableProvider依托于ChangeNotifier的一個實(shí)現(xiàn),它將會在需要的時候自動調(diào)用ChangeNotifier.dispose方法
  • ValueListenableProvider 監(jiān)聽一個可被監(jiān)聽的值转捕,并且只暴露ValueListenable.value方法
  • StreamProvider 監(jiān)聽一個流作岖,并且暴露出其最近發(fā)送的值
  • FutureProvider 接受一個Future作為參數(shù),在這個Future完成的時候更新依賴

項(xiàng)目實(shí)戰(zhàn)

接下來筆者將以自己項(xiàng)目來舉例provider的用法
首先定義一個基類五芝,完成一些UI更新等通用工作

import 'package:provider/provider.dart';

class ProfileChangeNotifier extends ChangeNotifier {
  Profile get _profile => Global.profile;

  @override
  void notifyListeners() {
    Global.saveProfile(); //保存Profile變更
    super.notifyListeners();
  }
}

之后定義自己的數(shù)據(jù)類

class UserModle extends ProfileChangeNotifier {
  String get user => _profile.user;
  set user(String user) {
    _profile.user = user;
    notifyListeners();
  }

  bool get isLogin => _profile.isLogin;
  set isLogin(bool value) {
    _profile.isLogin = value;
    notifyListeners();
  }

  String get avatar => _profile.avatar;
  set avatar(String value) {
    _profile.avatar = value;
    notifyListeners();
  }

這里通過setget方法劫持對數(shù)據(jù)的獲取和修改痘儡,在有相關(guān)改動發(fā)生時通知組件樹同步狀態(tài)。
在主文件中枢步,使用provider

class MyApp extends StatelessWidget with CommonInterface {

  MyApp({Key key, this.info}) : super(key: key);
  final info;
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    UserModle newUserModel = new UserModle();
    return MultiProvider(
      providers: [
        //  用戶信息
        ListenableProvider<UserModle>.value(value: newUserModel),
      ],
      child: ListenContainer(),
    );
  }
}

接下來沉删,在所有的子組件中,如果需要使用用戶的名字醉途,只需Provider.of<UserModle>(context).user即可矾瑰,但是這樣的寫法看上去不夠精簡,每次調(diào)用時都需要寫很長的一段開頭Provider.of<xxx>(context).XXX很是繁瑣隘擎,故而這里我們可以簡單封裝一個抽象類:

abstract class CommonInterface {
  String cUser(BuildContext context) {
    return Provider.of<UserModle>(context).user;
  }
}

在子組件聲明時殴穴,使用with,來簡化代碼

class MyApp extends StatelessWidget with CommonInterface {
  ......
}

在使用時只需cUser(context)即可货葬。

class _FriendListState extends State<FriendList> with CommonInterface {
  @override
  Widget build(BuildContext context) {
    return Text(cUser(context));
  }
}

項(xiàng)目完整代碼詳見本人倉庫

其他相關(guān)細(xì)節(jié)和常見問題(來自官方文檔)

  1. 為什么在initState中獲取Provider會報錯?
    不要在只會調(diào)用一次的組件生命周期中調(diào)用Provider,比如如下的使用方法是錯誤的
initState() {
  super.initState();
  print(Provider.of<Foo>(context).value);
}

要解決這個問題采幌,要么使用其他生命周期方法(didChangeDependencies/build)

didChangeDependencies() {
  super.didChangeDependencies();
  final value = Provider.of<Foo>(context).value;
  if (value != this.value) {
    this.value = value;
    print(value);
  }
}

或者指明你不在意這個值的更新,比如

initState() {
  super.initState();
  print(Provider.of<Foo>(context, listen: false).value);
}
  1. 我在使用ChangeNotifier的過程中震桶,如果更新變量的值就會報出異常?
    這個很有可能因?yàn)槟阍诟淖兡硞€子組件的ChangeNotifier時休傍,整個渲染樹還處在創(chuàng)建過程中。
    比較典型的使用場景是notifier中存在http請求
initState() {
  super.initState();
  Provider.of<Foo>(context).fetchSomething();
}

這是不允許的尼夺,因?yàn)榻M件的更新是即時生效的尊残。
換句話來說如果某些組件在異步過程之前構(gòu)建,某些組件在異步過程之后構(gòu)建淤堵,這很有可能觸發(fā)你應(yīng)用中的UI表現(xiàn)不一致寝衫,這是不允許的。
為了解決這個問題拐邪,需要把你的異步過程放在能夠等效的影響組件樹的地方

  • 直接在你provider模型的構(gòu)造函數(shù)中進(jìn)行異步過程
class MyNotifier with ChangeNotifier {
  MyNotifier() {
    _fetchSomething();
  }

  Future<void> _fetchSomething() async {}
}
  • 或者直接添加異步行為
initState() {
  super.initState();
  Future.microtask(() =>
    Provider.of<Foo>(context).fetchSomething(someValue);
  );
}
  1. 為了同步復(fù)雜的狀態(tài)慰毅,我必須使用ChangeNotifier嗎?
    并不是,你可以使用一個對象來表示你的狀態(tài)扎阶,例如把Provider.value()StatefulWidget結(jié)合起來使用,達(dá)到即刷新狀態(tài)又同步UI的目的.
class Example extends StatefulWidget {
  const Example({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  ExampleState createState() => ExampleState();
}

class ExampleState extends State<Example> {
  int _count;

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

  @override
  Widget build(BuildContext context) {
    return Provider.value(
      value: _count,
      child: Provider.value(
        value: this,
        child: widget.child,
      ),
    );
  }
}

當(dāng)需要讀取狀態(tài)時:

return Text(Provider.of<int>(context).toString());

當(dāng)需要改變狀態(tài)時:

return FloatingActionButton(
  onPressed: Provider.of<ExampleState>(context).increment,
  child: Icon(Icons.plus_one),
);
  1. 我可以封裝我自己的Provider么?
    可以,provider暴露了許多細(xì)節(jié)api以便使用者封裝自己的provider,它們包括:SingleChildCloneableWidget牢裳、InheritedProvider参咙、DelegateWidgetBuilderDelegate惰赋、ValueDelegate
  2. 我的組件重建得過于頻繁宰掉,這是為什么?
    可以使用Provider.of來替代Consumer/Selector.
    可以使用可選的child參數(shù)來保證組件樹只會重建某個特定的部分
Foo(
  child: Consumer<A>(
    builder: (_, a, child) {
      return Bar(a: a, child: child);
    },
    child: Baz(),
  ),
)

在以上例子中,當(dāng)A改變時赁濒,只有Bar會重新渲染轨奄,FooBaz并不會進(jìn)行不必要的重建。
為了更精細(xì)地控制拒炎,我們還可以使用Selector來忽略某些不會影響組件數(shù)的改變挪拟。

Selector<List, int>(
  selector: (_, list) => list.length,
  builder: (_, length, __) {
    return Text('$length');
  }
);

在這個例子中,組件只會在list的長度發(fā)生改變時才會重新渲染击你,其內(nèi)部元素改變時并不會觸發(fā)重繪玉组。

  1. 我可以使用兩個不同的provider來獲取同一個類型的值嗎?
    不可以,哪怕你給多個provider定義了同一個類型果漾,組件也只能獲取距離其最近的一個父組件中的provider的值.
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末球切,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子绒障,更是在濱河造成了極大的恐慌吨凑,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件户辱,死亡現(xiàn)場離奇詭異鸵钝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)庐镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門恩商,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人必逆,你說我怎么就攤上這事怠堪±柯遥” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵粟矿,是天一觀的道長凰棉。 經(jīng)常有香客問我,道長陌粹,這世上最難降的妖魔是什么撒犀? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮掏秩,結(jié)果婚禮上或舞,老公的妹妹穿的比我還像新娘。我一直安慰自己蒙幻,他們只是感情好映凳,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著邮破,像睡著了一般魏宽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上决乎,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天队询,我揣著相機(jī)與錄音,去河邊找鬼构诚。 笑死蚌斩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的范嘱。 我是一名探鬼主播送膳,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼丑蛤!你這毒婦竟也來了叠聋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤受裹,失蹤者是張志新(化名)和其女友劉穎碌补,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棉饶,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厦章,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了照藻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袜啃。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖幸缕,靈堂內(nèi)的尸體忽然破棺而出群发,到底是詐尸還是另有隱情晰韵,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布熟妓,位于F島的核電站宫屠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏滑蚯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一抵栈、第九天 我趴在偏房一處隱蔽的房頂上張望告材。 院中可真熱鬧,春花似錦古劲、人聲如沸斥赋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疤剑。三九已至,卻和暖如春闷堡,著一層夾襖步出監(jiān)牢的瞬間隘膘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工杠览, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留弯菊,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓踱阿,卻偏偏與公主長得像管钳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子软舌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容