Flutter 中文文檔:簡單的應用狀態管理

2021-02-18 Flutter社區

現在大家已經了解了 狀態管理中的聲明式編程思維 和 短時 (ephemeral) 和應用 (app) 狀態的區別 之間的區別,現在可以學習如何管理簡單的全局應用狀態。

在這裡,我們打算使用 provider package。如果你是 Flutter 的初學者,而且也沒有很重要的理由必須選擇別的方式來實現(Redux、Rx、hooks 等等),那麼這就是你應該入門使用的。provider 非常好理解而且不需要寫很多代碼。它也會用到一些在其它實現方式中用到的通用概念。

即便如此,如果你已經從其它響應式框架上積累了豐富的狀態管理經驗的話,那麼可以在 狀態 (State) 管理參考 中找到相關的 package 和教程。

連結:https://flutter.cn/docs/development/data-and-backend/state-mgmt/options

1. 示例

為了演示效果,我們實現下面這個簡單應用。

程序有三個獨立的頁面:一個登陸提示,一個類別頁面,一個購物車頁面(分別用 MyLoginScreen, MyCatalog,MyCart widget 來展示)。雖然看上去是一個購物應用程式,但是你也可以和社交網絡應用類比(把類別頁面替換成朋友圈,把購物車替換成關注的人)。

類別頁面包含一個自定義的 app bar (MyAppBar) 以及一個包含元素列表的可滑動的視圖 (MyListItems)。

這是應用程式對應的可視化的 widget 樹。

所以我們有至少 6 個 Widget 的子類。他們中有很多需要訪問一些全局的狀態。比如,MyListItem 會被添加到購物車中。但是它可能需要檢查和自己相同的元素是否已經被添加到購物車中。

這裡我們出現了第一個問題:我們把當前購物車的狀態放在哪合適呢?

2. 提高狀態的層級

在 Flutter 中,有必要將存儲狀態的對象置於 widget 樹中對應 widget 的上層。

為什麼呢?在類似 Flutter 的聲明式框架中,如果你想要修改 UI,那麼你需要重構它。並沒有類似 MyCart.updateWith(somethingNew) 的簡單調用方法。換言之,你很難通過外部調用方法修改一個 widget。即便你自己實現了這樣的模式,那也是和整個框架不相兼容。


void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}

即使你實現了上面的代碼,也得處理 MyCart widget 中的代碼:


Widget build(BuildContext context) {
  return SomeWidget(
    
  );
}

void updateWith(Item item) {
  
}

你可能需要考慮當前 UI 的狀態,然後把最新的數據添加進去。但是這樣的方式很難避免出現 bug。

在 Flutter 中,每次當 widget 內容發生改變的時候,你就需要構造一個新的。你會調用 MyCart(contents)(構造函數),而不是 MyCart.updateWith(somethingNew)(調用方法)。因為你只能通過父類的 build 方法來構建新 widget,如果你想修改 contents,就需要調用 MyCart 的父類甚至更高一級的類。


void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}

這裡 MyCart 可以在各種版本的 UI 中調用同一個代碼路徑。


Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    
    
  );
}

在我們的例子中,contents會存在於 MyApp 的生命周期中。當它發生改變的時候,它會從上層重構 MyCart 。因為這個機制,所以 MyCart 無需考慮生命周期的問題—它只需要針對 contents 聲明所需顯示內容即可。當內容發生改變的時候,舊的 MyCart widget 就會消失,完全被新的 widget 替代。這就是我們所說的 widget 是不可變的。因為它們會直接被替換。現在我們知道在哪裡放置購物車的狀態,接下來看一下如何讀取該狀態。當用戶點擊類別頁面中的一個元素,它會被添加到購物車裡。然而當購物車在 widget 樹中,處於 MyListItem 的層級之上時,又該如何訪問狀態呢?一個簡單的實現方法是提供一個回調函數,當 MyListItem 被點擊的時候可以調用。Dart 的函數都是 first class 對象,所以你可以以任意方式傳遞它們。所以在 MyCatalog 裡你可以使用下面的代碼:

