Files
App.K12.Student.Miniapp_Flu…/lib/core/providers/user_provider.dart
T
2026-09-04 15:22:20 +08:00

106 lines
5.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
);