Immich 移动端 Infrastructure 层解析:用 Drift 与 Riverpod 构建数据访问实现
本文以 Immich 移动端工程中的 mobile/lib/infrastructure/ 目录及其架构说明文档为核心,深入讲解 Immich Flutter 客户端基础设施层(Infrastructure Layer)的职责定位、目录组织、典型 Repository 实现模式(Drift 数据库访问、DTO 转换)以及通过 Riverpod 完成依赖注入的使用方式。读完本文,你将能够看懂 Immich 移动端数据层的分层设计,并掌握"领域接口 + 基础设施实现 + Provider 组装"这一套在真实大型 Flutter 项目中的落地范式。
基础设施层在 Immich 移动端中的定位
Immich 移动端(位于 mobile/ 目录)采用清晰的架构分层:domain 层定义业务逻辑与数据访问契约,infrastructure 层负责这些契约的具体实现,presentation 层则只消费上层服务。mobile/lib/infrastructure/README.md 对该层的定义是:
The infrastructure layer is responsible for the implementation details of the app. It includes data sources, APIs, and other external dependencies.(基础设施层负责应用实现细节,包括数据源、API 以及其他外部依赖。)
也就是说,这一层回答的问题是"数据从哪里来、如何落地":本地 SQLite(通过 Drift 框架)、HTTP API 请求、键值缓存、网络状态监听等具体技术选型全部被封装在这里。领域层(domain)只面向接口编程,从不直接 new 一个实现类——这一点在 mobile/lib/infrastructure/README.md 的 Usage 一节中被明确强调:
The domain layer should never directly instantiate repository implementations, but instead receive them through dependency injection.(领域层绝不应该直接实例化 Repository 实现,而应通过依赖注入获取。)
目录结构:Repositories 与 Utils
按照 mobile/lib/infrastructure/README.md 的说明,该目录的组织方式为:
infrastructure/
├── repositories/
│ └── user.repository.dart
└── utils/
└── user.converter.dart
- Repositories:领域层接口(或数据访问契约)的具体实现,单个接口可能对应多个实现(例如本地数据源与远端 API 数据源分离);
- Utils:与基础设施实现绑定的工具类与函数,典型代表是 DTO 之间的转换器(converter)。
实际目录中,mobile/lib/infrastructure/repositories/ 下已沉淀了约 28 个仓储实现,覆盖了 Immich 移动端几乎所有核心数据域:user.repository.dart(用户本地表)、user_api.repository.dart(用户远端 API)、settings.repository.dart(设置)、log.repository.dart(日志)、network.repository.dart(网络监听)、timeline.repository.dart(时间线)、tags_api.repository.dart(标签)等。从命名规律可以推断出该项目的双数据源惯例:*_api.repository.dart 负责与服务端 HTTP 接口交互,同名不带后缀的 *.repository.dart 负责本地 Drift 数据库持久化,两者在领域服务中组合使用(先取缓存、再刷新、后写回),这正是"一个接口可能有多个实现"的典型体现。
实现细节一:基于 Drift 的本地数据库仓储
以 mobile/lib/infrastructure/repositories/user.repository.dart 为例,可以完整看到基础设施层如何使用 Drift 构建类型安全的数据库访问:
@DriftAccessor()
class UserRepository extends DatabaseAccessor<Drift> with $UserRepositoryMixin {
UserRepository(super.attachedDatabase);
Drift get _db => attachedDatabase;
Stream<Iterable<User>> getAll() => _db.select(_db.userEntity).map(mapToUser).watch();
Stream<User?> watch(String id) =>
(_db.select(_db.userEntity)..where((u) => u.id.equals(id))).map(mapToUser).watchSingleOrNull();
}
这里有几个值得注意的实现模式:
@DriftAccessor()代码生成:类继承DatabaseAccessor<Drift>并混入生成代码$UserRepositoryMixin(来自user.repository.drift.dart),查询能力由 Drift 的代码生成器从 schema 推导,避免了手写 SQL 字符串;- 响应式返回类型:
getAll()/watch(id)返回的是Stream而非一次性Future,UI 层订阅即可随数据库变更自动刷新,这是 Drift 与 FlutterStreamBuilder/Riverpod 生态配合的标准用法; - 实体到领域模型的映射:查询结果通过
.map(mapToUser)转换为领域模型User,映射函数定义在 mobile/lib/infrastructure/mapper.dart:
User mapToUser(UserEntityData data) => User(
id: data.id,
name: data.name,
email: data.email,
hasProfileImage: data.hasProfileImage,
profileChangedAt: data.profileChangedAt,
avatarColor: data.avatarColor,
);
同一个文件中还包含 AuthUserRepository,它演示了更完整的"读 + 写"模式:读取时先从 authUserEntity 表按主键取行,再联表查询 userMetadataEntity 组装出 UserDto(toDto(metadata));写入时则通过 insertOnConflictUpdate(即 upsert)把 UserDto 的字段逐个装入 AuthUserEntityCompanion 落库。注意这里对 UserMetadataKey.preferences 的解析逻辑——memoryEnabled 默认 true,仅当用户元数据中显式保存了偏好时才覆盖,这种"默认值兜底"的防御式写法是基础设施层处理脏数据/缺字段的常见手法。
实现细节二:通用键值仓储与 Settings 的特化
并非所有仓储都是面向"表"的。mobile/lib/infrastructure/repositories/cached_key_value_repository.dart 定义了一个抽象泛型基类 CachedKeyValueRepository<K extends Enum, S>,它把"枚举键 → 字符串值 → 解码为强类型快照"这条通用链路固化下来:
abstract class CachedKeyValueRepository<K extends Enum, S> {
S _snapshot;
S get snapshot => _snapshot;
List<K> get keys;
Object decodeValue(K key, String raw);
S buildSnapshot(Map<K, Object?> overrides);
Selectable<({String key, String? value})> selectable();
Future<void> refresh() async => _snapshot = _build(await selectable().get());
Stream<S> watchSnapshot() => selectable().watch().map((rows) => _snapshot = _build(rows));
// ..._build 内部将 key-value 行 fold 成 overrides 后调用 buildSnapshot
}
具体实现 mobile/lib/infrastructure/repositories/settings.repository.dart 只需要回答四个问题:键枚举是 SettingsKey、解码用 key.decode(raw)、快照用 AppConfig.fromEntries 构建、可查询对象指向 _db.settingsEntity。由此获得了两个有价值的语义:
- 写回默认值即删除:
write()中如果新值等于defaultConfig.read(key),会走clear([key])直接删行而不是写"默认值",让数据库只保存真正被用户改过的键,保持存储精简; - 响应式配置流:
watchConfig()直接透传watchSnapshot(),UI 监听该 Stream 即可在设置变化时热更新。
LogRepository(mobile/lib/infrastructure/repositories/log.repository.dart)则展示了另一侧:日志落库、按 logger 过滤的 watchMessages 流、以及 truncate 保留最近 kLogTruncateLimit 条的容量控制逻辑,同样基于 Drift 的 select/orderBy/limit 组合完成。
实现细节三:Utils 中的 DTO 转换器
utils/ 目录承载与具体数据源格式解耦的转换逻辑。以 mobile/lib/infrastructure/utils/user.converter.dart 为例,UserConverter 是 abstract final class(纯静态工具类),负责把 OpenAPI 生成的三种服务端 DTO 统一归一为领域模型 UserDto:
static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto(
id: dto.id,
email: dto.email,
name: dto.name,
isAdmin: false,
updatedAt: DateTime.now(),
hasProfileImage: dto.profileImagePath.isNotEmpty,
profileChangedAt: dto.profileChangedAt,
avatarColor: dto.avatarColor.toAvatarColor(),
);
此外还有 fromAdminDto(管理端响应,携带 isAdmin、配额等字段,并可附带偏好信息推导 memoryEnabled)与 fromPartnerDto(伙伴分享用户)。文件底部的 toAvatarColor() 扩展把 API 层的 UserAvatarColor 枚举逐项 switch 映射为领域层的 AvatarColor。这类 converter 存在的意义在于:外部协议(OpenAPI DTO)的任何变动都被隔离在基础设施层内消化,领域模型与 UI 层不感知协议细节。文件顶部的 TODO: Move to repository once all classes are refactored 注释也印证了文档中提到的仓库正在逐实体迁移的现状。
使用方式:Riverpod 依赖注入链路
README 给出的核心使用约定是:基础设施层的实现通过根 providers 目录下的 Riverpod Provider 暴露给上层,领域服务只持有"接口/实例引用"而不关心构造过程。以用户模块为真实示例,领域服务 mobile/lib/domain/services/user.service.dart 的构造如下:
class UserService {
final UserApiRepository _userApiRepository;
final UserRepository _userRepository;
final StoreService _storeService;
UserService({required this._userApiRepository, required this._userRepository, required this._storeService});
Future<UserDto?> refreshMyUser() async {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
await _storeService.put(StoreKey.currentUser, user);
return user;
}
Stream<User?> watch(String id) => _userRepository.watch(id);
}
可以看到典型的双数据源协作:refreshMyUser() 通过远端 API 仓储拉取最新用户并写入共享状态,watch() 则直接暴露本地 Drift 仓储的响应式流给 UI 订阅。README 中给出的伪代码(final userRepository = ref.watch(userRepositoryProvider);)表达的就是这一组装思想——Provider 负责把 UserRepository 等实现注入 UserService,而 mobile/lib/domain/README.md 补充了对称的另一半约束:presentation 层不应直接使用 repositories,而应通过 services 交互。三层之间的依赖方向因此被严格约束为 presentation → domain → infrastructure(经 DI 反转)。
演进中的注意:数据访问正在向 lib/data 迁移
README 中有一条重要的迁移注记:
The Drift schema, the database class, and the server API base moved to
lib/data/. Repositories here are migrating there one entity at a time; new data access should be added underlib/data/.
也就是说,Drift schema 定义、数据库类以及服务端 API 基座已经迁移到 mobile/lib/data/(包含 db/ 数据库层与 server/ API 层),而 infrastructure/repositories/ 中的仓储正按实体逐个随之迁移;新增的数据访问代码应当落在 lib/data/ 下,而不是继续往 infrastructure/ 里添加。这一信息对阅读旧代码与新代码的差异至关重要:你仍会看到部分仓储从 infrastructure/repositories/ import Drift 表定义(如 user.repository.dart 中 import 自 data/db/main/...),这正是过渡期的混合状态,而非两个数据库实现并存。
小结
mobile/lib/infrastructure/ 是 Immich 移动端"实现细节"的汇聚地:repositories/ 以 Drift 访问器、泛型键值仓储、API 仓储等形式实现数据契约,utils/ 与 mapper.dart 承担 DTO/实体与领域模型之间的转换隔离,Riverpod Provider 则把实现细节以依赖注入的方式交付给领域层。理解这一层后,再配合 mobile/lib/domain/README.md 的领域层说明与 mobile/lib/data/ 的迁移目标目录,即可完整把握 Immich 移动端从 UI 到数据库的整条数据流,也能在贡献代码时遵循"新数据访问进 lib/data/、实现经 Provider 注入"的现行规范。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00