Flutter源碼系列之《二》淺談Flutter的狀態管理庫Provider

轉載請註明出處:https://blog.csdn.net/llew2011/article/details/105450640

Flutter開發過程中一個常見的問題就是狀態管理,所謂狀態管理就是管理Flutter的Widget狀態,對於Flutter的狀態管理,社區上已有多種成熟的方案:ProviderReduxMobXBLoC等。在這些方案裏Google建議我們使用Provider,接下來我們就學習下Provider,看它是如何做到的狀態管理,在瞭解其原理之前,我們先看下它的使用。

Provider的安裝

首先創建Flutter項目providerDemo,創建完畢後在根目錄下找到pubspec.yaml文件,在dependencies下追加provider依賴,如下所示:

name: flutter_provider
description: A new Flutter application.

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2
  provider: 3.2.0					// 添加privider的依賴,注意格式

dev_dependencies:
  flutter_test:
    sdk: flutter

pubspec.yaml是flutter的配置文件,它與Android項目中的build.gradle功能類似,添加完依賴後狀態欄會彈出4個能選項:Package get , Package upgrade, Flutter upgrade以及Flutter doctor,直接點擊Packages get就會自動加載依賴,截圖如下:

Provider的使用

假設我們有三個頁面,他們分別是A頁面B頁面和C頁面,其中A頁面是主頁面(Flutter項目啓動後最先展示的頁面),它展示了一個用戶的姓名和年齡,然後從A頁面跳轉到B頁面,在B頁面可以修改用戶的姓名,之後又從B頁面跳轉到C頁面並在C頁面又修改了用戶的年齡,修改完成之後直接返回到A頁面,返回到A頁面後應該展示的是修改後的值。這個小功能在Android中能很容易實現,接下來我們看看在Flutter中該如何做。

創建UserModel

A頁面展示的是用戶姓名和年齡,我們可以定義一個UserModel(有用戶名和年齡屬性),然後在B頁面和C頁面分別修改UserModel的姓名和年齡(提供修改姓名和年齡的方法),修改後可以觸發頁面更新從而使A頁面展示更新後的值。UserModel定義如下:

class UserModel with ChangeNotifier {

  String _name;
  int _age;

  UserModel(this._name, this._age);

  void updateName(String name) {
    this._name = name;
    notifyListeners();
  }

  void updateAge(int age) {
    this._age = age;
    notifyListeners();
  }

  get name => _name;

  get age => _age;
}

UserModel定義了_name 和 _age私有屬性並對外提供了updateName()和updateAge()方法,在這些updateXXX()方法內調用了notifyListeners()方法,notifyListeners()方法是ChangeNotifier類中提供的,ChangeNotifier是Flutter提供的具有觀察者功能的類,UserModel使用with關鍵字表示它具有ChangeNotifier的所有功能,我們稍微看下ChangeNotifier的源碼,如下所示:

class ChangeNotifier implements Listenable {
  // 監聽器容器
  ObserverList<VoidCallback> _listeners = ObserverList<VoidCallback>();

  @override
  void addListener(VoidCallback listener) {
    // 添加監聽器
  }

  @override
  void removeListener(VoidCallback listener) {
    // 移除監聽器
  }

  void notifyListeners() {
    // 通知所有監聽器,觸發回調
  }
}

ChangeNotifier實現了Listenable接口,它對外提供了注入監聽器,移除監聽器以及觸發監聽器回調等功能,因爲UserModel使用了with關鍵字聚合了ChangeNotifier的這些功能,所以UserModel本質上也是一個具有觀察者功能的類。

注入UserModel

創建完UserModel後,使用Provider庫提供的ChangeNotifierProvider類給當前應用注入一個UserModel實例,注入之後就可以在其它頁面使用該實例。在mian()方法做如下修改:

void main() {
  // runApp(MyApp());  // 註釋掉runApp()方法,修改如下
  
  var newWidget = ChangeNotifierProvider.value(
    value: UserModel("張三", 22),
    child: MyApp(),
  );
  runApp(newWidget);
}

main()方法是flutter的入口,該方法內僅調用了runApp()方法,runApp()方法接收一個Widget類型的參數,我們使用ChangeNotifierProvider的命名構造方法value()創建一個newWidget實例並把newWidget實例傳給了runApp()方法,在構造newWidget實例時給newWidget傳遞了一個UserModel實例和MyApp實例。修改完main()方法後我們開始創建A、B、C三個頁面。

使用UserModel

首先創建主頁面A,在A頁面中展示UserModel的name和age,如下所示:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'A頁面'),
      navigatorObservers: <NavigatorObserver>[
        GlobalNavigatorObserver(),
      ],
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends BaseState<MyHomePage> {

  @override
  Widget buildContent(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("用戶名:${Provider.of<UserModel>(context).name}"),// 使用UserModel的name值
            Text("年  齡: ${Provider.of<UserModel>(context).age}"),// 使用UserModel的age值
            RaisedButton(
              child: Text("打開設置用戶名頁面"),
              // 跳轉到B頁面
              onPressed: () => NavigatorHelper.push(SettingNameWidget()),
            ),
          ],
        ),
      ),
    );
  }
}

