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

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。这样应用重启后,昵称和头像不会回到旧状态。
相关推荐
anew___3 分钟前
《从零手写操作系统 (30):环境变量与进程上下文——export/unset与继承语义》
java·开发语言·网络·jvm·算法
谢亮_vipxieliang24 分钟前
Go GMP调度模型——从原理到实战的完整指南
开发语言·网络·golang
天空之城--25 分钟前
Android一周动态:AI编程、Compose与车载生态新信号
android·flutter
wdfk_prog1 小时前
LWIP教程 03:从 `low_level_input()` 到 `pbuf_free()`——`pbuf` 的数据视图、Chain 与引用计数
运维·网络·笔记·学习
福兮说1 小时前
IP 地址转整数的七个坑:192.168.1.1 算出负数、127.1 也是合法地址、存进 INT 直接溢出
javascript·网络·网络协议·tcp/ip·mysql·golang
xiaoye-duck1 小时前
《Linux 网络编程》深入理解 epoll(上):从接口到底层内核工作机制剖析
linux·网络
码林鼠2 小时前
Flutter之Widget/State 入门文档
flutter
恋猫de小郭2 小时前
Dart 4.0 要彻底移除 dart:mirrors,Augmentations 应该要来了
android·前端·flutter
blanks20202 小时前
flutter 原生平台插件学习记录之 android 篇
flutter
anew___2 小时前
《从零手写操作系统 (29):管道与重定向进阶——命名管道、Here Document与Shell语法扩展》
java·开发语言·前端·javascript·网络