Flutter Provider使用指南

前言

  使用一種語言編寫各類應用的時候,橫亙在開發者面前的第一個問題就是如何進行狀態管理。在前端領域,咱們習慣使用框架或者各類輔助庫來進行狀態管理。例如,開發者常用react自帶的context,或者mobx/redux等工具來管理組件間狀態。在大熱的跨端框架flutter中,筆者將對社區中使用普遍的provider框架進行介紹。前端

準備工做

安裝與引入

provider pub連接
官方文檔宣稱(本文基於4.0版本),provider是一個依賴注入和狀態管理的混合工具,經過組件來構建組件。
provider有如下三個特色:react

  1. 可維護性,provider強制使用單向數據流
  2. 易測性/可組合性,provider能夠很方便地模擬或者複寫數據
  3. 魯棒性,provider會在合適的時候更新組件或者模型的狀態,下降錯誤率

在pubspec.yaml文件中加入以下內容:git

dependencies:
  provider: ^4.0.0
複製代碼

而後執行命令flutter pub get,安裝到本地。 使用時只需在文件頭部加上以下內容:github

import 'package:provider/provider.dart';
複製代碼

暴露一個值

若是咱們想讓某個變量可以被一個widget及其子widget所引用,咱們須要將其暴露出來,典型寫法以下:redux

Provider(
  create: (_) => new MyModel(),
  child: ...
)
複製代碼

讀取一個值

若是要使用先前暴露的對象,能夠這樣操做api

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

暴露和使用多個值(MultiProvider)

Provider的構造方法能夠嵌套使用bash

Provider<Something>(
  create: (_) => Something(),
  child: Provider<SomethingElse>(
    create: (_) => SomethingElse(),
    child: Provider<AnotherThing>(
      create: (_) => AnotherThing(),
      child: someWidget,
    ),
  ),
),
複製代碼

上述代碼看起來過於繁瑣,走入了嵌套地獄,好在provider給了更加優雅的實現app

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

代理provider(ProxyProvider)

在3.0版本以後,有一種新的代理provider可供使用,ProxyProvider可以將不一樣provider中的多個值整合成一個對象,並將其發送給外層provider,當所依賴的多個provider中的任意一個發生變化時,這個新的對象都會更新。下面的例子使用ProxyProvider來構建了一個依賴其餘provider提供的計數器的例子框架

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來應對具體的需求less

  • Provider 最基礎的provider,它會獲取一個值並將它暴露出來
  • ListenableProvider 用來暴露可監聽的對象,該provider將會監聽對象的改變以便及時更新組件狀態
  • ChangeNotifierProvider ListerableProvider依託於ChangeNotifier的一個實現,它將會在須要的時候自動調用ChangeNotifier.dispose方法
  • ValueListenableProvider 監聽一個可被監聽的值,而且只暴露ValueListenable.value方法
  • StreamProvider 監聽一個流,而且暴露出其最近發送的值
  • FutureProvider 接受一個Future做爲參數,在這個Future完成的時候更新依賴

項目實戰

接下來筆者將以本身項目來舉例provider的用法
首先定義一個基類,完成一些UI更新等通用工做

import 'package:provider/provider.dart';

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

  @override
  void notifyListeners() {
    Global.saveProfile(); //保存Profile變動
    super.notifyListeners();
  }
}
複製代碼

以後定義本身的數據類

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方法劫持對數據的獲取和修改,在有相關改動發生時通知組件樹同步狀態。
在主文件中,使用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便可,可是這樣的寫法看上去不夠精簡,每次調用時都須要寫很長的一段開頭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));
  }
}
複製代碼

項目完整代碼詳見本人倉庫

其餘相關細節和常見問題(來自官方文檔)

  1. 爲何在initState中獲取Provider會報錯?
    不要在只會調用一次的組件生命週期中調用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的過程當中,若是更新變量的值就會報出異常?
    這個頗有可能由於你在改變某個子組件的ChangeNotifier時,整個渲染樹還處在建立過程當中。
    比較典型的使用場景是notifier中存在http請求
initState() {
  super.initState();
  Provider.of<Foo>(context).fetchSomething();
}
複製代碼

這是不容許的,由於組件的更新是即時生效的。
換句話來講若是某些組件在異步過程以前構建,某些組件在異步過程以後構建,這頗有可能觸發你應用中的UI表現不一致,這是不容許的。
爲了解決這個問題,須要把你的異步過程放在可以等效的影響組件樹的地方

  • 直接在你provider模型的構造函數中進行異步過程
class MyNotifier with ChangeNotifier {
  MyNotifier() {
    _fetchSomething();
  }

  Future<void> _fetchSomething() async {}
}
複製代碼
  • 或者直接添加異步行爲
initState() {
  super.initState();
  Future.microtask(() =>
    Provider.of<Foo>(context).fetchSomething(someValue);
  );
}
複製代碼
  1. 爲了同步複雜的狀態,我必須使用ChangeNotifier嗎?
    並非,你可使用一個對象來表示你的狀態,例如把Provider.value()StatefulWidget結合起來使用,達到即刷新狀態又同步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,
      ),
    );
  }
}
複製代碼

當須要讀取狀態時:

return Text(Provider.of<int>(context).toString());
複製代碼

當須要改變狀態時:

return FloatingActionButton(
  onPressed: Provider.of<ExampleState>(context).increment,
  child: Icon(Icons.plus_one),
);
複製代碼
  1. 我能夠封裝我本身的Provider麼?
    能夠,provider暴露了許多細節api以便使用者封裝本身的provider,它們包括:SingleChildCloneableWidgetInheritedProviderDelegateWidgetBuilderDelegateValueDelegate
  2. 個人組件重建得過於頻繁,這是爲何?
    可使用Provider.of來替代Consumer/Selector.
    可使用可選的child參數來保證組件樹只會重建某個特定的部分
Foo(
  child: Consumer<A>(
    builder: (_, a, child) {
      return Bar(a: a, child: child);
    },
    child: Baz(),
  ),
)
複製代碼

在以上例子中,當A改變時,只有Bar會從新渲染,FooBaz並不會進行沒必要要的重建。
爲了更精細地控制,咱們還可使用Selector來忽略某些不會影響組件數的改變。

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

在這個例子中,組件只會在list的長度發生改變時纔會從新渲染,其內部元素改變時並不會觸發重繪。

  1. 我可使用兩個不一樣的provider來獲取同一個類型的值嗎? 不能夠,哪怕你給多個provider定義了同一個類型,組件也只能獲取距離其最近的一個父組件中的provider的值.
相關文章
相關標籤/搜索