用户模型设计
用户模型我设计得比较简单,就是id、用户名、邮箱、昵称这些基本信息,再加上一个JWT token。但后端返回的数据结构是嵌套的,前端需要自己拆开重组一下,把token塞进user对象里,这样存到本地的时候就是一个完整的用户对象。
模型里我还加了几个辅助方法,比如判断用户是否已认证、获取显示名称(优先用昵称、没有就用用户名)。这些小细节虽然不复杂,但后面用起来很方便。
认证状态管理
认证状态的核心是一个AuthNotifier,继承自Riverpod的StateNotifier。它管理三个核心状态:用户信息、加载状态、错误信息。
登录流程是这样的:用户提交用户名密码 -> Dio发起请求 -> 收到响应后解析data里的accessToken和user -> 把token合并到user里 -> 保存用户信息到SharedPreferences -> 在Dio实例里设置全局token -> 更新AuthState。
注册流程跟登录差不多,但多了前端验证这一步。用户名不能空、至少3个字符;邮箱不能空、必须包含@和点;密码不能空、至少6个字符;两次输入要一致。这些验证在发给后端之前就先拦住了,用户体验好很多,也减轻了后端的压力。
自动登录是我比较满意的一个功能。应用启动的时候,AuthNotifier的构造函数会调用_loadUserFromStorage,从SharedPreferences里读用户信息。如果读到了,就自动完成登录状态初始化,不用用户重新输账号密码。如果没读到,就停留在未登录状态,等用户去登录页操作。
登出逻辑就简单了,清除本地存储的user、清除Dio里的token、把AuthState重置为空。
注册页面的表单验证
注册页面我用了flutter_hooks来管理表单状态,用TextEditingController配合useState。验证逻辑全部写在提交按钮的onPressed里:
用户名为空或太短 -> 显示错误提示;邮箱格式不对 -> 显示错误提示;密码太短 -> 显示错误提示;两次密码不一致 -> 显示错误提示。只有所有验证都通过,才会调用注册API。
之前有个问题,什么都不填直接点注册会报Dio错误,就是因为前端没有做验证,直接把空字符串发给后端了。加上验证之后,这个问题就解决了。用户也能立刻知道自己哪里填错了,不用等后端返回错误。
路由守卫的实现
为了保护需要认证的页面,我实现了一个路由守卫。核心逻辑是在go_router的redirect回调里检查用户认证状态。
思路是这样的:如果用户未认证,并且要去的页面是/chat或者/tasks这种需要登录的,就重定向到/login。如果用户已认证,并且要去的是/login或者/register,就重定向到/chat。其他情况就正常放行。
实现的时候用了一个白名单数组,把所有不需要认证的路由都列在里面。判断当前路由是否在白名单里,在不在就决定了要不要拦截。这样以后加新页面的时候也很方便,只需要在白名单里添加就行了。
不过这里踩了个坑:useEffect的依赖项写错了,导致页面无限刷新。排查了好久才发现是空依赖数组的问题。Riverpod的状态变化触发了useEffect重新执行,又去刷新页面,就这样死循环了。后来把依赖项改成了空数组,问题解决了。
Token管理策略
所有需要认证的API请求都要在Header里带accessToken。我在登录成功后会调用一个_setAuthToken方法,把token设置到Dio实例的全局配置里。这样后续所有请求都会自动带上token,不用在每个请求里单独处理。
登出的时候再调用_clearAuthToken把它清掉就行了。
这种单例模式的好处是:登录后一次设置全局生效,token刷新后统一更新,避免重复创建Dio实例,代码也更简洁。
踩坑记录
Riverpod 3.x版本对StateNotifier的支持有变化,我一开始用了hooks_riverpod: ^3.3.1,结果extends StateNotifier报错,说什么类只能继承类。查了半天发现3.x推荐用Notifier替代StateNotifier,但改起来太麻烦,最后回退到2.5.1稳定版,一切正常了。
后端返回的archived字段类型不固定,有时候是bool有时候是int,需要做类型转换。我在fromJson里加了一个辅助方法,判断类型再转。
路由守卫的无限刷新问题前面说过了,还有就是GoRouterState.of(context).uri.toString()获取的是当前页面路径,不是目标路径,调试的时候要注意。
小结
用户系统虽然简单,但涉及的东西不少:状态管理、网络请求、本地存储、路由守卫、表单验证。把这一套搭好之后,后面加新功能就顺畅多了。