写点什么

手写一个持久化的 Flutter 会话管理器

作者:岛上码农
  • 2022 年 5 月 04 日
  • 本文字数:2973 字

    阅读完需:约 10 分钟

手写一个持久化的Flutter会话管理器

前言

上一篇从火车票验票来说 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 操作,因此本身是一个异步操作。



使用的话就很简单了:


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 进行处理:


@overridevoid 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 的过程中或者开发其他业务时,也可以参考拦截器的这种方法,能够提高代码的复用性和降低代码的耦合度。


发布于: 刚刚阅读数: 2
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
手写一个持久化的Flutter会话管理器_flutter_岛上码农_InfoQ写作社区