Write a fluent persistent session manager

Posted by direwolf on Wed, 22 Dec 2021 23:10:31 +0100

preface

Last Flutter network request session management This paper introduces Dio's Cookie processing. Although the desired effect has been achieved, there are still three problems to be solved:

  • Cookie management code and business code are put together, exposing the details of the implementation.
  • Cookie s are not persistent. Once the App is closed, you need to log in again every time you open it. The experience is not very good.
  • HttpUtil tool class manages cookies at the same time, which does not comply with the principle of single responsibility.

In this article, we will write a CookieManager and pass it shared_preferences Implement Cookie persistence.

thinking

To reduce code intrusion, using interceptors is a good choice. Dio officially provides an implementation example of a custom interceptor class:

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);
  }
}

We can define an interceptor to process cookies in the interceptor.

  • Check whether there is a cookie in onResponse. If there is, save it. however
  • In onRequest, submit with a cookie.

Then add it to Dio's interceptor. It looks very simple. Open!

Handwritten CookieManager

Define a CookieManager class, which inherits from interceptor and makes it into singleton mode. The following code does not do persistence management. The main business logic is as follows:

  • Single example implementation of Dart: you need to define the constructor as a private method and use {class name}_ Just declare privateConstructor().
  • Move the code for handling cookies after successful login in onReponse. If the returned status code is 200 and there are cookies, store the cookie information in the cookie manager_ Cookie string. If the returned status code is 401, the login session has expired and_ Cookie empty.
  • In onRequest, the_ The cookie is added to the cookie field to submit the request with the 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);
  }
}

Then remove the login, logout, and setCookie and clearCookie methods of HttpUtil. In this way, HttpUtil will not be exposed to the UI layer. At the same time, add the singleton object of CookieManager to the interceptor of Dio in HttpUtil.

static Dio getDioInstance() {
    if (_dioInstance == null) {
      _dioInstance = Dio();
      _dioInstance.interceptors.add(CookieManager.instance);
    }

    return _dioInstance;
}

Run it. The effect is the same as that in the previous article. Next, let's do persistence.

SharedPreferences persistence

SharedPreferences is a simple key value pair persistence tool, which corresponds to the native SharedPreferences of Android and NSUserDefaults of iOS. The reason why the name follows Android instead of iOS may be because Flutter and Android have a common father.
SharedPreferences supports the following Boolean, integer, floating point, string, and string arrays. If you want to store objects, you can also store them as json serialization. In addition, SharedPreferences is an asynchronous operation because it involves I/O operations.

It is simple to use:

SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
await prefs.setInt('counter', counter);

We can use SharedPreferences to achieve persistence when updating after obtaining a new cookie. Now pubspec Add dependencies to yaml because our current Flutter SDK is 2.0 6. Select 0.5 Version 7.

Add three methods in CookieManager:

  • initCookie: read the offline stored cookie into memory. This method should be executed in the startup phase.
  • _ Persistent cookie: persistent storage of cookies. Here, in order to reduce unnecessary I/O operations, it is only persistent when the cookies change.
  • _ clearCookie: clear cookies, including from memory and offline storage.
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');
}

Then, the cookie is processed in onResponse:

@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);
}

The method of initializing cookie is called in main. Here is a small detail:

  • If the original interaction is involved, it can not be called before runApp executes, because the native channel is not yet established. If you want to call, you must first call widgetsflutterbinding Ensureinitialized() ensures that the native channel has been established. Therefore, the way to initialize cookie s in the main method is as follows:
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  CookieManager.instance.initCookie();

  runApp(MyApp());
}
  • Another way is to call it after runApp and recommend it.
void main() {
  runApp(MyApp());
  CookieManager.instance.initCookie();
}

Operation results

We start the App first, log in and then log out, and then start it again to see if it is still logged in. We can see that the login is valid after starting it again.

summary

This article uses Dio's interceptor to implement a custom Cookie manager, and uses the SharedPreferences plug-in to realize Cookie offline caching. In fact, when we use DIO or develop other services, we can also refer to this method of interceptor, which can improve the reusability of code and reduce the coupling degree of code.

Topics: iOS Android Flutter