Flutter 入門與實戰(四十七):使用 Provider 改造?一樣的程式碼,程式碼量降低了2/3!

島上碼農發表於2021-08-10

這是我參與8月更文挑戰的第10天,活動詳情檢視:8月更文挑戰

前言

之前的幾篇我們寫了狀態管理的機制和狀態管理外掛,接下來幾篇我們就使用官方推薦的 Provider 來改造舊的程式碼,你會發現改造前後具有十分大的差別。關於 Provider 的示例,之前翻譯了一篇官網推薦的購物車示例文章:Flutter 入門與實戰(四十):以購物車為例初探狀態管理

Provider 簡介

Provider 當前最新版本是5.0.0,使得元件樹能夠共享狀態資料的方式為:

Provider (
  create: (_) => Model(),
  child: someWidget(),
);
複製程式碼

Provider類本身並不會在狀態改變的時候自動更新子元件,因此更常用的是使用其子類:

  • ListenableProvider:監聽實現了 Listenable 的的物件,並將其暴露給下級元件。當觸發一個事件後會通知子元件依賴發生變化進而實現重建。
  • ChangeNotifierProvider:最為常用的一個方式,是ListenableProvider的子類。監聽實現了 ChangeNotifier 介面的物件,當該物件呼叫 notifyListeners 的時候,就會通知全部的監聽元件更新元件。
  • ValueListenableProvider:監聽實現了ValueListenable介面的物件。當該物件改變時,會更新其下級元件。
  • StreamProvider:監聽 Stream 物件,然後將其內容暴露給子元件。通常是向一個元件以流的方式提供大量的內容,例如電池電量監測、Firebase 查詢等。

如果一個物件被多個元件共享,那麼可以使用如下方式:

// 被多個元件共享的物件
MyChangeNotifier variable;

ChangeNotifierProvider.value(
  value: variable,
  child: ...
)
複製程式碼

在 Widget 中使用狀態資料有三種方式:

  • 使用 context.read<T>() 方法:該方法返回T 型別的狀態資料物件,但不會監聽該物件的改變,適用於只讀的情況;
  • 使用 context.watch<T>() 方法:該方法返回 T 型別狀態資料物件,並且會監聽它的變化,適用於需要根據狀態更新的狀況。
  • 使用 context.select<T,R>(R cb(T value)) 方法:返回 T物件中的 R 型別物件,這可以使得 Widget 只監聽狀態物件的部分資料。

詳細內容建議大家去看 Provider 的官方文件,我們後續的篇章也會涉及其中的內容。

程式碼分析

我們在前面的篇章介紹了一個動態模組的管理,包括了整個 CRUD 過程。具體可以從專欄:Flutter 入門與實戰的第二十二到第二十七篇。首先我們來改造一下列表的程式碼,回頭再來看之前的程式碼,就會知道為什麼說直接使用 setState 的方式更新介面的開發者會被評為“**草包**”了!

image.png

之前程式碼一看就很亂,首先是在列表裡包括了新增、編輯、刪除的回撥程式碼,是想要是業務複雜一點,豈不是回撥要滿屏飛了!其次是業務程式碼和 UI 程式碼混用,一個是程式碼又臭又長——俗稱?一樣的程式碼,另外一個是業務程式碼的複用性降低了。比如說,我們在別的地方可能也會用到動態的增改刪查業務,總不能再複製、貼上再來一遍吧?

程式碼改造

現在我們來使用Provider 將業務和 UI 分離。將業務相關的程式碼統一放到狀態管理中,UI 這邊只處理介面相關的程式碼。首先抽取一個 DynamicModel 類,檔名為 dynamic_model.dart,把列表的相關業務程式碼放進來:

  • 列表資料:使用一個 List<DynamicEntity> 物件儲存列表資料,預設為空陣列。
  • 分頁資料:當前頁碼 _currentPage,固定每頁大小為20。
  • 重新整理方法:refresh,將當前頁碼置為1,重新請求第一頁資料。
  • 載入方法:load,將當前頁碼加1,請求第 N 頁的資料。
  • 獲取分頁資料:根據當前頁面和分頁大小請求動態資料,並更新列表資料。
  • 預留deleteaddupdate 方法,以便後面的刪除、新增和更新使用。

整個DynamicModel類的程式碼如下,這裡關鍵的一點是使用 with ChangeNotifier 使得 DynamicModel 混入ChangeNotifer的特性,以便 ChangeNotifierProvider 能夠為其新增監聽器,並且在呼叫 notiferListeners的時候通知狀態依賴的子元件進行更新。

