Flutter Provider使用指南

廣蘭路地鐵發表於2019-12-28

前言

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

準備工作

安裝與引入

provider pub連結
官方文件宣稱(本文基於4.0版本),provider是一個依賴注入和狀態管理的混合工具,通過元件來構建元件。
provider有以下三個特點:

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

在pubspec.yaml檔案中加入如下內容:

dependencies:
  provider: ^4.0.0
複製程式碼

然後執行命令flutter pub get,安裝到本地。 使用時只需在檔案頭部加上如下內容:

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的構造方法可以巢狀使用

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

上述程式碼看起來過於繁瑣,走入了巢狀地獄,好在provider給了更加優雅的實現

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

  • 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的值.

相關文章