# flutter_common_utils **Repository Path**: xiyg/flutter_common_utils ## Basic Information - **Project Name**: flutter_common_utils - **Description**: 分模块介绍常用的插件和工具类 - **Primary Language**: Unknown - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-06-19 - **Last Updated**: 2022-06-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # flutter_common_utils #### Flutter Dio介绍 前言 Flutter中网络请求有三种实现方式 系统自带的HttpClient 网络请求第三方库Http 网络请求第三方库Dio 而Dio是目前比较流行的网络请求库,里面包含了很多如 Restful API 、拦截器 、自定义适配器实现无缝切换其他网络库 等操作。 目的 本文主要是对Dio网络请求库进行源码分析,最后会对Dio封装一个通用且使用方便的类。 本文中所用到的所有网络请求均为请求本地网络请求,目的是为了方便调试,由SpringBoot驱动。 Dio介绍 dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等。 关键词解释 1、Restful API 1.1 API API(Application Programming Interface,应用程序接口)是一些预先定义的函数,或指软件系统不同组成部分衔接的约定。 目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。 1.2 REST REST即表述性状态传递(英文:Representational State Transfer,简称REST)。简单来说,就是用URI表示资源,用HTTP方法(GET, POST, PUT, DELETE)表征对这些资源的操作。 2、FromData Dio支持发送 FormData, 请求数据将会以 multipart/form-data方式编码, FormData中可以一个或多个包含文件。主要目的用于文件上传操作。 注意: 只有 post 方法支持发送 FormData. 3、拦截器 每个 Dio 实例都可以添加任意多个拦截器,他们组成一个队列,拦截器队列的执行顺序是FIFO先进先出原则。通过拦截器你可以在请求之前、响应之后和发生异常时(但还没有被 then 或 catchError处理)做一些统一的预处理操作。 4、请求取消 可以通过 cancel token 来取消发起的请求。 5、Cookie管理 CookieManager 拦截器可以帮助我们自动管理请求、响应 cookie。 6、文件上传/下载 Dio支持单文件和多文件的上传 7、超时 Dio可设定超时时间,当在约定时间内未响应,将报错。 8、自定义适配器 内置的适配器可无缝切换到别的请求库而不用改之前的代码。 如何使用 步骤一:添加依赖 ``` dependencies: dio: ^4.0.0 ``` 步骤二:发送请求 ``` import 'package:dio/dio.dart'; void _sendDioGet() async { try { var response = await Dio().get('http://localhost:8080/getUserInfo'); print(response); } catch (e) { print(e); } } ``` 完整示例代码 ``` import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; class DioExample extends StatelessWidget { void _getUserInfo() async { try { var response = await Dio().get('http://localhost:8080/getUserInfo'); print(response); } catch (e) { print(e); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("DioExample"), ), body: Center( child: Column( children: [ TextButton( onPressed: _getUserInfo, child: Text("发送get请求"), ) ], ), ), ); } } ``` #### Flutter HttpClient、Http、Dio对比 前言 在前文中我们对Dio进行了基本介绍,也写了一个简单的示例,今天我们继续来讲一下Flutter 网络请求的三种请求方式的对比,以达到更好理解Dio网络请求库的目的。 系统自带网络请求HttpClient 步骤一:创建一个HttpClient `HttpClient httpClient = HttpClient();` 步骤二:打开http连接,设置请求头 ``` HttpClientRequest request = await httpClient.getUrl(Uri.parse("http://localhost:8080/getUserInfo")); ``` 步骤三:通过HttpClientRequest可以设置请求header ``` request.headers.add("token", "123456"); ``` 步骤四:等待连接服务器 ``` HttpClientResponse response = await request.close(); ``` 步骤五:读取响应内容 // 响应流数据以utf8编码格式返回 ``` String responseBody = await response.transform(utf8.decoder).join(); ``` 步骤六:请求结束,关闭httpClient ``` httpClient.close(); ``` 完整示例代码 ``` import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; class HttpClientExample extends StatelessWidget { @override Widget build(BuildContext context) { void _getUserInfo() async { try { // 1. 创建httpClient HttpClient httpClient = HttpClient(); // 2. 打开http连接,设置请求头 HttpClientRequest request = await httpClient.getUrl(Uri.parse("http://localhost:8080/getUserInfo")); // 3. 通过HttpClientRequest可以设置请求header request.headers.add("token", "123456"); // 4. 等待连接服务器 HttpClientResponse response = await request.close(); // 5. 读取响应内容 String responseBody = await response.transform(utf8.decoder).join(); // 6. 请求结束,关闭httpClient httpClient.close(); print(responseBody); } catch (e) { print(e); } } return Scaffold( appBar: AppBar( title: Text("DioExample"), ), body: Center( child: Column( children: [ TextButton( onPressed: _getUserInfo, child: Text("发送get请求"), ) ], ), ), ); } } ``` 第三方网络请求库Http 步骤一:添加依赖 ``` dependencies: http: ^0.13.3 #latest version ``` 步骤二:导入库 ``` import 'package:http/http.dart' as http; ``` 步骤三:发送请求 ``` var response = await http.post(Uri.parse("http://localhost:8080/getUserInfo")); ``` 完整实例代码 ``` import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; class HttpClientExample extends StatelessWidget { @override Widget build(BuildContext context) { void _getUserInfo() async { try { // 1. 创建httpClient HttpClient httpClient = HttpClient(); // 2. 打开http连接,设置请求头 HttpClientRequest request = await httpClient.getUrl(Uri.parse("http://localhost:8080/getUserInfo")); // 3. 通过HttpClientRequest可以设置请求header request.headers.add("token", "123456"); // 4. 等待连接服务器 HttpClientResponse response = await request.close(); // 5. 读取响应内容 String responseBody = await response.transform(utf8.decoder).join(); // 6. 请求结束,关闭httpClient httpClient.close(); print(responseBody); } catch (e) { print(e); } } return Scaffold( appBar: AppBar( title: Text("DioExample"), ), body: Center( child: Column( children: [ TextButton( onPressed: _getUserInfo, child: Text("发送get请求"), ) ], ), ), ); } } ``` 第三方网络请求库Dio 步骤一:添加依赖 ``` dependencies: dio: ^4.0.0 #latest version ``` 步骤二:导入库 ``` import 'package:dio/dio.dart'; ``` 步骤三:发送请求 ``` var response = await Dio().get('http://localhost:8080/getUserInfo'); ``` 完整示例代码 ``` import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; class DioExample extends StatelessWidget { void _getUserInfo() async { try { var response = await Dio().get('http://localhost:8080/getUserInfo'); print(response); } catch (e) { print(e); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("DioExample"), ), body: Center( child: Column( children: [ TextButton( onPressed: _getUserInfo, child: Text("发送get请求"), ) ], ), ), ); } } ``` 总结 原生HttpClient发起网络请求非常的复杂,很多东西还需自己手动处理。如果涉及到上传、下载、断点续传 等那肯定非常繁琐,不建议使用。再来说一下Dio 和 http 两个第三方组件,他们封装的功能都差不多,反而 Dio 更强大易用,而且从gitbub的Star来说,Dio10000 star,而http才691 star,该数据由2021年08月24日统计。 #### Flutter --深度剖析 介绍 在前面两篇文章中我们说了Dio的介绍以及对HttpClient、Http、Dio这三个网络请求的分析,这章节主要是对Dio 源码的分析。 从post请求来进行分析 ``` var response = await Dio().post('http://localhost:8080/login', queryParameters: { "username": "123456", "password": "123456" }); ``` post方法 post 方法有七个参数,在该函数中调用了request方法,并没有做任何处理,接下来我们看下request 方法。 path: 请求的url链接 data: 请求数据,例如上传用到的FromData queryParameters: 查询参数 options: 请求选项 cancelToken: 用来取消发送请求的token onSendProgress: 网络请求发送的进度 onReceiveProgress: 网络请求接收的进度 @override Future> post( String path, { data, Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) { return request( path, data: data, options: checkOptions('POST', options), queryParameters: queryParameters, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); } request方法 request 接收了post 方法中传进来的参数。 第一步:合并选项 通过调用compose 方法来进行选项合并。 compose函数执行流程 首先判断queryParameters 是否为空,不为空则添加到一个query 临时变量中 把options 中的headers 全部拿出来存到临时变量_headers中进行不区分大小写的映射,并删除headers 中的 contentTypeHeader 如果headers不为空,则把headers 中的全部属性添加到临时变量_headers 中并把contentTypeHeader赋值到一个临时变量_contentType中。 把options中的自定义字段extra 赋值给一个临时变量 把method统一转换成大写字母 创建一个RequestOptions并传入上面处理过的参数并返回 compose源码 ``` RequestOptions compose( BaseOptions baseOpt, String path, { data, Map? queryParameters, CancelToken? cancelToken, Options? options, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) { var query = {}; if (queryParameters != null) query.addAll(queryParameters); query.addAll(baseOpt.queryParameters); var _headers = caseInsensitiveKeyMap(baseOpt.headers); _headers.remove(Headers.contentTypeHeader); var _contentType; if (headers != null) { _headers.addAll(headers!); _contentType = _headers[Headers.contentTypeHeader]; } var _extra = Map.from(baseOpt.extra); if (extra != null) { _extra.addAll(extra!); } var _method = (method ?? baseOpt.method).toUpperCase(); var requestOptions = RequestOptions( method: _method, headers: _headers, extra: _extra, baseUrl: baseOpt.baseUrl, path: path, data: data, connectTimeout: baseOpt.connectTimeout, sendTimeout: sendTimeout ?? baseOpt.sendTimeout, receiveTimeout: receiveTimeout ?? baseOpt.receiveTimeout, responseType: responseType ?? baseOpt.responseType, validateStatus: validateStatus ?? baseOpt.validateStatus, receiveDataWhenStatusError: receiveDataWhenStatusError ?? baseOpt.receiveDataWhenStatusError, followRedirects: followRedirects ?? baseOpt.followRedirects, maxRedirects: maxRedirects ?? baseOpt.maxRedirects, queryParameters: query, requestEncoder: requestEncoder ?? baseOpt.requestEncoder, responseDecoder: responseDecoder ?? baseOpt.responseDecoder, listFormat: listFormat ?? baseOpt.listFormat, ); requestOptions.onReceiveProgress = onReceiveProgress; requestOptions.onSendProgress = onSendProgress; requestOptions.cancelToken = cancelToken; requestOptions.contentType = _contentType ?? contentType ?? baseOpt.contentTypeWithRequestBody(_method); return requestOptions; } ``` 第二步:调用fetch 判断用户是否关闭请求,关闭则退出,未关闭调用Fetch方法 request源码 ``` @override Future> request( String path, { data, Map? queryParameters, CancelToken? cancelToken, Options? options, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { options ??= Options(); var requestOptions = options.compose( this.options, path, data: data, queryParameters: queryParameters, onReceiveProgress: onReceiveProgress, onSendProgress: onSendProgress, cancelToken: cancelToken, ); requestOptions.onReceiveProgress = onReceiveProgress; requestOptions.onSendProgress = onSendProgress; requestOptions.cancelToken = cancelToken; if (_closed) { throw DioError( requestOptions: requestOptions, error: "Dio can't establish new connection after closed.", ); } return fetch(requestOptions); } ``` Fetch方法 第一步:请求参数赋值 判断如果传递进来的requestOptions.cancelToken 不为空的情况下,则把传递进来的requestOptions 进行赋值。 ``` if (requestOptions.cancelToken != null) { requestOptions.cancelToken!.requestOptions = requestOptions; } ``` 第二步:响应数据设定 如果请求回来的参数不是动态类型并且不是bytes和stream的方式,则进行判断该返回值类型是否是字符串,为真返回UTF-8的编码类型,否则返回字符串类型 ``` if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || requestOptions.responseType == ResponseType.stream)) { if (T == String) { requestOptions.responseType = ResponseType.plain; } else { requestOptions.responseType = ResponseType.json; } } ``` 第三步:构建请求流并添加拦截器 1、构建一个请求流,InterceptorState是一个内部类,里面总共与两个属性T data 以及 InterceptorResultType type ,用于当前拦截器和下一个拦截器之间传递状态所定义。 2、按 FIFO 顺序执行,循环遍历向请求流中添加请求拦截器,拦截器中最主要的有RequestInterceptor 请求前拦截和 ResponseInterceptor 请求后拦截的两个实例。 ``` var future = Future(() => InterceptorState(requestOptions)); interceptors.forEach((Interceptor interceptor) { future = future.then(_requestInterceptorWrapper(interceptor.onRequest)); }); ``` 第四步:拦截器转换为函数回调 这里主要做的一步操作是把函数的回调作为方法的参数,这样就实现了把拦截器转换为函数回调,这里做了一层判断,如果state.type 等于 next 的话,那么会增加一个监听取消的异步任务,并把cancelToken传递给了这个任务,接下来他会检查当前的这个拦截器请求是否入队,最后定义了一个请求拦截器的变量,该拦截器里面有三个主要的方法分别是next() 、resole() 、 reject() ,最后把这个拦截器返回出去。 ``` FutureOr Function(dynamic) _requestInterceptorWrapper( void Function( RequestOptions options, RequestInterceptorHandler handler, ) interceptor, ) { return (dynamic _state) async { var state = _state as InterceptorState; if (state.type == InterceptorResultType.next) { return listenCancelForAsyncTask( requestOptions.cancelToken, Future(() { return checkIfNeedEnqueue(interceptors.requestLock, () { var requestHandler = RequestInterceptorHandler(); interceptor(state.data, requestHandler); return requestHandler.future; }); }), ); } else { return state; } }; } ``` 第五步:构建请求流调度回调 调度回调和添加拦截器转换为函数回调,不同的是调度回调里面进行了请求分发。 ``` future = future.then(_requestInterceptorWrapper(( RequestOptions reqOpt, RequestInterceptorHandler handler, ) { requestOptions = reqOpt; _dispatchRequest(reqOpt).then( (value) => handler.resolve(value, true), onError: (e) { handler.reject(e, true); }, ); })); ``` 第六步:请求分发 1、请求分发函数里面会调用_transfromData 进行数据转换,最终转换出来的数据是一个 Stream 流。 2、调用网络请求适配器进行网络请求 fetch 方法,这里说明下该适配器定义有两个,分别如下: 2.1、BrowserHttpClientAdapter 是调用了html_dart2js 的库进行了网络请求,该库是将dart代码编译成可部署的JavaScript 2.2、DefaultHttpClientAdapter 是采用系统请求库HttpClient进行网络请求。 3、把响应头赋值给临时变量responseBody 并通过fromMap 转换成 Map> 类型 4、初始化响应类,并对返回的数据进行赋值处理。 5、判断如果是正常返回就对ret.data 变量进行数据格式转换,失败则取消监听响应流 6、检查请求是否通过cancelToken 变量取消了,如果取消了则直接抛出异常 7、最后在进行请求是否正常,如果正常则检查是否入队并返回,否则直接抛出请求异常DioError ``` Future> _dispatchRequest(RequestOptions reqOpt) async { var cancelToken = reqOpt.cancelToken; ResponseBody responseBody; try { var stream = await _transformData(reqOpt); responseBody = await httpClientAdapter.fetch( reqOpt, stream, cancelToken?.whenCancel, ); responseBody.headers = responseBody.headers; var headers = Headers.fromMap(responseBody.headers); var ret = Response( headers: headers, requestOptions: reqOpt, redirects: responseBody.redirects ?? [], isRedirect: responseBody.isRedirect, statusCode: responseBody.statusCode, statusMessage: responseBody.statusMessage, extra: responseBody.extra, ); var statusOk = reqOpt.validateStatus(responseBody.statusCode); if (statusOk || reqOpt.receiveDataWhenStatusError == true) { var forceConvert = !(T == dynamic || T == String) && !(reqOpt.responseType == ResponseType.bytes || reqOpt.responseType == ResponseType.stream); String? contentType; if (forceConvert) { contentType = headers.value(Headers.contentTypeHeader); headers.set(Headers.contentTypeHeader, Headers.jsonContentType); } ret.data = await transformer.transformResponse(reqOpt, responseBody); if (forceConvert) { headers.set(Headers.contentTypeHeader, contentType); } } else { await responseBody.stream.listen(null).cancel(); } checkCancelled(cancelToken); if (statusOk) { return checkIfNeedEnqueue(interceptors.responseLock, () => ret) as Response; } else { throw DioError( requestOptions: reqOpt, response: ret, error: 'Http status error [${responseBody.statusCode}]', type: DioErrorType.response, ); } } catch (e) { throw assureDioError(e, reqOpt); } } ``` download方法 download 方法的执行流程和post一样,只是接收的数据类型以及逻辑处理上不一样,会把下载的文件保存到本地,具体实现流程在 src>entry>dio_fornative.dart 文件中,这里不在做过多的赘述。 总结 在我们进行 get() post() 等调用时,都会进入到request方法,request 方法主要负责对请求参数以及自定义请求头的统一处理,并调用了fetch 方法,而 fetch 中是对响应数据设定、构建请求流、添加拦截器、请求分发的操作。 #### Flutter --封装 前言 本文会手把手教你该怎么去封装一个类库,平时在我们的工作中都是拿着别人的造好的轮子在使用,这篇文章将带你怎么去自己造轮子,以后再碰到别的类库需要对其进行封装的时候提供一个的思路和方法。 为什么需要封装Dio? 在前面的文章中,我们对Dio的基本使用、请求库对比、源码分析,我们知道Dio 的使用非常的简单,那为什么还需要进行封装呢?有两点如下: 1、代码迁移 当组件库方法发生重要改变需要迁移的时候如果有多处地方用到,那么需要对使用到的每个文件都进行修改,非常的繁琐而且很容易出问题。 2、请求库切换 当不需要Dio 库的时候,我们可以随时方便切换到别的网络请求库,当然Dio 目前内置支持使用第三方库的适配器。 3、统一配置 因为一个应用程序基本都是统一的配置方式,所以我们可以针对拦截器 、转换器 、 缓存 、统一处理错误 、代理配置、证书校验 等多个配置进行统一管理。 使用单例模式进行Dio封装 为什么使用单例模式? 因为我们的应用程序在每个页面中都会用到网络请求,那么如果我们每次请求的时候都去实例化一个Dio,无非是增加了系统不必要的开销,而使用单例模式对象一旦创建每次访问都是同一个对象,不需要再次实例化该类的对象。 创建单例类 这是通过静态变量的私有构造器来创建的单例模式 ``` class DioUtil { factory DioUtil() => _getInstance(); static DioUtil get instance => _getInstance(); static DioUtil _instance; DioUtil._init() { // 初始化 } static DioUtil _getInstance() { if (_instance == null) { _instance = DioUtil._init(); } return _instance; } } ``` 对Dio请求进行初始化 我们对 超时时间 、响应时间 、BaseUrl 进行统一设置 ``` /// 连接超时时间 static const int CONNECT_TIMEOUT = 60*1000; /// 响应超时时间 static const int RECEIVE_TIMEOUT = 60*1000; /// 声明Dio变量 Dio _dio; DioUtil._init() { if (_dio == null) { /// 初始化基本选项 BaseOptions options = BaseOptions( baseUrl: "http://localhost:8080", connectTimeout: CONNECT_TIMEOUT, receiveTimeout: RECEIVE_TIMEOUT ); /// 初始化dio _dio = Dio(options); } } ``` 对Restful APi风格进行统一封装 因为不管是get()还是post()请求,Dio 内部最终都会调用request 方法,只是传入的method 不一样,所以我们这里定义一个枚举类型在一个方法中进行处理 ``` enum DioMethod { get, post, put, delete, patch, head, } /// 请求类 Future request(String path, { DioMethod method = DioMethod.get, Map params, data, CancelToken cancelToken, Options options, ProgressCallback onSendProgress, ProgressCallback onReceiveProgress, }) async { const _methodValues = { DioMethod.get: 'get', DioMethod.post: 'post', DioMethod.put: 'put', DioMethod.delete: 'delete', DioMethod.patch: 'patch', DioMethod.head: 'head' }; options ??= Options(method: _methodValues[method]); try { Response response; response = await _dio.request(path, data: data, queryParameters: params, cancelToken: cancelToken, options: options, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress ); return response.data; } on DioError catch (e) { throw e; } } ``` 拦截器 介绍 我们已经把Restful API 风格简化成了一个方法,通过DioMethod 来标明不同的请求方式。在我们平时开发的过程中,需要在请求前、响应前、错误时对某一些接口做特殊的处理,那我们就需要用到拦截器。Dio 为我们提供了自定义拦截器功能,很容易轻松的实现对请求、响应、错误时进行拦截 错误统一处理 我们发现虽然Dio框架已经封装了一个DioError类库,但如果需要对返回的错误进行统一弹窗处理或者路由跳转等就只能自定义了 请求前统一处理 在我们发送请求的时候会碰到几种情况,比如需要对非open开头的接口自动加上一些特定的参数,获取需要在请求头增加统一的token 响应前统一处理 在我们请求接口前可以对响应数据进行一些基础的处理,比如对响应的结果进行自定义封装,还可以针对单独的url 做特殊处理等。 自定义拦截器实现 ``` import 'package:dio/dio.dart'; import 'package:flutter_dio/dio_util/dio_response.dart'; class DioInterceptors extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { // 对非open的接口的请求参数全部增加userId if (!options.path.contains("open")) { options.queryParameters["userId"] = "xxx"; } // 头部添加token options.headers["token"] = "xxx"; // 更多业务需求 handler.next(options); // super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { // 请求成功是对数据做基本处理 if (response.statusCode == 200) { response.data = DioResponse(code: 0, message: "请求成功啦", data: response); } else { response.data = DioResponse(code: 1, message: "请求失败啦", data: response); } // 对某些单独的url返回数据做特殊处理 if (response.requestOptions.baseUrl.contains("???????")) { //.... } // 根据公司的业务需求进行定制化处理 // 重点 handler.next(response); } @override void onError(DioError err, ErrorInterceptorHandler handler) { switch(err.type) { // 连接服务器超时 case DioErrorType.connectTimeout: { // 根据自己的业务需求来设定该如何操作,可以是弹出框提示/或者做一些路由跳转处理 } break; // 响应超时 case DioErrorType.receiveTimeout: { // 根据自己的业务需求来设定该如何操作,可以是弹出框提示/或者做一些路由跳转处理 } break; // 发送超时 case DioErrorType.sendTimeout: { // 根据自己的业务需求来设定该如何操作,可以是弹出框提示/或者做一些路由跳转处理 } break; // 请求取消 case DioErrorType.cancel: { // 根据自己的业务需求来设定该如何操作,可以是弹出框提示/或者做一些路由跳转处理 } break; // 404/503错误 case DioErrorType.response: { // 根据自己的业务需求来设定该如何操作,可以是弹出框提示/或者做一些路由跳转处理 } break; // other 其他错误类型 case DioErrorType.other: { } break; } super.onError(err, handler); } } class DioResponse { /// 消息(例如成功消息文字/错误消息文字) final String message; /// 自定义code(可根据内部定义方式) final int code; /// 接口返回的数据 final T data; /// 需要添加更多 /// ......... DioResponse({ this.message, this.data, this.code, }); @override String toString() { StringBuffer sb = StringBuffer('{'); sb.write("\"message\":\"$message\""); sb.write(",\"errorMsg\":\"$code\""); sb.write(",\"data\":\"$data\""); sb.write('}'); return sb.toString(); } } class DioResponseCode { /// 成功 static const int SUCCESS = 0; /// 错误 static const int ERROR = 1; /// 更多 } ``` 转换器 介绍 转换器Transformer 用于对请求数据和响应数据进行编解码处理。Dio实现了一个默认转换器DefaultTransformer作为默认的 Transformer. 如果想对请求/响应数据进行自定义编解码处理,可以提供自定义转换器 为什么需要转换器? 我们看了转换器的介绍,发现和拦截器的功能差不多,那为什么还要存在转换器,有两点: 和拦截器解耦 不修改原始请求数据 执行流程:请求拦截器 » 请求转换器 » 发起请求 » 响应转换器 » 响应拦截器 » 最终结果。 请求转换器 只会被用于 ‘PUT’、 ‘POST’、 ‘PATCH’方法,因为只有这些方法才可以携带请求体(request body) 响应转换器 会被用于所有请求方法的返回数据。 自定义转换器实现 ``` import 'dart:async'; import 'package:dio/dio.dart'; class DioTransformer extends DefaultTransformer { @override Future transformRequest(RequestOptions options) async { // 如果请求的数据接口是List那我们直接抛出异常 if (options.data is List) { throw DioError( error: "你不能直接发送List数据到服务器", requestOptions: options, ); } else { return super.transformRequest(options); } } @override Future transformResponse(RequestOptions options, ResponseBody response) async { // 例如我们响应选项里面没有自定义某些头部数据,那我们就可以自行添加 options.extra['myHeader'] = 'abc'; return super.transformResponse(options, response); } } ``` 刷新Token 在开发过程中,客户端和服务器打交道的时候,往往会用一个token来做校验,因为每个公司处理刷新token的逻辑都不一样,我这里举一个简单的例子 我们需要给所有的请求头中添加一个refreshToken,如果refreshToken不存在,我们先去请求refreshToken,获取到refreshToken后,再发起后续请求。 由于请求refreshToken的过程是异步的,我们需要在请求过程中锁定后续请求(因为它们需要refreshToken), 直到refreshToken请求成功后,再解锁 ``` import 'package:dio/dio.dart'; import 'package:flutter_dio/dio_util/dio_util.dart'; class DioTokenInterceptors extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (options.headers['refreshToken'] == null) { DioUtil.instance.dio.lock(); Dio _tokenDio = Dio(); _tokenDio..get("http://localhost:8080/getRefreshToken").then((d) { options.headers['refreshToken'] = d.data['data']['token']; handler.next(options); }).catchError((error, stackTrace) { handler.reject(error, true); }) .whenComplete(() { DioUtil.instance.dio.unlock(); }); // unlock the dio } else { options.headers['refreshToken'] = options.headers['refreshToken']; handler.next(options); } } @override void onResponse(Response response, ResponseInterceptorHandler handler) async { // 响应前需要做刷新token的操作 super.onResponse(response, handler); } @override void onError(DioError err, ErrorInterceptorHandler handler) { super.onError(err, handler); } } ``` 取消请求 为什么我们需要有取消请求的功能,如果当我们的页面在发送请求时,用户主动退出当前界面或者app应用程序退出的时候数据还没有响应,那我们就需要取消该网络请求,防止不必要的错误。 ``` /// 取消请求token CancelToken _cancelToken = CancelToken(); /// 取消网络请求 void cancelRequests({CancelToken token}) { token ?? _cancelToken?.cancel("cancelled"); } ``` cookie管理 cookie介绍 由服务器生成的一小段文本信息,发送给浏览器,浏览器把 cookie 以kv形式保存到本地某个目录下的文本文件内,下一次请求同一网站时会把该 cookie 发送给服务器。 原理 客户端发送一个请求(http请求+用户认证信息)到服务器 认证成功,服务器发送一个HttpResponse响应到客户端,其中包含Set-Cookie的头部 客户端提取并保存 cookie 于内存或磁盘 再次请求时,HttpRequest请求中会包含一个已认证的 Cookie 的头部 服务器解析cookie,获取 cookie 中客户端的相关信息 服务器返回响应数据 使用 cookie 的使用需要用到两个第三方组件 dio_cookie_manager 和 cookie_jar cookie_jar:Dart 中 http 请求的 cookie 管理器,通过它您可以轻松处理复杂的 cookie 策略和持久化 cookie dio_cookie_manager: CookieManager 拦截器可以帮助我们自动管理请求/响应 cookie。 CookieManager 依赖于 cookieJar 包 导入文件 ``` dio_cookie_manager: ^2.0.0 cookie_jar: ^3.0.1 /// cookie CookieJar cookieJar = CookieJar(); /// 添加cookie管理器 _dio.interceptors.add(CookieManager(cookieJar)); List cookies = [ Cookie("xxx", xxx), // .... ]; //Save cookies DioUtil.instance.cookieJar.saveFromResponse(Uri.parse(BaseUrl.url), cookies); //Get cookies List cookies = DioUtil.instance.cookieJar.loadForRequest(Uri.parse(BaseUrl.url)); ``` 网络接口缓存 为什么使用缓存? 因为在我们平时的开发过程中,会碰到一种情况,在进行网络请求时,我们希望能正常访问到上次的数据,对于用户的体验比较好,而不是展示一个空白的页面,该缓存主要是 《Flutter实战》网络接口缓存 提供参考。 使用shared_preferences持久化 我们在程序退出后内存缓存将会消失,所以我们用shared_preferences 进行磁盘缓存数据。 ``` import 'dart:collection'; import 'package:dio/dio.dart'; import 'package:flutter_dio/dio_util/dio_util.dart'; class CacheObject { CacheObject(this.response) : timeStamp = DateTime.now().millisecondsSinceEpoch; Response response; int timeStamp; @override bool operator ==(other) { return response.hashCode == other.hashCode; } @override int get hashCode => response.realUri.hashCode; } class DioCacheInterceptors extends Interceptor { // 为确保迭代器顺序和对象插入时间一致顺序一致,我们使用LinkedHashMap var cache = LinkedHashMap(); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (!DioUtil.CACHE_ENABLE) return super.onRequest(options, handler); // 通过refresh字段来判断是否刷新缓存 bool refresh = options.extra["refresh"] == true; if (refresh) { // 删除本地缓存 delete(options.uri.toString()); } // 只有get请求才开启缓存 if (options.extra["noCache"] != true && options.method.toLowerCase() == 'get') { String key = options.extra["cacheKey"] ?? options.uri.toString(); var ob = cache[key]; if (ob != null) { //若缓存未过期,则返回缓存内容 if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 < DioUtil.MAX_CACHE_AGE) { return handler.resolve(cache[key].response); } else { //若已过期则删除缓存,继续向服务器请求 cache.remove(key); } } } super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { // 把响应的数据保存到缓存 if (DioUtil.CACHE_ENABLE) { _saveCache(response); } super.onResponse(response, handler); } @override void onError(DioError err, ErrorInterceptorHandler handler) { // TODO: implement onError super.onError(err, handler); } _saveCache(Response object) { RequestOptions options = object.requestOptions; if (options.extra["noCache"] != true && options.method.toLowerCase() == "get") { // 如果缓存数量超过最大数量限制,则先移除最早的一条记录 if (cache.length == DioUtil.MAX_CACHE_COUNT) { cache.remove(cache[cache.keys.first]); } String key = options.extra["cacheKey"] ?? options.uri.toString(); cache[key] = CacheObject(object); } } void delete(String key) { cache.remove(key); } } ``` 代理配置 在我们用flutter进行抓包的时候需要配置Dio代理。由DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 HttpClient的代理。 ``` /// 设置Http代理(设置即开启) void setProxy({ String proxyAddress, bool enable = false }) { if (enable) { (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) { client.findProxy = (uri) { return proxyAddress; }; client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; }; } } ``` 证书校验 用于验证正在访问的网站是否真实。提供安全性,因为证书和域名绑定,并且由根证书机构签名确认。 ``` /// 设置https证书校验 void setHttpsCertificateVerification({ String pem, bool enable = false }) { if (enable) { (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { client.badCertificateCallback=(X509Certificate cert, String host, int port){ if(cert.pem==pem){ // 验证证书 return true; } return false; }; }; } } ``` 统一日志打印 日志打印主要是帮助我们开发时进行辅助排错 ``` /// 开启日志打印 void openLog() { _dio.interceptors.add(LogInterceptor(responseBody: true)); } DioUtil().openLog(); ``` # Flutter shared_preferences Flutter shared_preferences的基本使用封装 前言 本文是基于官方最新稳定版本^2.0.8进行开发 目的 本文主要对shared_preferences: ^2.0.8的作用以及基本使用来进行源码分析,最终会封装一个比较通用的类库,因为2.0以上版本是空安全,所以后面讲的所有代码以及封装都是基于空安全的。 shared_preferences介绍 shared_preferences主要的作用是用于将数据异步持久化到磁盘,因为持久化数据只是存储到临时目录,当app删除时该存储的数据就是消失,web开发时清除浏览器存储的数据也将消失。 支持存储类型: bool int double string stringList shared_preferences应用场景 主要用于持久化数据,如持久化用户信息、列表数据等。 持久化用户信息 因为用户信息基本是不改变的,而在一个应用程序中常常会有多个页面需要展示用户信息,我们不可能每次都去获取接口,那么本地持久化就会变得很方便。 持久化列表数据 为了给用户更好的体验,在获取列表数据时我们常常会先展示旧数据,带给用户更好的体验,不至于一打开页面就是空白的,当我们采用持久化列表数据后,可以直接先展示本地数据,当网络数据请求回来后在进行数据更新。 shared_preferences使用的对应类库 我们知道每个平台持久化数据的方式都不一样,而shared_preferences针对不同的平台封装了一个通用的类库,接下来我们看看不同平台下他们使用的库: iOS: NSUserDefaults Android: SharedPreferences Web: localStorage Linux: FileSystem(保存数据到本地系统文件库中) Mac OS: FileSystem(保存数据到本地系统文件库中) Windows: FileSystem(保存数据到本地系统文件库中) shared_preferences基本使用 pubspec.yaml导入依赖 ``` shared_preferences: ^2.0.8 ``` 导入头文件 ``` import 'package:shared_preferences/shared_preferences.dart'; ``` 获取实例对象 ``` SharedPreferences? sharedPreferences = await SharedPreferences.getInstance(); ``` 设置持久化数据 我们可以通过sharedPreferences的实例化对象调用对应的set方法设置持久化数据 ``` SharedPreferences? sharedPreferences; // 设置持久化数据 void _setData() async { // 实例化 sharedPreferences = await SharedPreferences.getInstance(); // 设置string类型 await sharedPreferences?.setString("name", "Jimi"); // 设置int类型 await sharedPreferences?.setInt("age", 18); // 设置bool类型 await sharedPreferences?.setBool("isTeacher", true); // 设置double类型 await sharedPreferences?.setDouble("height", 1.88); // 设置string类型的数组 await sharedPreferences?.setStringList("action", ["吃饭", "睡觉", "打豆豆"]); setState(() {}); } ``` 读取持久化数据 我们可以通过sharedPreferences的实例化对象调用对应的get方法读取持久化数据 ``` Text("名字: ${sharedPreferences?.getString("name") ?? ""}", style: TextStyle( color: Colors.blue, fontSize: 20 ), ), SizedBox(height: 20,), Text("年龄: ${sharedPreferences?.getInt("age") ?? ""}", style: TextStyle( color: Colors.red, fontSize: 20 ), ), SizedBox(height: 20,), Text("是老师吗?: ${sharedPreferences?.getBool("isTeacher") ?? ""}", style: TextStyle( color: Colors.orange, fontSize: 20 ), ), SizedBox(height: 20,), Text("身高: ${sharedPreferences?.getDouble("height") ?? ""}", style: TextStyle( color: Colors.pink, fontSize: 20 ), ), SizedBox(height: 20,), Text("我正在: ${sharedPreferences?.getStringList("action") ?? ""}", style: TextStyle( color: Colors.purple, fontSize: 20 ), ), ``` 完整代码 ``` import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SharedPreferencesExample extends StatefulWidget { @override _SharedPreferencesExampleState createState() => _SharedPreferencesExampleState(); } class _SharedPreferencesExampleState extends State { SharedPreferences? sharedPreferences; // 设置持久化数据 void _setData() async { // 实例化 sharedPreferences = await SharedPreferences.getInstance(); // 设置string类型 await sharedPreferences?.setString("name", "Jimi"); // 设置int类型 await sharedPreferences?.setInt("age", 18); // 设置bool类型 await sharedPreferences?.setBool("isTeacher", true); // 设置double类型 await sharedPreferences?.setDouble("height", 1.88); // 设置string类型的数组 await sharedPreferences?.setStringList("action", ["吃饭", "睡觉", "打豆豆"]); setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("SharedPreferences"), ), floatingActionButton: FloatingActionButton( onPressed: _setData, child: Icon(Icons.add), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text("名字: ${sharedPreferences?.getString("name") ?? ""}", style: TextStyle( color: Colors.blue, fontSize: 20 ), ), SizedBox(height: 20,), Text("年龄: ${sharedPreferences?.getInt("age") ?? ""}", style: TextStyle( color: Colors.red, fontSize: 20 ), ), SizedBox(height: 20,), Text("是老师吗?: ${sharedPreferences?.getBool("isTeacher") ?? ""}", style: TextStyle( color: Colors.orange, fontSize: 20 ), ), SizedBox(height: 20,), Text("身高: ${sharedPreferences?.getDouble("height") ?? ""}", style: TextStyle( color: Colors.pink, fontSize: 20 ), ), SizedBox(height: 20,), Text("我正在: ${sharedPreferences?.getStringList("action") ?? ""}", style: TextStyle( color: Colors.purple, fontSize: 20 ), ), ], ), ), ); } } ``` shared_preferences辅助操作 获取持久化数据中所有存入的key ``` List keys = sharedPreferences?.getKeys().toList() ?? []; print(keys); ``` // 控制台输出 [name, age, isTeacher, height, action] 判断持久化数据中是否包含某个key ``` bool isContainKey = sharedPreferences?.containsKey("name") ?? false; print(isContainKey); ``` // 控制台输出 flutter: true 删除持久化数据中某个key ``` bool isRemoveKey = await sharedPreferences?.remove("name") ?? false; print(isRemoveKey); ``` // 控制台输出 flutter: true 清除所有持久化数据 ``` bool isClearAllKey = await sharedPreferences?.clear() ?? false; print(isClearAllKey); ``` // 控制台输出 flutter: true 重新加载所有数据(仅重载运行时) ``` await sharedPreferences?.reload(); ``` shared_preferences源码分析 实例化对象源码分析 接下来我们来对shared_preferences进行分析,我们在使用的时候需要通过getInstance实例化一个对象,接下来我们看下这里面它都做了什么操作。 静态变量分析 我们先来看下它定义了三个静态变量: _prefix: 设置持久化数据和读取持久化数据时统一设置前缀(flutter.) _completer: 持久化数据异步通知,就是当shared_preferences实例化完成后通过completer.future来返回结果 _manualDartRegistrationNeeded: 是否需要手动注册,因为涉及到Linux、Windows、Mac Os的持久化数据时,是需要手动进行注册的,默认为true static const String _prefix = 'flutter.'; static Completer? _completer; static bool _manualDartRegistrationNeeded = true; getInstance()源码分析 当我们获取实例化对象时先判断_completer 是否为空,如果不为空则直接返回它的的future结果,否则它会实例化一个SharedPreferences的Completer对象,然后通过_getSharedPreferencesMap来获取持久化的map对象,获取到map对象后,通过completer.complete(SharedPreferences._(preferencesMap))将Map结果返回出去,代码如下: static Future getInstance() async { if (_completer == null) { final completer = Completer(); try { final Map preferencesMap = await _getSharedPreferencesMap(); completer.complete(SharedPreferences._(preferencesMap)); } on Exception catch (e) { // If there's an error, explicitly return the future with an error. // then set the completer to null so we can retry. completer.completeError(e); final Future sharedPrefsFuture = completer.future; _completer = null; return sharedPrefsFuture; } _completer = completer; } return _completer!.future; } _getSharedPreferencesMap()源码分析 在我们调用getInstance()方法里,会调用_getSharedPreferencesMap()来获取持久化的Map数据,我们接下来看看它是如何获取的,首先它通过_store.getAll()就可以直接获取到本地的所有持久化数据,当我们调用_store时,它会判断是否需要手动注册 不需要手动注册时: 在iOS、Android等平台中使用不需要手动注册,所以它直接就返回的对应的实例对象 需要手动注册时: 先判断是否是web,如果是就返回localStorage,否则判断是Linux还是Windows,然后根据平台的不同返回其对应的实例。 static Future> _getSharedPreferencesMap() async { final Map fromSystem = await _store.getAll(); assert(fromSystem != null); // Strip the flutter. prefix from the returned preferences. final Map preferencesMap = {}; for (String key in fromSystem.keys) { assert(key.startsWith(_prefix)); preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; } return preferencesMap; } static SharedPreferencesStorePlatform get _store { // TODO(egarciad): Remove once auto registration lands on Flutter stable. // https://github.com/flutter/flutter/issues/81421. if (_manualDartRegistrationNeeded) { // Only do the initial registration if it hasn't already been overridden // with a non-default instance. if (!kIsWeb && SharedPreferencesStorePlatform.instance is MethodChannelSharedPreferencesStore) { if (Platform.isLinux) { SharedPreferencesStorePlatform.instance = SharedPreferencesLinux(); } else if (Platform.isWindows) { SharedPreferencesStorePlatform.instance = SharedPreferencesWindows(); } } _manualDartRegistrationNeeded = false; } return SharedPreferencesStorePlatform.instance; } _setValue()源码分析 不管我们是存储什么内容的数据,最终都会调用_setValue()来进行存储, 首先它会检查存入的value是否为空,如果为空就抛出异常,否则就用_prefix + key来作为存入的key值。 判断存入的值是不是List,如果是先把value通过toList()方法转换,然后在存入,否则直接存入,这步存入操作只是存入缓存中,当应用程序退出时将消失 最后通过_store来异步写入到磁盘中 Future _setValue(String valueType, String key, Object value) { ArgumentError.checkNotNull(value, 'value'); final String prefixedKey = '$_prefix$key'; if (value is List) { // Make a copy of the list so that later mutations won't propagate _preferenceCache[key] = value.toList(); } else { _preferenceCache[key] = value; } return _store.setValue(valueType, prefixedKey, value); } shared_preferences封装 我们在使用shared_preferences时每次都需要去获取它的实例,如果多个地方用到,那么每次都要实例化一次。这样代码的可读性差,后期的维护成本也变得很高,而且还不支持存储Map类型,所以接下来我们对shared_preferences来封装一个通用而且使用更简单的库。 ##### 使用单例模式进行shared_preferences封装 因为我们获取的都是同一个实例,所以采用单例模式来进行封装最好,而且获取实例是异步的,所以我们在应用程序启动时先初始化,这样使用起来更加的方便。 创建单例类 因为我们代码都是采用了空安全,所以空安全里面有个非常重要的属性late来延迟加载我们实例,如下: ``` class JSpUtil { JSpUtil._internal(); factory JSpUtil() => _instance; static late final JSpUtil _instance = JSpUtil._internal(); } ``` 初始化shared_preferences 因为采用单例模式,所以在获取唯一实例的时候我们在入口统一获取一次即可。 ``` static late SharedPreferences _preferences; static Future getInstance() async { _preferences = await SharedPreferences.getInstance(); return _instance; } ``` 封装方式一:对应get、set方法 ``` /// 根据key存储int类型 static Future setInt(String key, int value) { return _preferences.setInt(key, value); } /// 根据key获取int类型 static int? getInt(String key, {int defaultValue = 0}) { return _preferences.getInt(key) ?? defaultValue; } /// 根据key存储double类型 static Future setDouble(String key, double value) { return _preferences.setDouble(key, value); } /// 根据key获取double类型 static double? getDouble(String key, {double defaultValue = 0.0}) { return _preferences.getDouble(key) ?? defaultValue; } /// 根据key存储字符串类型 static Future setString(String key, String value) { return _preferences.setString(key, value); } /// 根据key获取字符串类型 static String? getString(String key, {String defaultValue = ""}) { return _preferences.getString(key) ?? defaultValue; } /// 根据key存储布尔类型 static Future setBool(String key, bool value) { return _preferences.setBool(key, value); } /// 根据key获取布尔类型 static bool? getBool(String key, {bool defaultValue = false}) { return _preferences.getBool(key) ?? defaultValue; } /// 根据key存储字符串类型数组 static Future setStringList(String key, List value) { return _preferences.setStringList(key, value); } /// 根据key获取字符串类型数组 static List getStringList(String key, {List defaultValue = const []}) { return _preferences.getStringList(key) ?? defaultValue; } /// 根据key存储Map类型 static Future setMap(String key, Map value) { return _preferences.setString(key, json.encode(value)); } /// 根据key获取Map类型 static Map getMap(String key) { String jsonStr = _preferences.getString(key) ?? ""; return jsonStr.isEmpty ? Map : json.decode(jsonStr); } ``` 封装方式二:统一set、get方法 ``` /// 通用设置持久化数据 static setLocalStorage(String key, T value) { String type = value.runtimeType.toString(); switch (type) { case "String": setString(key, value as String); break; case "int": setInt(key, value as int); break; case "bool": setBool(key, value as bool); break; case "double": setDouble(key, value as double); break; case "List": setStringList(key, value as List); break; case "_InternalLinkedHashMap": setMap(key, value as Map); break; } } /// 获取持久化数据 static dynamic getLocalStorage(String key) { dynamic value = _preferences.get(key); if (value.runtimeType.toString() == "String") { if (_isJson(value)) { return json.decode(value); } } return value; } ``` 其他辅助方法封装 ``` /// 获取持久化数据中所有存入的key static Set getKeys() { return _preferences.getKeys(); } /// 获取持久化数据中是否包含某个key static bool containsKey(String key) { return _preferences.containsKey(key); } /// 删除持久化数据中某个key static Future remove(String key) async { return await _preferences.remove(key); } /// 清除所有持久化数据 static Future clear() async { return await _preferences.clear(); } /// 重新加载所有数据,仅重载运行时 static Future reload() async { return await _preferences.reload(); } /// 判断是否是json字符串 static _isJson(String value) { try { JsonDecoder().convert(value); return true; } catch(e) { return false; } } ``` 两种封装的使用方式 ``` // 设置String类型 await JSpUtil.setString("name", "Jimi"); // 设置int类型 await JSpUtil.setInt("age", 18); // 设置bool类型 await JSpUtil.setBool("isTeacher", true); // 设置double类型 await JSpUtil.setDouble("height", 1.88); // 设置string类型的数组 await JSpUtil.setStringList("action", ["吃饭", "睡觉", "打豆豆"]); // 设置Map类型 await JSpUtil.setMap("weight", {"weight": 112}); JSpUtil.setLocalStorage("name", "Jimi"); JSpUtil.setLocalStorage("age", 18); JSpUtil.setLocalStorage("isTeacher", true); JSpUtil.setLocalStorage("height", 1.88); JSpUtil.setLocalStorage("action", ["吃饭", "睡觉", "打豆豆"]); JSpUtil.setLocalStorage("weight", {"weight": "112"}); JSpUtil.getLocalStorage("name"); JSpUtil.getLocalStorage("age"); JSpUtil.getLocalStorage("isTeacher"); JSpUtil.getLocalStorage("height"); JSpUtil.getLocalStorage("action"); JSpUtil.getLocalStorage("weight"); // 获取磁盘中所有存入的key List keys = JSpUtil.getKeys().toList(); print(keys); // 持久化数据中是否包含某个key bool isContainKey = JSpUtil.containsKey("name"); print(isContainKey); // 删除持久化数据中某个key bool isRemoveKey = await JSpUtil.remove("name"); print(isRemoveKey); // 清除所有持久化数据 bool isClearAllKey = await JSpUtil.clear(); print(isClearAllKey); // 重新加载所有数据,仅重载运行时 await JSpUtil.reload(); ``` 获取值的方式 ``` Text("名字: ${JSpUtil.getString("name")}", style: TextStyle( color: Colors.blue, fontSize: 20 ), ), SizedBox(height: 20,), Text("年龄: ${JSpUtil.getInt("age")}", style: TextStyle( color: Colors.red, fontSize: 20 ), ), SizedBox(height: 20,), Text("是老师吗?: ${JSpUtil.getBool("isTeacher")}", style: TextStyle( color: Colors.orange, fontSize: 20 ), ), SizedBox(height: 20,), Text("身高: ${JSpUtil.getDouble("height")}", style: TextStyle( color: Colors.pink, fontSize: 20 ), ), SizedBox(height: 20,), Text("我正在: ${JSpUtil.getStringList("action")}", style: TextStyle( color: Colors.purple, fontSize: 20 ), ), ``` 总结 当我们需要对数据进行持久化储存的时候,我们可以采用shared_preferences来进行储存到磁盘,这样app启动时可访问储存后的数据,而且支持存储多种类型。 # Flutter 数据库Sqlite 学习本章节前必须要掌握基本的SQL语句,如果对SQlite和SQL的各种语法还不熟悉,那么可以查看SQlite的官方教程SQlite英文教程 SQLite中文教程 sqflite介绍 sqflite是Flutter的SQLite插件,它能在App端能够高效的存储和处理数据库数据,目前sqflite支持的平台有iOS、Android、MacOS,如果你的应用为桌面应用,可以尝试使用sqflite_common_ffi,sqflite具备以下优势: 支持事务和批量 自动版本管理 插入、查询、更新、删除助手 轻量级 存储单一跨平台磁盘文件 不依赖任何外部依赖 sqflite支持的数据类型 总共支持五种数据类型,如下: NULL 不存储数据时,默认值为NULL INTEGER dart中的int类型,取值范围 -2^63 to 2^63 - 1 REAL dart中的num类型 TEXT dart中的String类型 BLOB dart中的Uint8List 如果需要存储其他类型,比如DateTime,我们可以把他转换为毫秒级时间戳或者string进行存储。bool可以使用整数0和1代替,还有想List、Map这类可以先转换成字符串在进行存储。 ROM 如果大家需要对Sqflite更简单的使用,可以使用ROM封装的组件库sqfentity 数据库操作 所有的数据库操作都在iOS模拟器运行。 定义变量 关于该变量为全局变量,所有使用database的调用都是基于该变量。 1 late Database database; 打开数据库 我们可以通过openDatabase()方式来打开数据库,这里注意一点,当我们打开数据库时如果发现数据库文件不存在,那么就会默认创建,iOS的目录是doucuments,Android是默认的数据库目录。 构造函数 ``` Future openDatabase(String path, {int? version, OnDatabaseConfigureFn? onConfigure, OnDatabaseCreateFn? onCreate, OnDatabaseVersionChangeFn? onUpgrade, OnDatabaseVersionChangeFn? onDowngrade, OnDatabaseOpenFn? onOpen, bool readOnly = false, bool singleInstance = true}) { final options = OpenDatabaseOptions( version: version, onConfigure: onConfigure, onCreate: onCreate, onUpgrade: onUpgrade, onDowngrade: onDowngrade, onOpen: onOpen, readOnly: readOnly, singleInstance: singleInstance); return databaseFactory.openDatabase(path, options: options); } ``` 详细描述 字段 属性 描述 version int? 数据库的版本号 onConfigure OnDatabaseConfigureFn? 数据库初始化配置(启用外键、预写日志等) onCreate OnDatabaseCreateFn? 数据库不存在时回调,可用于创建所需的表 onUpgrade OnDatabaseVersionChangeFn? 数据库升级时回调 onDowngrade OnDatabaseVersionChangeFn? 数据库版本过低,需要更新版本时调用 onOpen OnDatabaseOpenFn? 打开数据库时回调,在openDatabase返回之前 readOnly bool 数据库是否只读,默认false singleInstance bool 是否返回数据库的路径,默认ture 使用方式 ``` void _openDatabase() async { database = await openDatabase("person.db"); print(database); } ``` 控制台输出 flutter: 1 /Users/jm/Library/Developer/CoreSimulator/Devices/C41CF6D8-1782-4B64-9398-D70CA8514AEC/data/Containers/Data/Application/C5D36DB4-1F9F-40D4-A358-B21B9ED04874/Documents/person.db 数据库是否打开 我们可以通过调用isOpen方法来判断数据库是否被打开。 使用方式 ``` void _isOpenDataBase() async { print(database.isOpen); } ``` 控制台输出 flutter: false 关闭数据库 当我们不需要使用数据库的时候,一定要关闭数据库,否则应用程序永远不会关闭,知道应用程序终止时才会关闭。 我们先打开数据库,然后调用_closeDatabase()方法。 使用方式 ``` void _closeDatabase() async { print(database.isOpen); await database.close(); print(database.isOpen); } ``` 控制台输出 flutter: 4 /Users/jm/Library/Developer/CoreSimulator/Devices/C41CF6D8-1782-4B64-9398-D70CA8514AEC/data/Containers/Data/Application/C5D36DB4-1F9F-40D4-A358-B21B9ED04874/Documents/person.db flutter: true flutter: false 删除数据库 当我们不在需要该数据库时可进行删除,删除数据库其实就是对文件的删除, 使用方式 这里都是通过path_provider来获,在我们将文件读写的时候用到了一个path_provider获取到document目录,而不是这样写死路径,这里只是演示如果删除数据库。 void _deleteDatabase() async { await deleteDatabase("/Users/jm/Library/Developer/CoreSimulator/Devices/C41CF6D8-1782-4B64-9398-D70CA8514AEC/data/Containers/Data/Application/C5D36DB4-1F9F-40D4-A358-B21B9ED04874/Documents/person.db"); } 表操作 重点说一下Sqflite中的几个方法: 方法 属性 描述 execute() Future 执行没有返回值的原始sql语句 insert() Future 执行sql插入语句,通过Map映射插入 delete() Future 执行sql删除语句,通过where或者whereArgs update() Future 执行sql更新语句,可通过Map映射查询 query() Future 执行sql查询语句,以及很方便的对查到的数据进行过滤(分组、排序、限制等) rawInsert() Future 执行原始sql插入语句 rawDelete() Future 执行原始sql删除语句 rawUpdate() Future 执行原始sql更新语句 rawQuery() Future 执行原始sql查询语句 创建表 创建表的方式有两种,一种在打开数据库的时候即创建,还有一种就是通过execute()执行语句来创建,下面我创建了人的表,具体含义如下: id: 是Dart的int类型,和数据库表中对应的是INTEGER类型,并且我把id设置成了自增以及主键。 name: 是DartString类型,和数据库表中对应的是TEXT类型,存储人的姓名。 age: 是Dart的int类型,和数据库表中对应的是INTEGER类型,存储人的年龄。 使用方法 ``` void _createTable() async { database.execute("CREATE TABLE person(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)"); } ``` 删除表 删除表我们直接使用无返回值的execute()即可。 使用方法 ``` await database.execute("DROP TABLE person"); ``` 插入数据 插入数据有两种方法,一种通过rawInsert(),还有一种通过insert(),他们实现的原理是一样的,只是insert()使用更方便,下面我们来看看两种方式: 第一种:rawInsertData() 使用原始sql往person表中增加了一条数据 ``` int result = await database.rawInsert("INSERT INTO person(name, age) values('Jimi', 18)"); ``` 第二种:insert() 更优雅的插入方式 ``` int result = await database.insert("person", { "name": "是Jimi啊", "age": 28 }); ``` 删除数据 删除数据有两种方法,一种通过rawDelete(),还有一种通过delete(),下面我们来看下这两种方式: 第一种:rawDelete() 使用原始sql删除person表中的数据。 ``` int result = await database.rawUpdate('UPDATE person SET name = "是Jimi哦" where id = 3'); ``` 第二种:delete() 更优雅的删除方式 ``` int result = await database.delete("person", where: "id = 2"); ``` 修改数据 修改数据有两种方法,一种通过rawUpdate(),还有一种通过update(),下面我们来看下这两种方式: 第一种:rawUpdate() 使用原始sql更新person表中的数据。 ``` int result = await database.rawUpdate('UPDATE person SET name = "是Jimi哦" where id = 3'); ``` 第二种:update() 更优雅的更新方式 ``` int result = await database.update("person", { "name": "我是Jimi啊" }, where: "id = 4"); ``` 查询数据 修改数据有两种方法,一种通过rawUpdate(),还有一种通过update(),下面我们来看下这两种方式: 构造函数 ``` Future>> query(String table, {bool? distinct, List? columns, String? where, List? whereArgs, String? groupBy, String? having, String? orderBy, int? limit, int? offset}); ``` 详细描述 字段 属性 描述 table String 需要查询的表名 distinct bool? 查询的数据是否重复 columns List? 需要展示的列 where String? 查询数据的条件 whereArgs List? 查询条件参数,防止sql注入攻击 groupBy String? 查询到的数据进行分组 having String? 给分组设置条件进行过滤 orderBy String? 数据的排序规则(升序or降序)排列 limit int? 限定返回的数据数量 offset int? 跳过几条数据 第一种:rawQuery() 使用原始sql查询person表中的数据。 ``` var list = await database.rawQuery("SELECT * FROM person"); ``` 第二种:query() 1 ``` var list = await database.query("person",); ``` Batch批量处理 批量处理的意思就是多条sql语句进行批量操作,我们可以通过batch来批量操作,等操作完成后使用commit进行提交,代码如下: ``` var batch = database.batch(); batch.insert("person", { "name": "是Jimi啊", "age": 28 }); batch.update("person", { "name": "我是Jimi啊" }, where: "id = 2"); var results = await batch.commit(); ``` Transaction 事务 事务就是一个对数据库执行的工作的单元,以逻辑顺序完成序列。 事务的属性 原子性: 确保所有操作都成功完成,否则所有操作回滚以前的状态。 一致性: 确保数据库在成功提交前能正确的改变状态 隔离性: 事务操作互相独立和透明 持久性: 确保已提交的事务结果在发生故障的情况下仍然存在。 使用方法 ``` await database.transaction((txn) async { var batch = txn.batch(); batch.insert("person", { "name": "是Jimi啊", "age": 18 }); batch.insert("person", { "name": "Jimi", "age": 28 }); batch.update("person", { "name": "我是Jimi啊" }, where: "id = 2"); var results = await batch.commit(); }); ``` 总结 sqflite是Flutter的SQLite插件,它能在App端能够高效的存储和处理数据库数据,适用于需要查询大量持久化数据的应用。如果大家对SQLite或者MySql的语法比较熟悉的话,那么很快就能上手sqflite。 # Flutter 文件读写---path_provider path_provider介绍 path_provider是一个Flutter插件,主要作用是提供一种以平台无关一致的方式访问设备的文件系统,比如应用临时目录、文档目录等。而且path_provider支持Android、iOS、Linux、MacOS、Windows。 path_provider App目录 app存储目录总共分为八种,我们来看一下他们的区别: 临时目录 临时目录的是系统可以随时清空的缓存文件夹 iOS对应的实现方式是 NSCachesDirectory Android对应的实现方式是getCacheDir() 文档目录 文档目录用于存储只能由该应用访问的文件,系统不会清除该目录,只有在删除应用时才会消失。 iOS对应的实现方式是 NSDocumentDirectory Android对应的实现方式是 AppData 应用程序支持目录 应用程序支持目录用于不想向用户公开的文件,也就是你不想给用户看到的文件可放置在该目录中,系统不会清除该目录,只有在删除应用时才会消失。 iOS对应的实现方式是 NSApplicationSupportDirectory Android对应的实现方式是 getFilesDir() 应用程序持久文件目录 该目录主要存储持久文件的目录,并且不会对用户公开,常用于存储数据库文件,比如sqlite.db等。 外部存储目录 主要用于获取外部存储目录,如SD卡等,但iOS不支持外部存储目录,目前只有Android才支持。 外部存储缓存目录 主要用户获取应用程序特定外部缓存数据的目录,比如从SD卡或者手机上有多个存储目录的,但iOS不支持外部存储目录,目前只有Android才支持。 外部存储目录(单独分区) 可根据类型获取外部存储目录,如SD卡、单独分区等,和外部存储目录不同在于他是获取一个目录数组。但iOS不支持外部存储目录,目前只有Android才支持。 桌面程序下载目录 主要用于存储下载文件的目录,只适用于Linux、MacOS、Windows,Android和iOS平台无法使用。 path_provider方法和说明 方法 属性 描述 getTemporaryDirectory() Future 临时目录 getApplicationSupportDirectory() Future 应用程序支持目录 getLibraryDirectory() Future 应用程序持久文件目录 getApplicationDocumentsDirectory() Future 文档目录 getExternalStorageDirectory() Future 外部存储目录 getExternalCacheDirectories() Future 外部存储缓存目录 getExternalStorageDirectories() Future 外部存储目录(单独分区) getDownloadsDirectory() Future 桌面程序下载目录 path_provider基本使用 我们这里举一个简单的例子,通过path_provider获取磁盘中的路径,把文字写入到文件中,具体步骤如下: 添加依赖 获取本地目录 写入数据到磁盘中 读取磁盘数据 第一步:添加依赖 ``` environment: sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter path_provider: ^2.0.5 ``` 第二步:获取本地目录 虽然获取路径总共有八种,但是在实际应用开发过程中,我们经常使用的有三种,我们分别来获取这三种目录的路径,如下: ``` /// 获取文档目录文件 Future _getLocalDocumentFile() async { final dir = await getApplicationDocumentsDirectory(); return File('${dir.path}/str.txt'); } /// 获取临时目录文件 Future _getLocalTemporaryFile() async { final dir = await getTemporaryDirectory(); return File('${dir.path}/str.txt'); } /// 获取应用程序目录文件 Future _getLocalSupportFile() async { final dir = await getApplicationSupportDirectory(); return File('${dir.path}/str.txt'); } ``` 第三步:写入数据到磁盘中 我们这里通过writeAsString()来将name值写入到磁盘中,如果你需要同步写入可调用writeAsStringSync(),或者想通过字节流的方式写入可以调用writeAsBytes()。 ``` String name = "Jimi"; /// 写入数据 Future writeString(String str) async { final file = await _getLocalDocumentFile(); await file.writeAsString(name); final file1 = await _getLocalTemporaryFile(); await file1.writeAsString(name); final file2 = await _getLocalSupportFile(); await file2.writeAsString(name); print("写入成功"); } ``` 第四步:读取磁盘数据 这里加了一个try catch,防止在读取文件出现异常导致崩溃,我们分别读取三个目录里面的文件并对其增加相应的打印。 ``` /// 读取值 Future readString() async { try { final file = await _getLocalDocumentFile(); final result = await file.readAsString(); print("result-----$result"); final file1 = await _getLocalTemporaryFile(); final result1 = await file1.readAsString(); print("result1-----$result1"); final file2 = await _getLocalSupportFile(); final result2 = await file2.readAsString(); print("result2-----$result2"); } catch (e) { } } ``` 完整示例代码 ``` import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { String name = "Jimi"; /// 获取文档目录文件 Future _getLocalDocumentFile() async { final dir = await getApplicationDocumentsDirectory(); return File('${dir.path}/str.txt'); } /// 获取临时目录文件 Future _getLocalTemporaryFile() async { final dir = await getTemporaryDirectory(); return File('${dir.path}/str.txt'); } /// 获取应用程序目录文件 Future _getLocalSupportFile() async { final dir = await getApplicationSupportDirectory(); return File('${dir.path}/str.txt'); } /// 读取值 Future readString() async { try { final file = await _getLocalDocumentFile(); final result = await file.readAsString(); print("result-----$result"); final file1 = await _getLocalTemporaryFile(); final result1 = await file1.readAsString(); print("result1-----$result1"); final file2 = await _getLocalSupportFile(); final result2 = await file2.readAsString(); print("result2-----$result2"); } catch (e) { } } /// 写入数据 Future writeString(String str) async { final file = await _getLocalDocumentFile(); await file.writeAsString(name); final file1 = await _getLocalTemporaryFile(); await file1.writeAsString(name); final file2 = await _getLocalSupportFile(); await file2.writeAsString(name); print("写入成功"); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar(title: Text("path_provider"),), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(name, style: TextStyle( color: Colors.pink, fontSize: 30 ), ), SizedBox(height: 20), ElevatedButton( onPressed: (){ writeString(name); }, child: Text("存入本地目录"), ), ElevatedButton( onPressed: (){ readString(); }, child: Text("读取值"), ), ], ), ), ) ); } } ``` 控制台输出 flutter: 写入成功 flutter: result-----Jimi flutter: result1-----Jimi flutter: result2-----Jimi 总结 当我们需要持久化数据或下载文件、图片或保存数据库文件我们将文件写入到磁盘中,那我们需要借助dart:io以及path_provider,而path_provider主要作用是提供一种以平台无关一致的方式访问设备的文件系统,比如应用临时目录、文档目录等。 # flutter null-safe [flutter null-safe](https://www.liujunmin.com/flutter/null_safety.html) # flutter eventbus eventBus 基本使用步骤 1、 在 pubspec.yaml 包文件中添加包文件名,并通过 flutter package get 下载包依赖 ``` event_bus: ^1.1.1 ``` 2、 在需要使用 eventBus 的组件中引入包依赖文件 ``` import 'package:event_bus/event_bus.dart'; ``` 3、 通常封装一个eventBus 事件总线的文件 event_bus.dart // 引入 eventBus 包文件 ``` import 'package:event_bus/event_bus.dart'; ``` ``` // 创建EventBus EventBus eventBus = new EventBus(); // event 监听 class EventFn{ // 想要接收的数据时什么类型的,就定义相同类型的变量 dynamic obj; EventFn(this.obj); } ``` 4、 在需要发送事件的文件中利用 eventBus.fir 发送事件 ``` //引入封装的e vent_bus.dart 文件 import 'package:new_flutter/utils/event_bus.dart'; // 调用 eventBus.fir 发送事件信息 eventBus.fire(EventFn({ 'a':'b', 'c':'e' })); ``` 5、 在需要监听的文件中,利用 eventBus.on< >().listen() 监听信息 ``` // 注册监听器,订阅 eventbus var eventBusFn = eventBus.on().listen((event) { // event为 event.obj 即为 eventBus.dart 文件中定义的 EventFn 类中监听的数据 print(event.obj); }); ``` 6、 在组件销毁时,一定要销毁监听,防止内存泄漏 ``` @override void dispose() { super.dispose(); //取消订阅 eventBusFn.cancel(); } ``` # flutter-常用组件的教程[常用组件的教程](https://www.liujunmin.com/categories/深入浅出组件篇/) # flutter-常用手势[常用手势](https://www.liujunmin.com/categories/手势系列教程/ # weui ``` dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 event_bus: any weui: any ``` 导入 ``` import 'package:weui/form/index.dart'; ``` ``` import 'package:flutter/material.dart'; import 'package:weui/button/index.dart'; import 'package:weui/cell/index.dart'; import 'package:weui/form/index.dart'; import 'package:weui/input/index.dart'; import 'package:weui/switch/index.dart'; import 'event_bus.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; @override void initState() { // TODO: implement initState super.initState(); } void _incrementCounter() { setState(() { _counter++; }); eventBus.fire(EventFun({ "a":"b", "b":"c" })); } @override Widget build(BuildContext context) { var eventbusfun = eventBus.on().listen((event) { print(event.obj); }); return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Padding( padding: EdgeInsets.all(8), child: WeForm(children: [ WeInput(label: "用户名",hintText: '请输入用户名',autofocus: true,clearable: true,type: TextInputType.text,textInputAction: TextInputAction.next,), WeInput(label: "验证码",hintText: '请输入验证码',type: TextInputType.text,footer: WeButton(Text('获取验证码'),theme: WeButtonType.primary,onClick: (){ },),), Container( width: double.infinity, height: 20, ), WeButton(Text('登陆'),theme: WeButtonType.primary,onClick: (){},), WeButton(Text('注册'),theme: WeButtonType.primary,onClick: (){},), WeCell(label: "请选择地址",content: '',footer: WeSwitch(size: 20,checked: true,),), ]) ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } @override void dispose() { // TODO: implement dispose eventBus.destroy(); super.dispose(); } } ```