這是我參與8月更文挑戰的第22天,活動詳情檢視:8月更文挑戰
前言
上一篇我們介紹了 Redux
在Flutter
中的基本概念和簡單示例,看起來好像也沒有那麼複雜。但是這些操作都是同步的,點選按鈕、發起 Action
排程、然後更新狀態、最後更新介面是連貫的。那如果有一個非同步請求怎麼辦,也就是我們可能是點選一個按鈕發起的並不是 Action
,而是非同步網路請求,這個時候又如何通知更新狀態?通常來說,在這種場景裡我們需要發起三個 Action
:
- 網路載入提示:介面通知使用者當前正在請求資料,請等待,通常是一個
Loading
提示。 - 網路請求成功:接收到後端資料,然後通知介面以最新狀態的資料重新整理介面。
- 網路請求失敗:介面通知使用者請求失敗,通常是一個錯誤提示。
這個時候通過按鈕點選回撥肯定沒法完成這樣的操作,這個時候就需要利用到 Redux 的中介軟體了。本篇我們以聯絡人列表為例,來講述如何使用中介軟體完成非同步網路請求。
準備工作
- 首先請更新最新的後端程式碼:後端程式碼(基於 Express.js),更新後在目錄下執行
node seedContactor.js
產生資料庫Mock
資料,注意圖片這裡使用的是本機的附件地址,在後端專案的public/upload/image
下面,如果需要展示聯絡人頭像的自己可以找一張圖片,然後修改一下seedContactor.js
中的avatar
欄位為對應圖片檔名。聯絡人的介面地址為:http://localhost:3900/api/contactor
。 - 更新依賴檔案,包括如下外掛:
redux
: ^5.0.0:最新版的 Redux 外掛flutter_redux
: ^0.8.2:Flutter 適配 Redux 的外掛dio
: ^4.0.0:網路請求外掛,使用可以檢視本專欄的文章:Dio 篇章總結flutter_easyrefresh
: ^2.2.1:上拉、下拉重新整理元件。cached_network_image
: ^3.1.0:支援快取的網路圖片載入元件。flutter_easyloading
: ^3.0.0:全域性的彈窗提醒元件。shared_preferences
: ^2.0.6:本地離線簡單鍵值對儲存外掛。
- 拷貝和初始化:從之前的程式碼中拷貝網路請求的工具類到本工程,完成如
CookieManager
和EasyLoading
的初始化。當然,你可以直接從這裡下載本專欄關於Redux 篇章的程式碼:基於 Redux 的狀態管理。
完成上述工作後,我們就可以開始擼本篇的程式碼了。
Redux 三板斧
上篇也說過,Redux 的好處之一就是狀態管理的形式是統一的,三個元素 Action
、Store
和 Reducer
缺一不可,因此,我們先來梳理聯絡人列表業務中對應的這三個元素的內容。
首先來定義 Action
,列表頁面互動上會涉及2個Action
,重新整理和載入更多。但邏輯上還有另外兩個動作:獲取資料成功和獲取資料失敗,因此一共有4個Action
。
- 重新整理:獲取第一頁的資料,定義為
RefreshAction
,在互動時使用下來重新整理時排程該Action
。 - 載入更多:獲取下一頁的資料,定義為
LoadAction
,在互動時使用上拉載入時呼叫該Action
。 - 載入成功:網路請求成功,定義為
SuccessAction
。 - 載入失敗:網路請求異常或錯誤,定義為
FailedAction
。
成功和失敗這兩個是非同步操作,沒有使用者互動主動 排程的可能,這裡留給本篇的主角中介軟體來處理,稍後再單獨介紹。Action 的程式碼如下,成功和失敗因為要攜帶資料更新狀態,因此他們有自己的成員屬性:
class RefreshAction {}
class LoadAction {}
class SuccessAction {
final List<dynamic> jsonItems;
final int currentPage;
SuccessAction(this.jsonItems, this.currentPage);
}
class FailedAction {
final String errorMessage;
FailedAction(this.errorMessage);
}
複製程式碼
接下來是 Store
的狀態物件,我們要明確需要哪些資料。首先肯定的是,需要有網路請求成功後的聯絡人列表資料;其次是當前請求的頁碼,我們在載入更多的時候需要根據該頁面請求下一頁資料;之後是 Loading
狀態標記和錯誤資訊,Loading
狀態標記在某些場合可以用於提示,而錯誤資訊則用於錯誤提醒。因此,Store
對應的狀態資料有:
contactors
:聯絡人列表資料,為List<dynamic>
型別(要與Dio
接收的資料匹配,只能是該型別)。isLoading
:載入標識,預設是false
,當排程RefreshAction
或LoadAction
的時候,標記為true
,當請求成功或失敗為true
,標記為false
。errorMessage
:錯誤資訊,允許為空,因此定義為String?
。currentPage
:當前請求頁碼,預設為1。
Store
的狀態物件類的程式碼如下:
class ContactorState {
final List<dynamic> contactors;
final isLoading;
final String? errorMessage;
final int currentPage;
ContactorState(this.contactors,
{this.isLoading = false, this.errorMessage, this.currentPage = 1});
factory ContactorState.initial() => ContactorState(List.unmodifiable([]));
}
複製程式碼
最後是 Reducer
了,Reducer
定義是一個函式,根據舊的狀態物件和當前的 Action 來返回新的狀態物件。這裡的業務邏輯如下:
RefreshAction
處理:標記請求狀態isLoading
為true
,當前頁碼currentPage
為1
,其他保留和原先的狀態一致。LoadAction
處理:標記請求狀態isLoading
為true
,當前頁碼currentPage
為舊狀態的頁面加1
,其他保留和原先的狀態一致。FailedAction
處理:更新狀態的錯誤訊息,標記請求狀態isLoading
為false
。SuccessAction
處理:需要根據最新的請求頁碼來決定如何更新列表資料。如果當前頁碼是1
,則使用最新的的資料替換掉原有列表;如果當前頁面大於1
,則將新的資料拼接到舊的資料後面。之後則是更新狀態的isLoading
為false
,頁碼為當前action
的頁碼,以及清空errorMessage
。
Reducer
的程式碼如下所示:
ContactorState contactorReducer(ContactorState state, dynamic action) {
if (action is RefreshAction) {
ContactorState contactorState = ContactorState(state.contactors,
isLoading: true, errorMessage: null, currentPage: 1);
return contactorState;
}
if (action is LoadAction) {
ContactorState contactorState = ContactorState(state.contactors,
isLoading: true,
errorMessage: null,
currentPage: state.currentPage + 1);
return contactorState;
}
if (action is SuccessAction) {
int currentPage = action.currentPage;
List<dynamic> contactors = state.contactors;
if (currentPage > 1) {
contactors += action.jsonItems;
} else {
contactors = action.jsonItems;
}
ContactorState contactorState = ContactorState(contactors,
isLoading: false, errorMessage: null, currentPage: currentPage);
return contactorState;
}
if (action is FailedAction) {
ContactorState contactorState = ContactorState(
state.contactors,
isLoading: false,
errorMessage: action.errorMessage,
);
return contactorState;
}
return state;
}
複製程式碼
中介軟體
所謂的中介軟體,其實就和我們之前的 Dio 的攔截器(見:Dio 之攔截器)類似,也就是在排程 Action 前會先執行中介軟體方法,處理完之後再交給下一個中介軟體處理。Redux
的攔截器定義在 Store
構造方法中,形式為:
void (Store<T> store, action, NextDispatcher next)
複製程式碼
在這裡,我們定義的中介軟體方法名為:fetchContactorMiddleware
,需要在構建 Store
物件時加入到 middleware
引數中。middleware
本身是一個陣列,因此我們可以新增多種中介軟體,以便進行不同的處理。
final Store<ContactorState> store = Store(
contactorReducer,
initialState: ContactorState.initial(),
middleware: [
fetchContactorMiddleware,
],
);
複製程式碼
在中介軟體中我們可以獲取到當前的 Action
和狀態,因此可以根據 Action
做不同的業務。在這裡我們只需要處理重新整理和載入更多:
- 重新整理時,將頁碼置為1,請求資料,請求成功後發起
SuccessAction
排程,通知狀態更新。 - 載入更多時,將頁碼加1後再請求資料,請求成功後發起
SuccessAction
排程,通知狀態更新。 - 請求失敗都發起
FailedAction
排程,通知狀態請求失敗。
處理完之後,記得呼叫 next
方法,將當前action
傳遞過去,一般完成正常的排程過程。中介軟體的程式碼如下:
void fetchContactorMiddleware(
Store<ContactorState> store, action, NextDispatcher next) {
const int pageSize = 10;
if (action is RefreshAction) {
// 重新整理取第一頁資料
ContactorService.list(1, pageSize).then((response) {
if (response != null && response.statusCode == 200) {
store.dispatch(SuccessAction(response.data, 1));
} else {
store.dispatch(FailedAction('請求失敗'));
}
}).catchError((error, trace) {
store.dispatch(FailedAction(error.toString()));
});
}
if (action is LoadAction) {
// 載入更多時頁碼+1
int currentPage = store.state.currentPage + 1;
ContactorService.list(currentPage, pageSize).then((response) {
if (response != null && response.statusCode == 200) {
store.dispatch(SuccessAction(response.data, currentPage));
} else {
store.dispatch(FailedAction('請求失敗'));
}
}).catchError((error, trace) {
store.dispatch(FailedAction(error.toString()));
});
}
next(action);
}
複製程式碼
頁面程式碼
頁面程式碼和上一篇的結構類似,但是本篇構建了一個 ViewModel
類,使用了 StoreConnector
的 converter
方法將狀態的中的列表資料轉換為頁面展示所需要的物件。
class _ViewModel {
final List<_ContactorViewModel> contactors;
_ViewModel(this.contactors);
factory _ViewModel.create(Store<ContactorState> store) {
List<_ContactorViewModel> items = store.state.contactors
.map((dynamic item) => _ContactorViewModel.fromJson(item))
.toList();
return _ViewModel(items);
}
}
class _ContactorViewModel {
final String followedUserId;
final String nickname;
final String avatar;
final String description;
_ContactorViewModel({
required this.followedUserId,
required this.nickname,
required this.avatar,
required this.description,
});
static _ContactorViewModel fromJson(Map<String, dynamic> json) {
return _ContactorViewModel(
followedUserId: json['followedUserId'],
nickname: json['nickname'],
avatar: UploadService.uploadBaseUrl + 'image/' + json['avatar'],
description: json['description']);
}
}
複製程式碼
頁面的build
方法如下,可以看到頁面中沒有體現中介軟體部分的程式碼,而是在 dispatch
過程中自動完成了。
@override
Widget build(BuildContext context) {
return StoreProvider<ContactorState>(
store: store,
child: Scaffold(
//省略 appBar
body: StoreConnector<ContactorState, _ViewModel>(
converter: (Store<ContactorState> store) => _ViewModel.create(store),
builder: (BuildContext context, _ViewModel viewModel) {
return EasyRefresh(
child: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
leading:
_getRoundImage(viewModel.contactors[index].avatar, 50),
title: Text(viewModel.contactors[index].nickname),
subtitle: Text(
viewModel.contactors[index].description,
style: TextStyle(fontSize: 14.0, color: Colors.grey),
),
);
},
itemCount: viewModel.contactors.length,
),
onRefresh: () async {
store.dispatch(RefreshAction());
},
onLoad: () async {
store.dispatch(LoadAction());
},
firstRefresh: true,
);
},
),
// 省略其他程式碼
),
);
複製程式碼
這裡需要注意,EasyRefresh
元件要放置在 StoreConnector
的下一級,否則會因為在重新整理的時候找不到下級ScrollView
,報null
錯誤。
執行結果
執行結果如下圖所示,整個執行和之前我們使用Provider
沒有太大區別,但是從封裝性來看,使用 Redux 的封裝性會更好,比如網路請求部分的業務放在了中介軟體,對於元件層面來說只需要關心要發起什麼動作,而不需要關心具體動作後要怎麼處理。程式碼已提交至:Redux 相關程式碼。
總結
先來梳理一下 Redux
加了中介軟體的整個流程,如下圖所示。
加入了中介軟體後,在非同步完成後可以觸發非同步操作相關的 Action
,以便將非同步結果通過Reducer
處理後更新狀態。引入中介軟體後,可以使得非同步操作與介面互動分離,進一步降低的耦合性和提高了程式碼的可維護性。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!