@override
Widget build(BuildContext context) {
  return SomeWidget(
    
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}

這段代碼是沒問題的,但是對於全局應用狀態來說你需要在不同的地方進行修改,可能需要大量傳遞迴調函數—。幸運的是 Flutter 在 widget 中存在一種機制,能夠為其子孫節點提供數據和服務。(換言之,不僅僅是它的子節點,所有在它下層的 widget 都可以)。就像你所了解的, Flutter 中的 Everything is a Widget™。這裡的機制也是一種 widget —InheritedWidget, InheritedNotifier, InheritedModel等等。我們這裡不會詳細解釋他們,因為這些 widget 都太底層。我們會用一個 package 來和這些底層的 widget 打交道,就是 provider package 。provider package 中,你無須關心回調或者 InheritedWidgets。但是你需要理解三個概念:ChangeNotifier 是 Flutter SDK 中的一個簡單的類。它用於向監聽器發送通知。換言之,如果被定義為 ChangeNotifier,你可以訂閱它的狀態變化。(這和大家所熟悉的觀察者模式相類似)。在 provider 中,ChangeNotifier 是一種能夠封裝應用程式狀態的方法。對於特別簡單的程序,你可以通過一個 ChangeNotifier 來滿足全部需求。在相對複雜的應用中,由於會有多個模型,所以可能會有多個 ChangeNotifier。(不是必須得把 ChangeNotifier 和 provider 結合起來用,不過它確實是一個特別簡單的類)。在我們的購物應用示例中,我們打算用 ChangeNotifier 來管理購物車的狀態。我們創建一個新類,繼承它,像下面這樣:

class CartModel extends ChangeNotifier {
  
  final List<Item> _items = [];

  

  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  
  int get totalPrice => _items.length * 42;

  
  void add(Item item) {
    _items.add(item);
    
    notifyListeners();
  }
}

唯一一行和 ChangeNotifier 相關的代碼就是調用 notifyListeners()。當模型發生改變並且需要更新 UI 的時候可以調用該方法。而剩下的代碼就是 CartModel 和它本身的業務邏輯。ChangeNotifier 是 flutter:foundation 的一部分,而且不依賴 Flutter 中任何高級別類。測試起來非常簡單(你都不需要使用 widget 測試)。比如,這裡有一個針對 CartModel 簡單的單元測試:

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash');
});

5. ChangeNotifierProviderChangeNotifierProvider widget 可以向其子孫節點暴露一個 ChangeNotifier 實例。它屬於 provider package。我們已經知道了該把 ChangeNotifierProvider 放在什麼位置:在需要訪問它的 widget 之上。在 CartModel 裡,也就意味著將它置於 MyCart 和 MyCatalog 之上。你肯定不願意把 ChangeNotifierProvider 放的級別太高(因為你不希望破壞整個結構)。但是在我們這裡的例子中,MyCart 和 MyCatalog 之上只有 MyApp。

void main() {
  runApp(
    ChangeNotifierProvider(
      builder: (context) => CartModel(),
      child: MyApp(),
    ),
  );
}

請注意我們定義了一個 builder 來創建一個 CartModel 的實例。ChangeNotifierProvider 非常聰明,它 不會 重複實例化 CartModel,除非在個別場景下。如果該實例已經不會再被調用,ChangeNotifierProvider 也會自動調用 CartModel 的 dispose() 方法。如果你想提供更多狀態,可以使用 MultiProvider:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(builder: (context) => CartModel()),
        Provider(builder: (context) => SomeOtherClass()),
      ],
      child: MyApp(),
    ),
  );
}

現在 CartModel 已經通過 ChangeNotifierProvider 在應用中與 widget 相關聯。我們可以開始調用它了。完成這一步需要通過 Consumer widget。

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);

我們必須指定要訪問的模型類型。在這個示例中,我們要訪問 CartModel 那麼就寫上 Consumer<CartModel>。Consumer widget 唯一必須的參數就是 builder。當 ChangeNotifier 發生變化的時候會調用 builder 這個函數。(換言之,當你在模型中調用 notifyListeners() 時,所有和 Consumer 相關的 builder 方法都會被調用。)builder 在被調用的時候會用到三個參數。第一個是 context。在每個 build 方法中都能找到這個參數。builder 函數的第二個參數是 ChangeNotifier 的實例。它是我們最開始就能得到的實例。你可以通過該實例定義 UI 的內容。第三個參數是 child,用於優化目的。如果 Consumer 下面有一個龐大的子樹,當模型發生改變的時候,該子樹 並不會 改變,那麼你就可以僅僅創建它一次,然後通過 builder 獲得該實例。

return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
        children: [
          
          child,
          Text("Total price: ${cart.totalPrice}"),
        ],
      ),
  
  child: SomeExpensiveWidget(),
);

最好能把 Consumer 放在 widget 樹儘量低的位置上。你總不希望 UI 上任何一點小變化就全盤重新構建 widget 吧。


return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      
      child: AnotherMonstrousWidget(
        
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);


