Flutter 入門與實戰(三十三):手寫一個持久化的 CookieManager

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

前言

上一篇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 的時候,在 optionsheaders 裡將 _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);
  }
}
複製程式碼

之後移除登入、退出登入以及 HttpUtilsetCookieclearCookie 方法。這樣 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 操作,因此本身是一個非同步操作。

google1.gif 使用的話就很簡單了:

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,登入後再退出,然後再啟動看看是否還處於登入狀態,可以看到再次啟動後登入是有效的。

螢幕錄製2021-07-22 下午10.46.21.gif

總結

本篇利用 Dio 的攔截器實現了自定義的 CookieManager,並且藉助 SharedPreferences 外掛實現了 Cookie離線快取。實際上,在我們使用 Dio 的過程中或者開發其他業務時,也可以參考攔截器的這種方法,能夠提高程式碼的複用性和降低程式碼的耦合度。

相關文章