106 lines
5.5 KiB
Dart
106 lines
5.5 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:new_learn/core/models/user_model.dart';
|
||
import 'package:new_learn/core/storage/user_storage.dart';
|
||
|
||
/// ============================================================
|
||
/// 【currentUserProvider】全局当前用户状态(Riverpod,对标 Vue 的 Pinia store)
|
||
///
|
||
/// //冷启动 → build() 从 SharedPreferences 读缓存 → 页面拿到用户信息
|
||
/// 登录成功 → set(user) → 所有 watch 的页面自动刷新 + 路由守卫知道已登录
|
||
/// 退出登录 → set(null) → 所有 watch 的页面刷新 + 路由守卫踢回 /login
|
||
/// 换头像 → updateAvatar(url) → 内存 + 本地缓存都更新,页面自动刷新
|
||
///
|
||
/// 一、为什么用 AsyncNotifier 而不是 StateProvider:
|
||
/// AsyncNotifier 的 build() 可以【异步加载初始值】——冷启动时自动从
|
||
/// shared_preferences 读缓存回填,页面 ref.watch 后无需自己判断加载态;
|
||
/// StateProvider 的初始值是同步写死的,不适合"启动时从磁盘读"。
|
||
///
|
||
/// 二、怎么用(三句话):
|
||
/// 1. 读取:`final user = ref.watch(currentUserProvider).value;`
|
||
/// 页面必须用 ConsumerWidget(build 有 ref)或 Consumer 包裹;
|
||
/// 2. 更新:`ref.read(currentUserProvider.notifier).set(user);`
|
||
/// 登录成功后调用,所有 ref.watch 的页面自动刷新;
|
||
/// 3. 全局:ProviderScope 包在 main.dart 根,任何页面都能拿到。
|
||
///
|
||
/// 三、State 生命周期(AsyncValue):
|
||
/// - AsyncData(user) → 有值:已登录/有缓存
|
||
/// - AsyncData(null) → 明确未登录
|
||
/// - AsyncLoading() → 正在读缓存(一闪而过)
|
||
/// - AsyncError() → 读缓存异常
|
||
///
|
||
/// 四、高频坑:
|
||
/// 1. 不要直接改 state 引用,要用 set() 统一入口,方便以后加日志;
|
||
/// 2. 退出登录时调 set(null) → authTick 自增 → GoRouter.refresh(),
|
||
/// 登录守卫 redirect 会自动把当前页踢回 /login,无需手动跳转;
|
||
/// 3. AsyncNotifier 的 build 只在首次访问时执行,全局只有一份状态;
|
||
/// 4. 冷启动走 build()(读缓存),不走 set(),所以 authTick 不触发;
|
||
/// 运行中登录/登出才走 set(),此时守卫才会重新计算跳转。
|
||
/// ============================================================
|
||
class CurrentUserNotifier extends AsyncNotifier<CurrentUserExtension?> {
|
||
/// 冷启动初始值:从 shared_preferences 读缓存(登录时已写入)
|
||
@override
|
||
Future<CurrentUserExtension?> build() async {
|
||
return UserStorage.instance.readUserInfo();
|
||
}
|
||
|
||
/// 登录成功 / 手动刷新后,写入全局状态(所有 ref.watch 页面自动刷新)
|
||
void set(CurrentUserExtension? user) {
|
||
state = AsyncData(user);
|
||
// 通知路由守卫:登录态变化了,触发 GoRouter.refresh() 重算跳转
|
||
authTick.value++;
|
||
}
|
||
|
||
/// 只更新头像:登录态没变,不需要触发路由守卫重算。
|
||
///
|
||
/// 使用场景:
|
||
/// 头像上传成功后,后端接口 /api/v1/User/UpdateUserAvatar 已经把服务器里的
|
||
/// avatar 字段改掉了。此时前端不需要再去"我的"页重新请求一次用户信息,
|
||
/// 只要把 currentUserProvider 里的 avatar 同步改掉即可。
|
||
///
|
||
/// 这段代码做了两件事:
|
||
/// 1. 更新 Riverpod 内存状态:让 ref.watch(currentUserProvider) 的页面立刻刷新;
|
||
/// 2. 更新本地缓存:保证 App 重启后还是新头像,不会回退成旧值。
|
||
///
|
||
/// 注意:
|
||
/// 这里只是"用户资料局部字段更新",不是登录/登出,所以不需要 authTick++。
|
||
Future<void> updateAvatar(String avatar) async {
|
||
// 先拿到当前全局用户对象。
|
||
// state 是 AsyncValue<CurrentUserExtension?>,value 就是里面当前的用户数据。
|
||
final current = state.value;
|
||
|
||
// 防御:如果当前没有用户信息(比如未登录),就没有可更新的对象,直接返回。
|
||
if (current == null) return;
|
||
|
||
// 基于旧对象复制一个新对象,只替换 avatar 字段,其他字段保持不变。
|
||
// 这是状态管理里的常见写法:不要直接改旧对象,而是生成一个"新状态"。
|
||
final updated = current.copyWith(avatar: avatar);
|
||
|
||
// 把 Riverpod 状态更新成新对象。
|
||
// 一旦这里赋值成功,所有 ref.watch(currentUserProvider) 的页面都会自动 rebuild,
|
||
// 所以"我的"页头像会立刻刷新,不用再手动 setState 整个页面。
|
||
state = AsyncData(updated);
|
||
|
||
// 同步写回本地缓存:
|
||
// 这样不仅当前界面是新头像,App 下次重启时从 SharedPreferences 读出来的
|
||
// 也还是最新头像。
|
||
await UserStorage.instance.saveUserInfo(updated);
|
||
}
|
||
}
|
||
|
||
/// 登录态变化信号(给 go_router 的 refreshListenable 用)
|
||
///
|
||
/// 为什么需要它:go_router 的 redirect 只在【导航发生时】执行。
|
||
/// 登出/登录不导航,守卫就不会重新判断——需要一个外部信号去主动
|
||
/// 触发一次重算。用 ValueNotifier 当信号源:
|
||
/// set(null) 登出 → authTick.value++ → GoRouter.refresh()
|
||
/// → redirect 重跑 → 未登录访问 /my → 自动重定向 /login。
|
||
/// 这就是「响应式登录守卫」:业务代码永远不用手动跳登录页。
|
||
final ValueNotifier<int> authTick = ValueNotifier<int>(0);
|
||
|
||
/// 全局用户信息 Provider
|
||
final currentUserProvider =
|
||
AsyncNotifierProvider<CurrentUserNotifier, CurrentUserExtension?>(
|
||
CurrentUserNotifier.new,
|
||
);
|