概述
该方案实现了 Flutter 应用的登录状态管理和 Token 持久化,包含:- 登录信息本地持久化(SharedPreferences)
- 应用启动时自动恢复登录状态
- 网络请求自动添加 Token
- 登录状态全局管理(Provider)
- 登录状态变化事件通知
依赖配置
在 pubspec.yaml 中添加以下依赖:
dependencies:
flutter:
sdk: flutter
# 状态管理
provider: ^6.0.0
# 本地存储
flustars: ^1.0.0
# 网络请求
dio: ^5.0.0
# 事件总线(可选,用于登录状态变化通知)
event_bus: ^2.0.0
# JSON 序列化(可选,如果使用 json_serializable)
json_annotation: ^4.8.0
dev_dependencies:
# JSON 序列化代码生成(可选)
json_serializable: ^6.7.0
build_runner: ^2.4.0
完整代码实现
1. 常量定义文件 lib/common/constant.dart
/// 应用常量定义
class Constant {
/// 登录信息缓存键名
/// 用于在 SharedPreferences 中存储登录信息的键
static const String cacheLoginInfo = 'loginInfo';
// 可以根据需要添加其他常量
// static const String cacheUserId = 'userId';
// static const String cacheToken = 'token';
}
2. 登录数据模型 lib/models/login_bean.dart
import 'package:json_annotation/json_annotation.dart';
/// 登录信息数据模型
/// 包含服务器返回的登录相关信息
///
/// 如果使用 json_serializable,需要运行:
/// flutter pub run build_runner build
@JsonSerializable()
class LoginBean {
/// 认证令牌,用于后续 API 请求的身份验证
final String token;
/// 用户ID
final int userId;
/// 用户编号(可根据实际业务调整字段)
final int userNo;
LoginBean({
required this.token,
required this.userId,
required this.userNo,
});
/// 从 JSON 创建对象
factory LoginBean.fromJson(Map<String, dynamic> json) =>
_$LoginBeanFromJson(json);
/// 转换为 JSON
Map<String, dynamic> toJson() => _$LoginBeanToJson(this);
}
// 如果使用 json_serializable,取消下面的注释并运行 build_runner
// part 'login_bean.g.dart';
如果不使用 json_serializable,可以使用手动序列化:
/// 登录信息数据模型(手动序列化版本)
class LoginBean {
final String token;
final int userId;
final int userNo;
LoginBean({
required this.token,
required this.userId,
required this.userNo,
});
/// 从 JSON 创建对象
factory LoginBean.fromJson(Map<String, dynamic> json) {
return LoginBean(
token: json['token'] as String,
userId: json['userId'] as int,
userNo: json['userNo'] as int,
);
}
/// 转换为 JSON
Map<String, dynamic> toJson() {
return {
'token': token,
'userId': userId,
'userNo': userNo,
};
}
}
3. 用户信息工具类 lib/utils/user_utils.dart
import 'dart:convert';
import 'package:flustars/flustars.dart';
import 'package:your_app/common/constant.dart';
import 'package:your_app/models/login_bean.dart';
/// 用户信息管理工具类
///
/// 职责:
/// 1. 保存登录信息到本地存储(SharedPreferences)
/// 2. 从本地存储读取登录信息
/// 3. 清除登录信息
///
/// 使用单例模式,确保全局只有一个实例
class UserUtils {
// 单例模式实现
static UserUtils? _instance;
/// 工厂构造函数,返回单例实例
factory UserUtils() => _getInstance();
/// 静态 getter,方便直接访问实例
static UserUtils get instance => _getInstance();
/// 获取单例实例
static UserUtils _getInstance() {
_instance ??= UserUtils._internal();
return _instance!;
}
/// 私有构造函数,防止外部直接创建实例
UserUtils._internal();
/// 从本地存储获取登录信息
///
/// 返回值:
/// - 如果存在登录信息,返回 LoginBean 对象
/// - 如果不存在或解析失败,返回 null
///
/// 使用场景:
/// - 应用启动时恢复登录状态
/// - 检查用户是否已登录
LoginBean? getLoginInfo() {
try {
// 从 SharedPreferences 读取存储的 JSON 字符串
String? loginInfoString = SpUtil.getString(
Constant.cacheLoginInfo,
defValue: null,
);
// 如果存在数据,解析为 LoginBean 对象
if (loginInfoString != null && loginInfoString.isNotEmpty) {
// 将 JSON 字符串解析为 Map,然后创建 LoginBean 对象
Map<String, dynamic> jsonMap = json.decode(loginInfoString);
LoginBean loginInfo = LoginBean.fromJson(jsonMap);
return loginInfo;
}
} catch (e) {
// 捕获异常,避免因解析失败导致应用崩溃
// 实际项目中可以使用日志框架记录错误
print("获取缓存中的登录信息失败:$e");
}
return null;
}
/// 保存登录信息到本地存储
///
/// 参数:
/// - loginInfo: 要保存的登录信息,如果为 null 则清除已保存的信息
///
/// 使用场景:
/// - 登录成功后保存用户信息
/// - 退出登录时清除用户信息(传入 null)
void save(LoginBean? loginInfo) {
try {
if (loginInfo == null) {
// 如果传入 null,清除已保存的登录信息
SpUtil.remove(Constant.cacheLoginInfo);
} else {
// 将 LoginBean 对象转换为 JSON 字符串并保存
String jsonString = json.encode(loginInfo.toJson());
SpUtil.putString(Constant.cacheLoginInfo, jsonString);
}
} catch (e) {
// 捕获异常,避免因保存失败导致应用崩溃
print("缓存登录信息失败:$e");
}
}
/// 清除登录信息(可选方法,与 save(null) 功能相同)
void clearLoginInfo() {
save(null);
}
}
4. 登录状态管理 Provider lib/providers/login_provider.dart
import 'package:flutter/material.dart';
import 'package:your_app/models/login_bean.dart';
import 'package:your_app/utils/user_utils.dart';
// 如果使用事件总线,取消下面的注释
// import 'package:event_bus/event_bus.dart';
// import 'package:your_app/events/login_event.dart';
/// 登录状态管理 Provider
///
/// 职责:
/// 1. 管理全局登录状态
/// 2. 在应用启动时自动恢复登录状态
/// 3. 提供登录/登出方法
/// 4. 通知其他组件登录状态变化
///
/// 使用 ChangeNotifier 实现状态管理,配合 Provider 使用
class LoginProvider extends ChangeNotifier {
// 单例模式实现
static LoginProvider? _instance;
/// 工厂构造函数,返回单例实例
factory LoginProvider() => _getInstance();
/// 获取单例实例
static LoginProvider _getInstance() {
_instance ??= LoginProvider._internal();
return _instance!;
}
/// 当前登录信息
/// 如果为 null,表示未登录
LoginBean? loginInfo;
/// 私有构造函数
/// 在构造函数中自动从本地存储恢复登录状态
LoginProvider._internal() {
// 应用启动时,从本地存储读取登录信息
loginInfo = UserUtils.instance.getLoginInfo();
// 打印日志,方便调试(实际项目中可以使用日志框架)
if (loginInfo != null) {
print("已恢复登录状态,token: ${loginInfo?.token}");
} else {
print("未找到登录信息,用户未登录");
}
}
/// 判断用户是否已登录
///
/// 返回值:
/// - true: 已登录
/// - false: 未登录
bool get isLogin => loginInfo != null;
/// 更新登录信息
///
/// 参数:
/// - loginInfo: 新的登录信息,如果为 null 则表示退出登录
///
/// 功能:
/// 1. 更新内存中的登录信息
/// 2. 持久化保存到本地存储
/// 3. 发送登录状态变化事件(如果使用事件总线)
/// 4. 通知所有监听者状态已更新
///
/// 使用场景:
/// - 登录成功后调用,传入服务器返回的登录信息
/// - 退出登录时调用,传入 null
void updateLoginInfo(LoginBean? loginInfo) {
// 更新内存中的登录信息
this.loginInfo = loginInfo;
// 持久化保存到本地存储
UserUtils.instance.save(loginInfo);
// 如果使用事件总线,发送登录状态变化事件
// 其他组件可以监听这个事件,响应登录状态变化
// eventBus.fire(LoginEvent(isLogin: isLogin));
// 通知所有监听者状态已更新
// 使用 Provider 的组件会自动重建
notifyListeners();
}
/// 退出登录
///
/// 功能:
/// 1. 清除登录信息
/// 2. 跳转到登录页面(可选)
///
/// 使用场景:
/// - 用户主动退出登录
/// - Token 过期需要重新登录
void logout() {
// 清除登录信息(传入 null)
updateLoginInfo(null);
// 如果需要跳转到登录页面,可以在这里添加导航逻辑
// Navigator.of(context).pushNamedAndRemoveUntil(
// '/login',
// (route) => false,
// );
}
}
5. 事件定义(可选)lib/events/login_event.dart
/// 登录状态变化事件
///
/// 当用户登录或退出登录时,会发送此事件
/// 其他组件可以监听此事件,响应登录状态变化
///
/// 使用场景:
/// - 购物车需要根据登录状态显示不同内容
/// - 个人中心页面需要根据登录状态显示不同信息
/// - 需要实时响应登录状态变化的场景
class LoginEvent {
/// 是否已登录
final bool isLogin;
LoginEvent({required this.isLogin});
}
6. 事件总线(可选)lib/events/event_bus.dart
import 'package:event_bus/event_bus.dart';
/// 全局事件总线实例
///
/// 用于在应用的不同部分之间传递事件
/// 例如:登录状态变化、用户信息更新等
///
/// 使用方式:
/// 1. 发送事件:eventBus.fire(LoginEvent(isLogin: true));
/// 2. 监听事件:eventBus.on<LoginEvent>().listen((event) { ... });
/// 3. 取消监听:subscription.cancel();
final EventBus eventBus = EventBus();
7. Token 拦截器 lib/net/interceptors/token_interceptor.dart
import 'package:dio/dio.dart';
import 'package:your_app/providers/login_provider.dart';
/// Token 拦截器
///
/// 功能:
/// 在每次网络请求时,自动在请求头中添加 Token
///
/// 工作原理:
/// 1. 拦截所有发出的 HTTP 请求
/// 2. 检查用户是否已登录
/// 3. 如果已登录,在请求头中添加 Access_token
///
/// 使用方式:
/// 在创建 Dio 实例时,添加到拦截器列表中:
/// dio.interceptors.add(TokenInterceptor());
class TokenInterceptor extends Interceptor {
/// 请求拦截器
/// 在请求发送前执行
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
// 获取登录状态管理实例
LoginProvider provider = LoginProvider();
// 如果用户已登录,在请求头中添加 Token
if (provider.isLogin && provider.loginInfo != null) {
// 根据后端要求设置 Token 字段名
// 常见的有:Authorization, Access_token, Token, X-Auth-Token 等
options.headers['Access_token'] = provider.loginInfo!.token;
// 如果需要使用 Bearer Token 格式:
// options.headers['Authorization'] = 'Bearer ${provider.loginInfo!.token}';
}
// 继续处理请求
super.onRequest(options, handler);
}
/// 响应拦截器(可选)
/// 在收到响应后执行
/// 可以在这里处理 Token 过期等情况
@override
void onResponse(
Response response,
ResponseInterceptorHandler handler,
) {
// 可以在这里检查响应状态码
// 例如:如果返回 401(未授权),说明 Token 过期,可以自动退出登录
// if (response.statusCode == 401) {
// LoginProvider().logout();
// }
super.onResponse(response, handler);
}
/// 错误拦截器(可选)
/// 在请求出错时执行
@override
void onError(
DioException err,
ErrorInterceptorHandler handler,
) {
// 可以在这里处理 Token 相关的错误
// 例如:401 错误时自动退出登录
// if (err.response?.statusCode == 401) {
// LoginProvider().logout();
// }
super.onError(err, handler);
}
}
8. Dio 配置示例 lib/net/dio_config.dart
import 'package:dio/dio.dart';
import 'package:your_app/net/interceptors/token_interceptor.dart';
/// Dio 网络请求配置
///
/// 创建配置好的 Dio 实例,包含 Token 拦截器
class DioConfig {
static Dio? _dio;
/// 获取配置好的 Dio 实例
static Dio getInstance() {
_dio ??= Dio(
BaseOptions(
// 基础 URL(根据实际情况修改)
baseUrl: 'https://api.example.com',
// 连接超时时间(毫秒)
connectTimeout: const Duration(seconds: 30),
// 接收超时时间(毫秒)
receiveTimeout: const Duration(seconds: 30),
// 请求头
headers: {
'Content-Type': 'application/json',
},
),
);
// 添加 Token 拦截器
// 这样所有通过此 Dio 实例发送的请求都会自动添加 Token
_dio!.interceptors.add(TokenInterceptor());
return _dio!;
}
}
9. 在应用入口配置 Provider lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/providers/login_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
// 注册全局 Provider
providers: [
// 登录状态管理 Provider
// 使用 ChangeNotifierProvider.value 确保使用单例实例
ChangeNotifierProvider.value(
value: LoginProvider(),
),
// 可以在这里添加其他 Provider
],
child: MaterialApp(
title: 'Your App',
// ... 其他配置
),
);
}
}
10. 登录页面使用示例 lib/pages/login/login_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/models/login_bean.dart';
import 'package:your_app/providers/login_provider.dart';
import 'package:your_app/net/dio_config.dart';
import 'package:dio/dio.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
/// 登录按钮点击事件
Future<void> _handleLogin() async {
try {
// 1. 发送登录请求
Dio dio = DioConfig.getInstance();
Response response = await dio.post(
'/api/login',
data: {
'phone': _phoneController.text,
'code': _codeController.text,
},
);
// 2. 解析响应数据
Map<String, dynamic> data = response.data['data'];
LoginBean loginInfo = LoginBean.fromJson(data);
// 3. 保存登录信息
// 这会自动:
// - 更新 LoginProvider 中的登录状态
// - 持久化保存到本地存储
// - 通知所有监听者
Provider.of<LoginProvider>(context, listen: false)
.updateLoginInfo(loginInfo);
// 4. 登录成功,跳转到主页
if (mounted) {
Navigator.of(context).pushReplacementNamed('/home');
}
} catch (e) {
// 处理登录失败
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登录失败:$e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('登录')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _phoneController,
decoration: const InputDecoration(labelText: '手机号'),
),
TextField(
controller: _codeController,
decoration: const InputDecoration(labelText: '验证码'),
),
ElevatedButton(
onPressed: _handleLogin,
child: const Text('登录'),
),
],
),
),
);
}
@override
void dispose() {
_phoneController.dispose();
_codeController.dispose();
super.dispose();
}
}
11. 在其他页面使用登录状态示例
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/providers/login_provider.dart';
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('个人中心')),
body: Consumer<LoginProvider>(
// Consumer 会自动监听 LoginProvider 的变化
builder: (context, loginProvider, child) {
// 根据登录状态显示不同内容
if (loginProvider.isLogin) {
return Column(
children: [
Text('用户ID: ${loginProvider.loginInfo?.userId}'),
Text('用户编号: ${loginProvider.loginInfo?.userNo}'),
ElevatedButton(
onPressed: () {
// 退出登录
loginProvider.logout();
},
child: const Text('退出登录'),
),
],
);
} else {
return const Center(
child: Text('请先登录'),
);
}
},
),
);
}
}
12. 监听登录状态变化事件(可选)
import 'package:event_bus/event_bus.dart';
import 'package:your_app/events/event_bus.dart';
import 'package:your_app/events/login_event.dart';
class SomeWidget extends StatefulWidget {
const SomeWidget({super.key});
@override
State<SomeWidget> createState() => _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
// 事件订阅
late StreamSubscription<LoginEvent> _loginSubscription;
@override
void initState() {
super.initState();
// 监听登录状态变化事件
_loginSubscription = eventBus.on<LoginEvent>().listen((event) {
// 当登录状态变化时,执行相应操作
if (event.isLogin) {
print('用户已登录');
// 执行登录后的操作,例如:刷新数据、更新UI等
} else {
print('用户已退出登录');
// 执行退出登录后的操作,例如:清除数据、跳转登录页等
}
// 如果需要更新 UI,调用 setState
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
// 取消事件订阅,避免内存泄漏
_loginSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
// ... UI 代码
);
}
}
使用流程总结
1. 登录流程
2. 应用启动流程
3. 网络请求流程
关键点说明
- 单例模式:UserUtils 和 LoginProvider 使用单例,确保全局状态一致
- 持久化存储:使用 SharedPreferences(通过 flustars)持久化登录信息
- 自动恢复:应用启动时自动从本地存储恢复登录状态
- 自动添加 Token:通过 Dio 拦截器自动在请求头添加 Token
- 状态管理:使用 Provider 管理登录状态,支持全局访问和自动更新
- 事件通知:使用 EventBus 通知登录状态变化(可选)
注意事项
- 修改包名:将所有 your_app 替换为你的实际包名
- Token 字段名:根据后端要求修改 TokenInterceptor 中的字段名(Access_token)
- 错误处理:根据实际需求完善错误处理逻辑
- 安全性:Token 存储在 SharedPreferences 中,对于敏感应用可以考虑加密存储
- Token 过期:可以在 TokenInterceptor 的 onResponse 中处理 Token 过期情况
扩展功能
- Token 刷新:在拦截器中检测 Token 过期,自动刷新 Token
- 加密存储:对敏感信息进行加密后再存储
- 多账号支持:扩展支持多个账号切换
- 自动登录:记住密码功能(需要额外存储密码,注意安全性)
浙公网安备 33010602011771号