03. Flutter 鸿蒙实战 03:登录注册与本地会话持久化

用 AuthController、AuthScope 和 shared_preferences_ohos 管理登录态。
登录态入口
登录态管理在 lib/features/auth 目录下,核心文件是 auth_controller.dart、auth_repository.dart、session_store.dart 和 auth_scope.dart。项目启动时,TalkingAlbumApp 创建 AuthController 并调用 initialize()。这个过程决定首屏显示登录页还是应用主页面。
dart
_authController = AuthController(
apiClient: _apiClient,
sessionStore: SharedPreferencesSessionStore(),
);
_authController.initialize();
MaterialApp 的 home 是 _AuthGate,它根据状态切页面:
dart
return switch (auth.status) {
AuthStatus.initializing => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
AuthStatus.signedOut => const AuthPage(),
AuthStatus.signedIn => const AppShell(initialRoute: AppRoutes.home),
};
这个结构比在登录页里手动判断更干净。登录态是应用级状态,不应该散落在各个页面里。
SessionStore 抽象
项目没有直接在 AuthController 里写 SharedPreferences,而是抽象了一个 SessionStore:
dart
abstract interface class SessionStore {
Future<StoredSession?> read();
Future<void> write(String token, AuthUser user);
Future<void> clear();
}
当前实现是 SharedPreferencesSessionStore,底层使用 shared_preferences_ohos 适配鸿蒙:
dart
class SharedPreferencesSessionStore implements SessionStore {
static const _tokenKey = 'talking_album_token';
static const _userKey = 'talking_album_user';
@override
Future<StoredSession?> read() async {
final preferences = await SharedPreferences.getInstance();
final token = preferences.getString(_tokenKey);
final rawUser = preferences.getString(_userKey);
if (token == null || rawUser == null) return null;
try {
final json = jsonDecode(rawUser);
if (json is! Map<String, dynamic>) return null;
return StoredSession(token: token, user: AuthUser.fromJson(json));
} on FormatException {
await clear();
return null;
}
}
}
这里有一个细节:本地用户 JSON 解析失败时会 clear(),不会继续使用损坏会话。移动端本地存储并不总是可靠,升级版本、字段变化、异常写入都可能导致旧数据不能解析。
登录流程
AuthRepository.login 只负责请求接口和解析返回,AuthController.login 负责状态变更、token 写入和 UI 通知:
dart
Future<bool> login(String account, String password) async {
error = null;
notifyListeners();
try {
final result = await _repository.login(account.trim(), password);
StoriesRepository.clearCache();
_apiClient.accessToken = result.token;
user = result.user;
await _sessionStore.write(result.token, result.user);
status = AuthStatus.signedIn;
notifyListeners();
return true;
} catch (exception) {
error = _message(exception);
notifyListeners();
return false;
}
}
登录成功后清理故事缓存是合理的。不同用户看到的故事、照片、家人关系都不一样,如果不清缓存,切账号后可能看到上个账号的数据。
启动恢复登录
initialize() 会先读本地 session,如果存在 token,就请求 /api/auth/me 校验:
dart
Future<void> initialize() async {
final session = await _sessionStore.read();
if (session == null) {
status = AuthStatus.signedOut;
notifyListeners();
return;
}
try {
user = await _repository.me(session.token);
await _sessionStore.write(session.token, user!);
status = AuthStatus.signedIn;
} catch (_) {
await _sessionStore.clear();
_apiClient.accessToken = null;
StoriesRepository.clearCache();
status = AuthStatus.signedOut;
}
notifyListeners();
}
这个设计避免了一个常见问题:本地有 token 但服务端已经失效,App 还误以为登录成功。这里通过 /me 做一次真实校验,失败就清理本地会话。
退出登录
退出登录不只是回到登录页,还要清理 token、用户、错误状态和业务缓存:
dart
Future<void> logout() async {
await _sessionStore.clear();
_apiClient.accessToken = null;
StoriesRepository.clearCache();
user = null;
error = null;
status = AuthStatus.signedOut;
notifyListeners();
}
在多用户应用里,退出时清业务缓存很关键。照片故事类数据带有强烈用户属性,不能跨账号残留。

扩展拆解
登录态和业务缓存的关系
登录成功后,项目会调用 StoriesRepository.clearCache()。这一步很重要,因为故事、照片、地图、家人关系都和当前账号有关。如果切换账号时不清缓存,可能出现新账号看到旧账号故事的严重问题。
dart
final result = await _repository.login(account.trim(), password);
StoriesRepository.clearCache();
_apiClient.accessToken = result.token;
user = result.user;
await _sessionStore.write(result.token, result.user);
status = AuthStatus.signedIn;
退出登录同样要清理:
dart
await _sessionStore.clear();
_apiClient.accessToken = null;
StoriesRepository.clearCache();
user = null;
status = AuthStatus.signedOut;
这说明登录模块不是孤立的表单功能,它会影响整个应用的数据边界。移动端项目里 token、用户信息、业务缓存必须同步管理,不能只保存 token。
注册和资料更新
注册只负责创建账号,不直接登录。资料更新、头像上传成功后,会更新当前用户并写回本地 session。这样应用重启后,昵称和头像不会回到旧状态。
dOut;
这说明登录模块不是孤立的表单功能,它会影响整个应用的数据边界。移动端项目里 token、用户信息、业务缓存必须同步管理,不能只保存 token。
## 注册和资料更新
注册只负责创建账号,不直接登录。资料更新、头像上传成功后,会更新当前用户并写回本地 session。这样应用重启后,昵称和头像不会回到旧状态。