手写一个持久化的 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 进行处理:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容