A頁面用兩個文本框和一個按鈕(文本框用來展示用戶名和年齡,按鈕用來做頁面跳轉),展示姓名和年齡時是通過Provider提供的of()靜態方法獲取到了剛剛注入的UserModel實例然後分別獲取姓名和年齡值。當點擊了按鈕後頁面會跳轉打B頁面(NavigatorHelper是封裝的一個不依賴BuildContext就可以進行頁面跳轉的輔助類)。B頁面佈局如下:

class SettingNameWidget extends StatefulWidget {

  @override
  State createState() {
    return _SettingNameState();
  }
}

class _SettingNameState extends State<SettingNameWidget> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("設置用戶名頁面"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("設置用戶名"),
            TextField(
              showCursor: true,
              onChanged: (value) {
                // 更新name
                Provider.of<UserModel>(context).updateName(value);
              },
            ),
            RaisedButton(
              child: Text("打開設置年齡頁面"),
              onPressed: () {
                // 跳轉到C頁面
                NavigatorHelper.push(SettingAgeWidget());
              },
            ),
          ],
        ),
      ),
    );
  }
}

B頁面包含了一個輸入框和按鈕,在輸入數據後會調用Provider的of()方法獲取UserModel實例並設置UserModel的name值,點擊按鈕後跳轉打C頁面,C頁面佈局如下:

class SettingAgeWidget extends StatefulWidget {

  @override
  State createState() {
    return _SettingAgeState();
  }
}

class _SettingAgeState extends State<SettingAgeWidget> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("設置年齡頁面"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("設置年齡"),
            TextField(
              showCursor: true,
              keyboardType: TextInputType.number,
              onChanged: (value) {
                // 更新年齡
                Provider.of<UserModel>(context).updateAge(int.parse(value));
              },
            ),
            RaisedButton(
              child: Text("返回首頁"),
              onPressed: () => NavigatorHelper.popUntil(ModalRoute.withName("/")),
            )
          ],
        ),
      ),
    );
  }
}

C頁面添加了一個輸入框和一個按鈕,當輸入框中的數據改變後會修改UserModel的age值,最後點擊按鈕返回到A頁面,此時A頁面就顯示的是修改後的名字和年齡,演示如下:

以上就是Provider的簡單使用,通過這個示例向小夥伴們展示瞭如何使用Provider,接下來我給小夥伴們介紹一下Provider的工作原理,看看Provider是如何做到跨頁面共享狀態的。

Provider的原理

根據剛纔的示例我們知道main.dart的main()方法調用了runApp()方法,而runApp()方法接收一個Widget類型的參數,通過使用ChangeNotifierProvider的命名構造方法value()生成了一個ChangeNotifierProvider實例newWidget然後把newWidget傳遞給了runApp()方法,那也就是說ChangeNotifierProvider一定是Widget的子類。我們就看下ChangeNotifierProvider的源碼,如下所示:

class ChangeNotifierProvider<T extends ChangeNotifier>
    extends ListenableProvider<T> implements SingleChildCloneableWidget {
  // 省略相關代碼
  ChangeNotifierProvider.value({
    Key key,
    @required T value,
    Widget child,
  }) : super.value(key: key, value: value, child: child);
}

ChangeNotifierProvider繼承了ListenableProvider,它除了定義了兩個構造方法外,什麼都沒有做,繼續看下它的父類ListenableProvider,源碼如下:

class ListenableProvider<T extends Listenable> extends ValueDelegateWidget<T>
    implements SingleChildCloneableWidget {
  // 包含一個Widget實例,該實例是剛剛傳遞進來的MyApp
  final Widget child;

  // 省略相關代碼
  ListenableProvider.value({
    Key key,
    @required T value,
    Widget child,
  }) : this._(
    key: key,
    delegate: _ValueListenableDelegate(value), // 創建一個Delegate
    child: child,
  );
  
  // 重寫了build()方法,返回InheritedProvider實例
  @override
  Widget build(BuildContext context) {
    final delegate = this.delegate as _ListenableDelegateMixin<T>;
    return InheritedProvider<T>(
      value: delegate.value,
      updateShouldNotify: delegate.updateShouldNotify,
      child: child, // 把child傳遞給InheritedProvider
    );
  }
}

ListenableProvider繼承了ValueDelegateWidget也提供了一個value()命名構造方法並重寫了build()方法,它又定義了一個Widget類型的child屬性,因爲我們在創建ChangeNotifierProvider的時候傳遞的child是MyApp實例,所以當前的child就是MyApp實例。ListenableProvider的build()方法返了一個InheritedProvider實例,在創建InheritedProvider實例的時候把child傳遞給了InheritedProvider。我們繼續看InheritedProvider的源碼,如下所示:

class InheritedProvider<T> extends InheritedWidget {
  const InheritedProvider({
    Key key,
    @required T value,
    UpdateShouldNotify<T> updateShouldNotify,
    Widget child,
  })  : _value = value,
        _updateShouldNotify = updateShouldNotify,
        super(key: key, child: child);// 把child傳遞給了InheritedWidget,也就是把MyApp傳遞給了InheritedWidget

