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

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

用 AuthController、AuthScope 和 shared_preferences_ohos 管理登录态。

登录态入口

登录态管理在 lib/features/auth 目录下,核心文件是 auth_controller.dartauth_repository.dartsession_store.dartauth_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。这样应用重启后,昵称和头像不会回到旧状态。
相关推荐
程序员黑豆7 小时前
鸿蒙应用开发:一次开发多端部署之响应式布局完全指南
前端·harmonyos
可爱系程序猿9 小时前
网络打印机地址正常却无法出纸:从 TCP/IP 端口到 Print Spooler 的排错步骤
网络·网络协议·tcp/ip
Dontla11 小时前
预置known_hosts如何防止中间人攻击(MITM)?(服务器公钥、服务器私钥)Runner、DNS污染、BGP劫持、同网络ARP欺骗、SSH握手
服务器·网络·ssh
条tiao条13 小时前
MVVM架构与ArkUI状态管理
华为·架构·harmonyos·鸿蒙·mvvm
liulilittle14 小时前
UCP 控制协议 [简述]
网络·网络协议·计算机网络·udp·ip·通信
程序员黑豆15 小时前
鸿蒙应用开发:一次开发多端适配与自适应布局实战
前端·harmonyos
Lust Dusk16 小时前
记一次Linux应急响应题目解析
linux·运维·服务器·网络·安全·网络安全
三翼鸟数字化技术团队16 小时前
GN (generate ninja) 学习手册
harmonyos
程序员黑豆17 小时前
鸿蒙应用开发:网络请求三种方式详解(http / rcp / axios)
前端·harmonyos
凌肖战17 小时前
TCP网络通信中setsockopt()函数解决地址已占用问题
服务器·网络·tcp/ip