return HumongousWidget(
  
  child: AnotherMonstrousWidget(
    
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);

有的時候你不需要模型中的 數據 來改變 UI,但是你可能還是需要訪問該數據。比如,ClearCart 按鈕能夠清空購物車的所有商品。它不需要顯示購物車裡的內容,只需要調用 clear() 方法。我們可以使用 Consumer<CartModel> 來實現這個效果,不過這麼實現有點浪費。因為我們讓整體框架重構了一個無需重構的 widget。所以這裡我們可以使用 Provider.of,並且將 listen 設置為 false。

Provider.of<CartModel>(context, listen: false).add(item);

在 build 方法中使用上面的代碼,當 notifyListeners 被調用的時候,並不會使 widget 被重構。連結:https://github.com/flutter/samples/tree/master/provider_shopper如果你想參考稍微簡單一點的示例,可以看看 Counter 應用程式是如何 基於 provider 實現的。連結:https://github.com/flutter/samples/tree/master/provider_counter如果你已經學會了並且準備使用 provider 的時候,別忘了先在 pubspec.yaml 中添加相應的依賴。連結:https://github.com/flutter/samples/tree/master/provider_counter

name: my_name
description: Blah blah blah.



dependencies:
  flutter:
    sdk: flutter

  provider: ^3.0.0

dev_dependencies:
  

現在你可以 import 'package:provider/provider.dart';,開始寫代碼吧。

相關焦點

  • Flutter狀態State的5種應對方法
    (https://github.com/chimon2000/flutter_state_five_ways)不管你是剛開始了解 Flutter,還是已經接觸 Flutter 有了一段時間,你都可能已經知道有很多方法可以處理應用程式狀態。我可以肯定,每個月都會冒出來一些新的途徑。因為沒有太多可以直接對比的例子,所以想要了解這些方法之間的差異和各自的權衡可能會很困難。
  • Flutter 中文文檔:Packages 的開發和提交
    連結:https://medium.com/flutter/writing-a-good-flutter-plugin-1a561b986c9c1.1 Package 類別Package 包含以下兩種類別:純 Dart 庫:用 Dart 編寫的傳統 package,比如 path。
  • Flutter 中文文檔:Widget 測試介紹
    在 Flutter 中文文檔:單元測試介紹 部分,我們學習了使用 test 這個 package 測試 Dart 類的方法。
  • Flutter教程從零構建電商應用(一)
    在這個系列中,我們將學習如何使用google的移動開發框架flutter創建一個電商應用。本文是flutter框架系列教程的第一部分,將學習如何安裝Flutter開發環境並創建第一個Flutter應用,並學習Flutter應用開發中的核心概念,例如widget、狀態等。
  • Flutter狀態管理:Provider4 入門教程(三
    舉個例子上面說了一堆無非是對官方文檔的羅列,我們說說具體應用。簡單說一下我們要實現的功能,十分簡單,有一個商品列表,當我們點擊某個商品的時候,商品會顯示加入購物車。運行一下,隨便點幾個商品,然後看一下日誌:I/flutter (29438): build selector 1I/flutter (29438): build item 0I/flutter (29438): build item 1I/flutter (29438): build item 2I/flutter (29438): build item
  • Flutter 中文文檔:定位到目標 widgets
    我們可以編寫自己的 finderclasses,不過通常使用 flutter_test 包提供的工具來定位 widgets 更加方便。下面,我們來看看 flutter_test 包提供的 find 常量並演示如何使用其所提供的 Finders。
  • Flutter 狀態管理指南篇 —— Provider
    in Flutter (Google I/O'19) 主題演講上正式介紹了 由社區作者 Remi Rousselet 與 Flutter Team 共同編寫的 Provider 代替 Provide 成為官方推薦的狀態管理方式之一。
  • Flutter 中文文檔:點擊、拖拽事件和文本輸入
    為了測試這些交互,我們需要在測試環境中模擬上述場景,可以藉助 flutter_test 庫中的 WidgetTester 類來實現。WidgetTester 提供了文本輸入、點擊、拖動的相關方法:在很多情況下,用戶交互會更新應用狀態。在測試環境中,Flutter 在狀態發生改變的時候並不會自動重建 Widget。
  • 使用 Flutter 開發 Mac 桌面應用
    開啟 Mac 開發桌面應用Flutter 的 master 默認是關閉 桌面應用的,我們可以使用下面的命令開啟:flutter config --enable-macos-desktopflutter config --enable-linux-desktopflutter config --enable-windows-desktop
  • Flutter Go首頁、文檔和下載 - Flutter 學習 App - OSCHINA
    Flutter是一個跨平臺的移動UI框架,旨在幫助開發者使用一套代碼開發高性能、高保真的Android和iOS應用。flutter優點主要包括: 跨平臺 開源 Hot Reload、響應式框架、及其豐富的控制項以及開發工具 靈活的界面設計以及控制項組合 藉助可以移植的GPU加速的渲染引擎以及高性能ARM代碼運行時已達到高質量的用戶體驗Flutter Go 的由來 Flutter學習資料太少,對於英文不好的同學相對來說比較困難 官網文檔示例不夠健全,不夠直觀 各個 widget 的用法各異,屬性紛繁
  • 我們用Flutter重寫了一個React Native應用
    眾所周知,開發人員更喜歡扁平設計,因為它可以讓界面設計變得更簡單和高效。作為一項實驗,我們使用谷歌的 Flutter 成功地重寫了這個應用,結果真的很棒。在這篇文章中,我將從語言棧、UI、樣式和其他方面對 Flutter 和 React Native 進行比較。同時,我還將討論在重寫 Flat App 過程中遇到的一些挑戰,以及我們是怎樣解決的。
  • 使用Flutter一年後,這是我得到的經驗
    首先,Flutter 是一項新技術,因此在實際應用、可信的架構模式和狀態管理工具方面仍然有待發展。有些人會遵循「BLoC」(或「業務邏輯組件」,https://www.youtube.com/watch?v=fahC3ky_zW0)模式。在我看來,它有點太過複雜了,而且有些複雜性是不必要的。
  • Flutter 入門路線圖
    什麼是 flutterFlutter 是 Google 的 UI 工具包,可通過單個代碼庫為行動裝置,web 和桌面系統構建漂亮的,本機編譯的應用程式。這是開發人員文檔的連結,您可以在其中找到在現有的作業系統中安裝Flutter。• Install21[21]https://flutter.dev/docs/get-started/install解決安裝過程中的問題如果您在安裝 flutter 時遇到任何問題,並且 flutter 無法正常工作,那麼這就是出現了一些問題。
  • Flutter 狀態管理指南之 Provider
    推薦閱讀時間:1小時本文結構:1.為什麼需要狀態管理2.什麼是 Provider3.創建一個簡單計數器 app4.你還需要知道的5.Tips6.Q&A7.源碼淺析一、為什麼需要狀態管理在弄清楚如何使用 Provider 之前,我們首先要了解為什麼我的應用需要狀態管理。
  • Flutter實現國際化
    ,應該顯示中文(比如彈出的時間選擇器);國際化二.如果想要添加其他語言,你的應用必須指定額外的 MaterialApp 屬性並且添加一個單獨的 package,叫做 flutter_localizations。截至到 2020 年 2 月份,這個 package 已經支持大約 77 種語言。
  • 【Flutter桌面篇】Flutter&Windows應用嘗鮮
    前言最近換了一臺新的windows,把搭建Flutter&Windows應用的環境過程順便記錄分享一下。Flutter對MacOS的支持還是非常好的,因為iOS和MacOS最終都是用XCode構建的,所以運行在Mac桌面上也輕而易舉。要讓Flutter運行在Windows上,還是比較麻煩的,這也造成一定的門檻。
  • 使用 Flutter 開發 Github 客戶端及學習歷程的小結
    第一周:初識Flutter最初學習Flutter的方式是通過學習 wendux 老師的 《Flutter實戰》:https://book.flutterchina.club/這是一本非常優秀的中文Flutter教程,對個人學習Flutter入門有非常大的幫助。
  • 為啥Flutter Hooks沒有受到太多關注和青睞?
    Hooks 是一種與多個小部件共享同一代碼的方法,這些代碼往往是在有狀態小部件之間重複或難以共享的代碼。這裡我的總結是:「 Hooks 是 UI 邏輯的管理者」。接下來我會介紹自己在應用中使用最多的 Hooks,及其有狀態小部件的等效形式,方便你對比兩者並理解前者帶來的實際收益。
  • 推薦幾個優質Flutter 開源項目
    肯定有同學要問我 Flutter 與其他跨平臺方案的對比,這裡引用下一位各種跨平臺方案都熟悉的作者的文章:本文簡單跟大家聊5毛錢的,忙的同學收藏下就行了,也不是一定要立刻投入學習。千萬要仔細的查看文檔,不要省略一些步驟。xcode 的環境可以不去管,不影響在 AS 中運行。中間你可能會或多或少遇到一些問題,合理利用搜尋引擎即可。
  • Mac下安裝Flutter,並創建第一個App
    【應用程式】中,完成安裝。安裝完成後打開Android Studio應用;第一次打開會詢問是否導入設置,按照需求選擇,我是第一使用,我選擇第二項;第一次使用會彈出無法訪問Android SDK,暫時點Cancel;