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 { /// 冷启动初始值:从 shared_preferences 读缓存(登录时已写入) @override Future 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 updateAvatar(String avatar) async { // 先拿到当前全局用户对象。 // state 是 AsyncValue,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 authTick = ValueNotifier(0); /// 全局用户信息 Provider final currentUserProvider = AsyncNotifierProvider( CurrentUserNotifier.new, );