前言
上一篇从火车票验票来说 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
。
然后添加到 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
中调用,这里有一个小细节:
void main() {
WidgetsFlutterBinding.ensureInitialized();
CookieManager.instance.initCookie();
runApp(MyApp());
}
复制代码
void main() {
runApp(MyApp());
CookieManager.instance.initCookie();
}
复制代码
运行结果
我们先启动 App,登录后再退出,然后再启动看看是否还处于登录状态,可以看到再次启动后登录是有效的。
总结
本篇利用 Dio 的拦截器实现了自定义的 CookieManager
,并且借助 SharedPreferences
插件实现了 Cookie
离线缓存。实际上,在我们使用 Dio 的过程中或者开发其他业务时,也可以参考拦截器的这种方法,能够提高代码的复用性和降低代码的耦合度。
评论