相关链接
所需依赖项
shared_preferences: ^2.5.4
dio: ^5.9.0
connectivity_plus: ^7.0.0完整代码
系统配置
lib/utils/config.dart
import 'package:shared_preferences/shared_preferences.dart';
enum APPENVMODE { dev, prod, local }
class AppConfig {
static const String appName = "Flutter Demo";
static const String appVersion = "1.0.0";
static const String _localServer = "";
static const String _devServer = "yycc.hc8.ren";
static const String _prodServer = "";
static const APPENVMODE _appEnvMode = APPENVMODE.dev;
static const String _urlSuffix = "/home";
static const String _wsUrlSuffix = "/socket?authorization=";
// baseUrl Api接口地址
String baseUrl() {
switch (_appEnvMode) {
case APPENVMODE.dev:
return "https://${_devServer}${_urlSuffix}";
case APPENVMODE.prod:
return "https://${_prodServer}${_urlSuffix}";
default:
return "http://${_localServer}${_urlSuffix}";
}
}
// staticUrl 静态资源地址
String staticUrl(){
switch (_appEnvMode) {
case APPENVMODE.dev:
return "https://${_devServer}";
case APPENVMODE.prod:
return "https://${_prodServer}";
default:
return "http://${_localServer}";
}
}
// wsUrl WebSocket接口地址
String wsUrl() {
switch (_appEnvMode) {
case APPENVMODE.dev:
return "wss://${_devServer}${_wsUrlSuffix}${AppTokenOperate.getToken()}";
case APPENVMODE.prod:
return "wss://${_prodServer}${_wsUrlSuffix}${AppTokenOperate.getToken()}";
default:
return "ws://${_localServer}${_wsUrlSuffix}${AppTokenOperate.getToken()}";
}
}
}
enum AppTokenType {
accessToken,
userInfo,
appConfig,
userPermission
}
class AppTokenOperate {
static const Map<AppTokenType,String> appTokenName = {
AppTokenType.accessToken: "yycc_erp_app_access_token",
AppTokenType.userInfo: "yycc_erp_app_user_info",
AppTokenType.appConfig: "yycc_erp_app_config",
AppTokenType.userPermission: "yycc_erp_app_user_permission"
};
// getToken 获取token
static Future<dynamic> getToken({AppTokenType tokenType=AppTokenType.accessToken}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString(appTokenName[tokenType]!);
}
// setToken 设置token
static Future<bool> setToken({required String token, AppTokenType tokenType=AppTokenType.accessToken}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setString(appTokenName[tokenType]!, token);
}
// removeToken 删除token
static Future<bool> removeToken({AppTokenType tokenType=AppTokenType.accessToken}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.remove(appTokenName[tokenType]!);
}
// isLogin 判断是否登录
static Future<bool> isLogin() async {
String? token = await getToken();
if (token == null) {
removeToken();
removeToken(tokenType: AppTokenType.userInfo);
return false;
}else{
return true;
}
}
// logout 退出登录
static Future<void> logout({bool msg=false}) async {
String? token = await getToken();
if(token!=null){
// TODO:退出登录接口调用
}
await removeToken();
await removeToken(tokenType: AppTokenType.userInfo);
if(msg){
// TODO:退出登录提示
return;
}
// TODO:退出登录跳转
}
}工具类
lib/utils/utils.dart
class AppUtils {
// getDateDiff 时间戳转多少分钟之前(带异常处理)
String getDateDiff(String dateTimeStamp) {
// 解析时间
DateTime? parsedTime = DateTime.tryParse(dateTimeStamp);
if (parsedTime == null) {
throw FormatException("日期格式错误: $dateTimeStamp");
}
int timestamp = parsedTime.millisecondsSinceEpoch;
int now = DateTime.now().millisecondsSinceEpoch;
int diffValue = now - timestamp;
if (diffValue < 0) {
throw FormatException("时间不能是未来时间: $dateTimeStamp");
}
const minute = 1000 * 60;
const hour = minute * 60;
const day = hour * 24;
const month = day * 30;
const year = day * 365;
double yearC = diffValue / year;
double monthC = diffValue / month;
double weekC = diffValue / (7 * day);
double dayC = diffValue / day;
double hourC = diffValue / hour;
double minC = diffValue / minute;
if (yearC >= 1) {
return "${yearC.toInt()}年前";
} else if (monthC >= 1) {
return "${monthC.toInt()}个月前";
} else if (weekC >= 1) {
return "${weekC.toInt()}周前";
} else if (dayC >= 1) {
return "${dayC.toInt()}天前";
} else if (hourC >= 1) {
return "${hourC.toInt()}小时前";
} else if (minC >= 1) {
return "${minC.toInt()}分钟前";
} else {
return "刚刚";
}
}
}
请求封装
lib/request/request.dart
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_test_2/utils/config.dart';
/// 网络请求方法枚举
enum HttpMethod { get, post, put, delete }
/// 常量配置
class HttpConstants {
static const int timeout = 10; // 秒
static const int cacheExpireMinutes = 30; // 分钟
}
/// 统一接口响应对象
class ApiResponse<T> {
final int code;
final String message;
final T? data;
ApiResponse({required this.code, required this.message, this.data});
factory ApiResponse.fromMap(Map<String, dynamic> map) {
return ApiResponse(
code: map['code'] ?? -1,
message: map['message'] ?? '',
data: map['data'],
);
}
}
/// 网络请求管理类(单例)
class HttpManager {
static HttpManager? _instance;
late Dio _dio;
final Connectivity _connectivity = Connectivity();
HttpManager._internal() {
_initDio();
_initInterceptors();
}
static HttpManager get instance {
_instance ??= HttpManager._internal();
return _instance!;
}
/// 初始化 Dio
void _initDio() {
_dio = Dio(
BaseOptions(
baseUrl: AppConfig().baseUrl(),
connectTimeout: Duration(seconds: HttpConstants.timeout),
receiveTimeout: Duration(seconds: HttpConstants.timeout),
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Accept': 'application/json',
},
responseType: ResponseType.json,
),
);
}
/// 初始化拦截器
void _initInterceptors() {
// 仅 debug 打印日志
if (kDebugMode) {
_dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (log) => print('Dio日志:$log'),
),
);
}
_dio.interceptors.add(
InterceptorsWrapper(
onRequest:
(RequestOptions options, RequestInterceptorHandler handler) async {
final sp = await SharedPreferences.getInstance();
final token = sp.getString('token');
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = token;
}
handler.next(options);
},
onResponse: (Response response, ResponseInterceptorHandler handler) {
handler.next(response);
},
onError: (DioException error, ErrorInterceptorHandler handler) {
final errorMsg = _handleError(error);
handler.reject(
DioException(
requestOptions: error.requestOptions,
error: errorMsg,
type: error.type,
response: error.response,
),
);
},
),
);
}
/// 错误统一处理
String _handleError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
return '网络连接超时,请检查网络';
case DioExceptionType.sendTimeout:
return '请求发送超时,请稍后重试';
case DioExceptionType.receiveTimeout:
return '响应接收超时,请稍后重试';
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode ?? 0;
String? msg;
if (error.response?.data is Map<String, dynamic>) {
msg = error.response?.data['message'];
}
if (statusCode == 401) {
_clearToken();
return '登录状态过期,请重新登录';
} else if (statusCode == 403) {
return '权限不足,无法访问';
} else if (statusCode == 404) {
return '请求接口不存在';
} else if (statusCode == 500) {
return '服务器内部错误,请稍后重试';
} else {
return '请求失败($statusCode):${msg ?? '未知错误'}';
}
case DioExceptionType.cancel:
return '请求已取消';
case DioExceptionType.connectionError:
return '网络连接错误,请检查网络';
default:
return '未知错误:${error.message ?? '请稍后重试'}';
}
}
/// 清空 token
Future<void> _clearToken() async {
final sp = await SharedPreferences.getInstance();
await sp.remove('token');
}
/// 网络检查
Future<bool> _checkNetwork() async {
final result = await _connectivity.checkConnectivity();
return result != ConnectivityResult.none;
}
/// 通用请求
Future<ApiResponse> request({
required String path,
required HttpMethod method,
Map<String, dynamic>? params,
Map<String, dynamic>? queryParams,
bool needCache = false,
bool forceRefresh = false,
}) async {
final hasNetwork = await _checkNetwork();
if (!hasNetwork) {
if (needCache) {
final cacheData = await _getCache(path, method, params);
if (cacheData != null) return ApiResponse.fromMap(cacheData);
}
throw DioException(
requestOptions: RequestOptions(path: path),
error: '当前无网络连接,无法完成请求',
type: DioExceptionType.connectionError,
);
}
if (needCache && !forceRefresh) {
final cacheData = await _getCache(path, method, params);
if (cacheData != null) return ApiResponse.fromMap(cacheData);
}
Response response;
try {
switch (method) {
case HttpMethod.get:
response = await _dio.get(path, queryParameters: queryParams ?? params);
break;
case HttpMethod.post:
response = await _dio.post(path, data: params, queryParameters: queryParams);
break;
case HttpMethod.put:
response = await _dio.put(path, data: params, queryParameters: queryParams);
break;
case HttpMethod.delete:
response = await _dio.delete(path, data: params, queryParameters: queryParams);
break;
}
} catch (e) {
rethrow;
}
// 仅缓存 GET 请求
if (needCache && method == HttpMethod.get && response.statusCode == 200) {
await _setCache(path, method, params, response.data);
}
// 返回安全的 ApiResponse
if (response.data is! Map<String, dynamic>) {
throw DioException(
requestOptions: response.requestOptions,
error: '返回数据格式异常',
type: DioExceptionType.badResponse,
);
}
final apiResponse = ApiResponse.fromMap(response.data);
if (apiResponse.code != 0) {
throw DioException(
requestOptions: response.requestOptions,
error: apiResponse.message,
type: DioExceptionType.badResponse,
response: response,
);
}
return apiResponse;
}
/// 生成缓存 key
String _generateCacheKey(String path, HttpMethod method, Map<String, dynamic>? params) {
final paramsStr = params != null ? json.encode(params) : '';
return '$method-$path-$paramsStr';
}
/// 设置缓存
Future<void> _setCache(String path, HttpMethod method, Map<String, dynamic>? params, dynamic data) async {
if (method != HttpMethod.get) return; // 仅缓存 GET
final sp = await SharedPreferences.getInstance();
final cacheKey = _generateCacheKey(path, method, params);
final cacheData = json.encode({
'data': data,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
await sp.setString(cacheKey, cacheData);
}
/// 获取缓存
Future<Map<String, dynamic>?> _getCache(String path, HttpMethod method, Map<String, dynamic>? params) async {
final sp = await SharedPreferences.getInstance();
final cacheKey = _generateCacheKey(path, method, params);
final cacheStr = sp.getString(cacheKey);
if (cacheStr == null) return null;
final cacheData = json.decode(cacheStr);
final timestamp = cacheData['timestamp'] as int;
final now = DateTime.now().millisecondsSinceEpoch;
final expireTime = HttpConstants.cacheExpireMinutes * 60 * 1000;
if (now - timestamp > expireTime) {
await sp.remove(cacheKey);
return null;
}
return cacheData['data'] as Map<String, dynamic>;
}
/// 带重试请求(只对网络异常重试)
Future<ApiResponse> requestWithRetry({
required String path,
required HttpMethod method,
Map<String, dynamic>? params,
Map<String, dynamic>? queryParams,
bool needCache = false,
bool forceRefresh = false,
int retryCount = 2,
}) async {
int currentRetry = 0;
while (true) {
try {
return await request(
path: path,
method: method,
params: params,
queryParams: queryParams,
needCache: needCache,
forceRefresh: forceRefresh,
);
} catch (e) {
if (e is DioException &&
(e.type == DioExceptionType.connectionError ||
e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout)) {
currentRetry++;
if (currentRetry > retryCount) rethrow;
await Future.delayed(Duration(milliseconds: 500 * currentRetry));
} else {
rethrow; // 业务错误直接抛
}
}
}
}
}