  final T _value; // _value就是UserModel實例
  final UpdateShouldNotify<T> _updateShouldNotify;

  @override
  bool updateShouldNotify(InheritedProvider<T> oldWidget) {
    if (_updateShouldNotify != null) {
      return _updateShouldNotify(oldWidget._value, _value);
    }
    return oldWidget._value != _value;
  }
}

通過InheritedProvider的源碼我們發現它繼承自InheritedWidgetInheritedWidget 是Flutter中比較重要的一個功能型Widget,它提供了一種數據可以在Widget樹中從上到下傳遞、共享的方式,比如我們在應用的根Widget中通過InheritedWidget共享了一個數據,那麼我們便可以在任意的子Widget中來獲取該共享數據,Provider的原理恰好是通過該特性實現了狀態的跨組件共享(#^.^#)。這也是Provider要求我們在main()方法做修改的目的,就是把根Widget(MyApp)替換成InheritedWidget,然後把MyApp作爲InheritedWidget的子Widget,操作過程如下所示:

到這裏我們已經知道了Provider是通過InheritedWidget實現了狀態跨Widget共享,那麼它是如何觸發頁面更新的呢?繼續看觸發頁面更新的時機,當調用了UserModel的setXXX()方法後會觸發notifyListeners()方法,notifyListeners()方法是在ChangeNotifier中定義的,源碼如下:

void notifyListeners() {
    if (_listeners != null) {
      final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
      for (VoidCallback listener in localListeners) {
        try {
          // 循環遍歷監聽器,並執行監聽器的回調函數listener
          if (_listeners.contains(listener))
            listener();
        } catch (exception, stack) {
        }
      }
    }
  }

notifyListeners()方法會循環遍歷注入的監聽器並執行其回調,那麼這些監聽器是什麼時候注入的呢?還記得ListenableProvider的value()命名構造方法嗎?它內部在調用私有構造方法的時候傳遞了一個_ValueListenableDelete實例,代碼如下:

ListenableProvider.value({
  Key key,
  @required T value,
  Widget child,
}) : this._(
      key: key,
      delegate: _ValueListenableDelegate(value), // 傳遞了一個_ValueListenableDelegate
      child: child,// MyApp實例
    );

// _ValueListenableDelegate源碼如下:
class _ValueListenableDelegate<T extends Listenable>
    extends SingleValueDelegate<T> with _ListenableDelegateMixin<T> {
  _ValueListenableDelegate(T value, [this.disposer]) : super(value);

  @override
  void didUpdateDelegate(_ValueListenableDelegate<T> oldDelegate) {
    super.didUpdateDelegate(oldDelegate);
    if (oldDelegate.value != value) {
      _removeListener?.call();
      oldDelegate.disposer?.call(context, oldDelegate.value);
      // 調用startListening()方法
      if (value != null) startListening(value, rebuild: true);
    }
  }

  @override
  void startListening(T listenable, {bool rebuild = false}) {
  	// 調用父類的startListening()方法
    super.startListening(listenable, rebuild: rebuild);
  }
}

// _ValueListenableDelegate的父類startListening方法如下:
void startListening(T listenable, {bool rebuild = false}) {
  var buildCount = 0;
  final setState = this.setState;
  // listener是一個函數,當執行listener的時候會執行setState()方法
  final listener = () => setState(() => buildCount++);

  var capturedBuildCount = buildCount;
  if (rebuild) capturedBuildCount--;
  updateShouldNotify = (_, __) {
    final res = buildCount != capturedBuildCount;
    capturedBuildCount = buildCount;
    return res;
  };

  // 添加監聽器,此時參數listenable就是UserModel
  // 調用UserModel的addListener()方法把listener函數注入
  listenable.addListener(listener);
  _removeListener = () {
    listenable.removeListener(listener);
    _removeListener = null;
    updateShouldNotify = null;
  };
}

通過對_ValueListenableDelete的執行流程分析,我們知道在_ValueListenableDelegate的didUpdateDelegate()方法內部startListening()方法,而startListening()方法最終會把setState()函數封裝成參數添加進UserModel的監聽器容器中,當調用UserModel的notifyListeners()方法時會循環遍歷監聽器容器並間接執行到setState()方法,而setState()方法是Flutter Framework層提供的刷新頁面的函數,所以只要UserModel的狀態發生改變都會觸發setState()函數從而刷新頁面。

以上就是Provider的狀態管理,它的原理就是合理利用了InheritedWidget特性。由於篇幅原因,Provider的of()靜態函數是如何獲取到注入的UserModel實例的,這個很簡單就不再進行源碼分析了。感興趣的小夥伴可以自行分析(#^.^#)

或許有的小夥伴有疑問,應用開發中我們不僅僅是用戶信息需要全局共享,假如其它信息也需要狀態共享,那該怎麼辦呢?Provider庫的開發者早已考慮到了多狀態共享的情況,遇見多狀態共享只需要簡單的使用MultiProvider就行了,具體用法可參考Provider的WiKi

另外畫了一下Provider的流程圖:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章