Flutter 入門與實戰(二十七):使用 GetIt 同步不同頁面間資料

島上碼農發表於2021-07-12

本文已參與好文召集令活動,點選檢視:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!

前言

之前幾篇都是關於 Dio 網路請求相關的內容,我們的動態模組也就差詳情頁了,但是每次新增和編輯成功後,返回到列表頁還是需要手動重新整理,沒有達到“所見即所得”的效果。本篇將介紹使用 GetIt 容器外掛完成頁面間的資料同步。 本篇涉及到的知識點如下:

  • 詳情頁面介面構建;
  • 更新詳情檢視次數介面實現;
  • GetIt 簡介;
  • 使用 GetIt 註冊全域性物件;
  • 使用 GetIt 實現頁面間的資料同步。

詳情頁面

詳情頁面我們顯示動態的標題、檢視次數、圖片和內容。最簡單的方式是使用 Column 元件將所有內容依次包裹。但是,考慮內容實際會很長(也可能是富文字),因此使用滾動元件包裹更合適,這裡還是使用 CustomerScrollView 來,相比普通的 ScrollView 來說,CustomerScrollView 使用 Sliver 子元件,滑動效能會更高也更順暢,就如同某巧克力廣告說的一樣——縱享絲滑。

image.png

頁面本身比較簡單,就不多介紹了,具體的頁面層級如下所示。關於 CustomerScrollView可以參考之前的文章:Flutter 入門與實戰(十二):利用CustomScrollView實現更有趣的滑動效果

  • CustomScrollView
    • slivers
      • 標題:使用 Container 包裹以便調整佈局。
      • 檢視次數:和列表的檢視次數類似。
      • 圖片:為了避免圖片佔據太高的高度,將圖片高度限制在240。
      • 內容:和標題類似,只是字型調小了2號

以上的 slivers 的子元件的內容都使用SliverToBoxAdapter轉換為 Sliver。最終介面看後面的動圖即可。

詳情檢視次數更新

當我們進入詳情頁面時,需要向後端提交更新檢視次數的介面。注意,有些應用處理是後端直接在獲取詳情介面時往資料庫增加檢視數。雖然這樣可以減少請求次數,但是後端處理存在缺陷是會把呼叫詳情介面直接當做檢視詳情頁面進行統計,結果在其他地方獲取詳情時(比如編輯介面)可能導致多統計,行業黑話稱之為“刷流量”。

image.png

使用前端更新檢視次數可以做到更精準的控制,比如我們可以設定在頁面停留多長時間才是有效檢視,或者滑動到頁面底部才算有效檢視等等。

更新檢視次數的介面地址為:http://localhost:3900/api/dynamics/view/:id,記得拉取最新的程式碼執行。我們在獲取詳情介面成功後再進行檢視次數更新。更新檢視次數不是很重要的邏輯,出現錯誤時無需給予提醒(避免給使用者造成困惑),這裡只是列印異常資訊,用於開發過程中排查問題。實際生產過程可以將異常資訊上傳到異常監控後臺。

if (response.statusCode == 200) {
  setState(() {
    _dynamicEntity = DynamicEntity.fromJson(response.data);
  });
  _updateViewCount();
}
//...

void _updateViewCount() async {
  try {
    var response = await DynamicService.updateViewCount(_dynamicEntity.id);
    if (response.statusCode == 200) {
      setState(() {
        _dynamicEntity.viewCount = response.data['viewCount'];
        GetIt.instance.get<DynamicListener>().dynamicUpdated(
              _dynamicEntity.id,
              _dynamicEntity,
            );
      });
    }
  } catch (e) {
    print(e.toString());
  }
}
複製程式碼

GetIt 簡介

GetIt 本身是一個容器管理外掛,其最初的設計是用於完成依賴注入DI 和 IOC 容器的功能,有點類似Java Spring 的Bean容器。由於容器中的物件是全域性的,因此可以用來做資料同步,也是 Flutter 官方推薦的狀態管理容器之一。另一個常用的狀態管理外掛是 Provider,後面我們涉及到狀態管理的時候再來講述。

所謂的容器,本質上就是一個全域性Map物件,可以往裡面存入物件後,在需要用的時候直接取出,而不需要每個使用者都自己建立物件,也實現了物件之間的解耦。

GetIt 的基礎用法很簡單,如下所示。如果考慮啟動時避免佔用太多資源,也可以使用 lazy懶載入的方式,懶載入時傳入的是一個構建物件的方法,在取出物件的時候,如果容器中沒有該物件,則使用構建物件的方法建立一個,如果已經有了就直接返回。

注意,GetIt 有很多個版本,對 Flutter 的 最低SDK 版本有要求,我們當前使用的 SDK 版本是2.0.6,因此最高只能選擇4.0.3版本(最新版本是7.1.2,需要2.12.x 以上版本)。

