前言
上一篇Flutter 入門與實戰(三十二):小夥子,你買票了嗎?介紹了 Dio 的 Cookie 處理。雖然實現了我們想要的效果,但是還有三個問題沒解決:
- Cookie 的管理程式碼和業務程式碼放在一起了,暴露了實現的細節。
- Cookie 沒有持久化,一旦 App 關閉後,每次開啟都需要重新登入,體驗不太好。
- HttpUtil 工具類同時管理了 Cookie,不符合單一職責原則。
本篇我們就來手寫一個 CookieManager,並通過shared_preferences實現 Cookie 持久化。
思路
降低程式碼的侵入性,使用攔截器是一個好的選擇,Dio 官方提供了自定義攔截器類的實現樣例:
import 'package:dio/dio.dart';
class CustomInterceptors extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
return super.onRequest(options, handler);
}
@override
Future onResponse(Response response, ResponseInterceptorHandler handler) {
print('RESPONSE[${response.statusCode}] => PATH: ${response.request?.path}');
return super.onResponse(response, handler);
}
@override
Future onError(DioError err, ErrorInterceptorHandler handler) {
print('ERROR[${err.response?.statusCode}] => PATH: ${err.request.path}');
return super.onError(err, handler);
}
}
複製程式碼
我們可以定義一個攔截器,在攔截器中處理 Cookie
。
- 在
onResponse
中檢測有沒有cookie
,如果有就存起來。然 - 在
onRequest
中,攜帶cookie
提交。
然後新增到 Dio 的攔截器中即可,看起來挺簡單的,開擼!
手寫CookieManager
定義一個 CookieManager 類,繼承自 Intercepter,該類做成單例模式。 下面的程式碼是沒有做持久化的管理。主要業務邏輯如下:
- Dart 的單例實現:需要把建構函式定義為私有方法,使用
{類名}._privateConstructor()
宣告即可。 - 在
onReponse
中將之前登入成功後處理cookie
的程式碼挪過來,如果返回的狀態碼是200
,且有cookie
就將cookie
資訊存入到CookieManager
的_cookie
字串中。如果返回的狀態碼是401
,說明登入會話已經失效,將_cookie
清空。 - 在
onRequest
的時候,在options
的headers
裡將_cookie
新增到Cookie
欄位中,實現攜帶cookie
提交請求。
import 'package:dio/dio.dart';
class CookieManager extends Interceptor {
CookieManager._privateConstructor();
static final CookieManager _instance = CookieManager._privateConstructor();
static get instance => _instance;
String _cookie;
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response != null) {
if (response.statusCode == 200) {
if (response.headers.map['set-cookie'] != null) {
_cookie = response.headers.map['set-cookie'][0];
}
} else if (response.statusCode == 401) {
_cookie = null;
}
}
super.onResponse(response, handler);
}
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
options.headers['Cookie'] = _cookie;
return super.onRequest(options, handler);
}
}
複製程式碼
之後移除登入、退出登入以及 HttpUtil
的 setCookie
和 clearCookie
方法。這樣 HttpUtil
就不會暴露給UI 層了。同時在 HttpUtil
中將 CookieManager
的單例物件新增到 Dio 的攔截器中。
static Dio getDioInstance() {
if (_dioInstance == null) {
_dioInstance = Dio();
_dioInstance.interceptors.add(CookieManager.instance);
}
return _dioInstance;
}
複製程式碼
執行一下,效果和上一篇一樣的,接下來來做持久化。
SharedPreferences持久化
SharedPreferences
是一個簡單的鍵值對持久化工具,對應原生實際上是安卓的SharedPreferences
和 iOS 的NSUserDefaults
。為啥名字沿用了安卓而不是 iOS的,可能是因為 Flutter 和安卓有一個共同的爹吧。
SharedPreferences
支援如下布林值、整型、浮點型、字串、字串陣列。如果要儲存物件的話,也可以將物件做 json 序列化儲存。另外就是SharedPreferences
因為涉及 I/O 操作,因此本身是一個非同步操作。
使用的話就很簡單了:
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
await prefs.setInt('counter', counter);
複製程式碼
我們可以在獲取到新的 cookie
後,更新時使用SharedPreferences
來實現持久化。現在 pubspec.yaml 中新增依賴,由於我們當前的 Flutter SDK是2.0.6,選擇0.5.7版本。
在 CookieManager 中增加三個方法:
initCookie
:讀取離線儲存的cookie
到記憶體中,這個方法應該在啟動階段執行。_persistCookie
:持久化儲存cookie
,這裡為了減少沒必要的I/O操作,只有在cookie
變化的時候才進行持久化。_clearCookie
:清除cookie
,包括從記憶體中和離線儲存中清除。
Future initCookie() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_cookie = prefs.getString('cookie');
}
void _persistCookie(String newCookie) async {
if (_cookie != newCookie) {
_cookie = newCookie;
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('cookie', _cookie);
}
}
void _clearCookie() async {
_cookie = null;
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove('cookie');
}
複製程式碼
之後就是在 onResponse
中對 cookie
進行處理:
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response != null) {
if (response.statusCode == 200) {
if (response.headers.map['set-cookie'] != null) {
_persistCookie(response.headers.map['set-cookie'][0]);
}
} else if (response.statusCode == 401) {
_clearCookie();
}
}
super.onResponse(response, handler);
}
複製程式碼
初始化 cookie 的方法我們在 main 中呼叫,這裡有一個小細節:
- 如果涉及到原生的互動的,正常不可以在 runApp 執行前呼叫,因為此時可能原生通道還沒建立。如果要呼叫,得先呼叫WidgetsFlutterBinding.ensureInitialized()確保原生通道已經建立。因此在main方法初始化 cookie 的方式如下:
void main() {
WidgetsFlutterBinding.ensureInitialized();
CookieManager.instance.initCookie();
runApp(MyApp());
}
複製程式碼
- 另一種方式就是在 runApp 之後呼叫,推薦使用該方式。
void main() {
runApp(MyApp());
CookieManager.instance.initCookie();
}
複製程式碼
執行結果
我們先啟動 App,登入後再退出,然後再啟動看看是否還處於登入狀態,可以看到再次啟動後登入是有效的。
總結
本篇利用 Dio 的攔截器實現了自定義的 CookieManager,並且藉助 SharedPreferences 外掛實現了 Cookie離線快取。實際上,在我們使用 Dio 的過程中或者開發其他業務時,也可以參考攔截器的這種方法,能夠提高程式碼的複用性和降低程式碼的耦合度。