class DynamicModel with ChangeNotifier {
  List<DynamicEntity> _dynamics = [];
  int _currentPage = 1;
  final int _pageSize = 20;

  List<DynamicEntity> get dynamics => _dynamics;

  void refresh() {
    _currentPage = 1;
    _requestNewItems();
  }

  void load() {
    _currentPage += 1;
    _requestNewItems();
  }

  void _requestNewItems() async {
    var response = await DynamicService.list(_currentPage, _pageSize);
    if (response != null && response.statusCode == 200) {
      List<dynamic> _jsonItems = response.data;
      List<DynamicEntity> _newItems =
          _jsonItems.map((json) => DynamicEntity.fromJson(json)).toList();
      if (_currentPage == 1) {
        _dynamics = _newItems;
      } else {
        _dynamics += _newItems;
      }
    }

    notifyListeners();
  }

  void removeWithId(String id) {}

  void add(DynamicEntity newDynamic) {}

  void update() {}
}

複製程式碼

接下來是使用 Provider 為動態模組提供狀態管理,如前面的幾章所述,Provider 需要處於元件的上級才能夠為子元件提供狀態共享,因此我們有兩種方式來實現這種方式。

  • 在構建 DynamicPage列表頁面的 app.dart 中將 DynamicPage 作為 Provider 的下級。如下所示,這種方式的缺點是因為這是首頁,如果各個模組的程式碼都往這裡對方,會使得 app.dart 很臃腫,而且耦合度也變高。
@override
void initState() {
  super.initState();
  _homeWidgets = [
    ChangeNotifierProvider<DynamicModel>(
      create: (context) => DynamicModel(),
      child: DynamicPage(),
    ),
    MessagePage(),
    CategoryPage(),
    MineSliverPage(),
  ];
}
複製程式碼
  • 使用一個 Widget 包裹 DynamicPage 以及 Provider來降低程式碼的耦合度,避免 app.dart 中的程式碼過於臃腫。
class DynamicWrapper extends StatelessWidget {
  const DynamicWrapper({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => DynamicModel(),
      child: DynamicPage(),
    );
  }
}
複製程式碼

之後就是對 DynamicPage進行改造,首先是將 DynamicPageStatefulWidget 改為 StatelessWidget,然後移除掉相關業務程式碼。,最後就是在 build 方法中從 Provider 獲取介面所需的資料,或呼叫對應的方法。改造完的 DynamicPage 就十分清爽了,如下所示:

class DynamicPage extends StatelessWidget {
  DynamicPage({Key key}) : super(key: key);

  final EasyRefreshController _refreshController = EasyRefreshController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('動態', style: Theme.of(context).textTheme.headline4),
        actions: [
          IconButton(
              icon: Icon(Icons.add),
              onPressed: () {
                RouterManager.router
                    .navigateTo(context, RouterManager.dynamicAddPath);
              }),
        ],
        brightness: Brightness.dark,
      ),
      body: EasyRefresh(
        controller: _refreshController,
        firstRefresh: true,
        onRefresh: () async {
          context.read<DynamicModel>().refresh();
        },
        onLoad: () async {
          context.read<DynamicModel>().load();
        },
        child: ListView.builder(
          itemCount: context.watch<DynamicModel>().dynamics.length,
          itemBuilder: (context, index) {
            return DynamicItem(context.watch<DynamicModel>().dynamics[index],
                (String id) {
              context.read<DynamicModel>().removeWithId(id);
            });
          },
        ),
      ),
    );
  }
}
複製程式碼

ListView.builder 中我們使用了 contxt.watch<DynamicModel>方法來獲取最新的動態列表 ,從而使得當列表資料改變時能夠重新整理介面。而在呼叫方法方面,我們則使用了 context.read<DynamicModel>方法,因為這裡並不需要監聽狀態的改變。執行一下,發現和之前的效果一樣,改造完成。

改造前後對比

我們來對比改造前後的 DynamicPage 程式碼,如下圖所示(左側為舊程式碼)。可以看到,大部分程式碼都被移除了,實際原先的程式碼有120行,而現在的程式碼只有40行了,足足減少了2/3

image.png

當然,程式碼減少是因為將業務程式碼抽離了,但是業務程式碼本身是可以複用的。下一篇我們將刪除、新增和編輯完成後,再來看 Provider 如何進一步提高程式碼複用性和簡化頁面程式碼。

總結

通過 Provider 狀態管理,得到的最大的好處其實是 UI 層和業務層程式碼分離,精簡了 UI層程式碼的同時,也提高了業務程式碼的複用性。而 Provider 的區域性重新整理特性,也能夠提高介面渲染的的效能。


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章