// 註冊物件:一般是單例
GetIt.instance.registerSingleton<T>(T object);
// 懶載入方式註冊
GetIt.instance.registerLazySingleton<T>(FactoryFunc<T> func)
// 獲取容器中的物件
GetIt.instance.get<T>();
複製程式碼

註冊動態改變監聽物件

當動態新增,或者動態內容發生改變時,我們需要更新列表。最簡單的方式是通知列表重新整理,但是那樣的增加了網路請求。我們可以直接修改列表的資料來完成列表的更新。考慮到不僅僅是動態列表頁需要更新(比如動態嵌入到其他頁面中),我們把動態更新的方法抽象為介面,只要是實現了對應介面的物件都可以在動態發生變化時呼叫對應的介面更新——即所謂的面向介面程式設計。

image.png

新增一個 dynamic_listener.dart 檔案,定義一個介面抽象類DynamicListener。在列表頁面的_DynamicPageState中實現對應的介面。

import 'package:home_framework/models/dynamic_entity.dart';

abstract class DynamicListener {
  void dynamicUpdated(String id, DynamicEntity updatedDynamic);

  void dynamicAdded(DynamicEntity newDynamic);
}
複製程式碼

_DynamicPageState使用 implements 關鍵字(也可以使用 with 關鍵字)實現DynamicListener介面的兩個方法:

  • 新增響應方法:當有新增動態時,將新增動態插入到開頭處;
  • 更新方法:使用新的動態替換舊的動態資料。

同時,在initState方法中註冊自身到 GetIt 容器。

class _DynamicPageState extends State<DynamicPage> implements DynamicListener {
	// ...
	
  @override
  void initState() {
    super.initState();
    // 註冊到 GetIt容器
    GetIt.instance.registerSingleton<DynamicListener>(this);
  }
  
  void dynamicUpdated(String id, DynamicEntity updatedDynamic) {
    int index = _listItems.indexWhere((element) => element.id == id);
    if (index != -1) {
      setState(() {
        _listItems[index] = updatedDynamic;
      });
    }
  }

  void dynamicAdded(DynamicEntity newDynamic) {
    setState(() {
      _listItems.insert(0, newDynamic);
    });
  }
  
  // ...
}
複製程式碼

頁面間資料更新

有了 GetIt 容器,因為可以直接從容器中獲取動態列表狀態管理物件,在其他頁面處理就比較簡單了,邏輯分別如下:

  • 新增頁面:新增成功後呼叫dynamicAdded方法更新列表頁面;
  • 編輯頁面:編輯成功後呼叫dynamicUpdated方法更新列表頁面;
  • 詳情頁面:更新檢視次數後呼叫dynamicUpdated方法更新列表頁面。

三個頁面的程式碼分別如下:

//新增頁面
var response = await DynamicService.post(newFormData);
if (response.statusCode == 200) {
  Dialogs.showInfo(context, '新增成功');
  GetIt.instance
      .get<DynamicListener>()
      .dynamicAdded(DynamicEntity.fromJson(response.data));
  Navigator.of(context).pop();
}
//-------------------------------------
//編輯頁面
if (response.statusCode == 200) {
  Dialogs.showInfo(context, '儲存成功');
  //處理成功更新後的業務
  _handleUpdated(newFormData);
  Navigator.of(context).pop();
}

// 處理更新,如果圖片更新了才更新動態圖片內容
void _handleUpdated(Map<String, String> newFormData) {
  _dynamicEntity.title = newFormData['title'];
  _dynamicEntity.content = newFormData['content'];
  if (newFormData.containsKey('imageUrl')) {
    _dynamicEntity.imageUrl = newFormData['imageUrl'];
  }
  GetIt.instance.get<DynamicListener>().dynamicUpdated(
      _dynamicEntity.id,
      _dynamicEntity,
  );
}

//-------------------------------------
//詳情頁面
void _updateViewCount() async {
  try {
    var response = await DynamicService.updateViewCount(_dynamicEntity.id);
    if (response.statusCode == 200) {
      setState(() {
        _dynamicEntity.viewCount = response.data['viewCount'];
        GetIt.instance.get<DynamicListener>().dynamicUpdated(
              _dynamicEntity.id,
              _dynamicEntity,
            );
      });
    }
  } catch (e) {
    print(e.toString());
  }
}
複製程式碼

執行效果

螢幕錄製2021-07-12 下午11.52.47.gif

總結

本篇完成了整個動態管理的業務邏輯,包括了新增、刪除、編輯、檢視次數等功能。通過 GetIt 容器管理外掛及介面定義,可以很簡單快速地完成頁面之間的資料同步。從整個系列也可以看到,我們在網路請求這塊的程式碼存在如下問題:

  • 重複程式碼很多:比如 try...catch 程式碼塊;
  • 暴露了 Dio 的細節;
  • 介面參與了業務物件的構建,沒有與業務邏輯分離。

接下來的篇章我們將逐步完成對 Dio的封裝和網路請求部分程式碼的重構。

相關文章