Android Jetpack 应用架构指南

Android Jetpack 应用架构指南

本指南涵盖 Android 应用开发的最佳实践和推荐架构,助力开发者构建健壮高效的应用程序。。

📌 前置要求

本文假设您已具备 Android 框架基础知识。若需系统学习 Android 开发,建议先完成《Android 基础知识

目录

文档修订记录

版本号 修订日期 修订内容 修订人
v1.0 2025-02-02 初始版本发布 小李子

责任豁免

  • 文档性质 本文档系个人学术研究产物,不隶属于任何官方实体
  • 使用建议 仅供自学者参考学习

需要权威资料的请移步 官方社区文档。参考文献

协作通道

新架构设计背景

Android 应用面临独特挑战:

  • 多任务环境 用户频繁切换应用
  • 资源受限 系统可能随时回收进程
  • 组件独立性 组件启动顺序不可控

传统架构痛点:

  • 数据状态存储在易销毁的组件中
  • 组件间存在强耦合
  • 业务逻辑与界面深度绑定

新架构核心目标:

  • 抗进程回收 保证数据持久性
  • 组件解耦 降低维护复杂度
  • 可测试性 支持分层测试
  • 状态一致性 统一数据源管理

移动应用交互特性

典型 Android 应用由以下核心组件构成:

组件类型 角色定位 核心职责 生命周期 声明要求
Activity 界面容器主界面 用户交互主入口 系统托管
Fragment 模块化界面单元 界面复用与组合 宿主控制
Service 后台服务 无界面长时任务处理 系统托管
ContentProvider 数据共享桥梁 跨应用数据访问接口 系统托管
BroadcastReceiver 全局事件监听器 系统 / 应用级广播处理 瞬时存活

组件需在应用清单 AndroidManifest.xml 中声明,示例:


<manifest>
    <application>
        <activity android:name=".MainActivity" />    <!-- 主交互界面 -->
        <service android:name=".DownloadService" />  <!-- 后台服务 -->
        <provider android:name=".DataProvider" />    <!-- 数据共享 -->
        <receiver android:name=".NetworkReceiver" /> <!-- 广播监听 -->
    </application>
</manifest>

核心架构原则

🔑 黄金法则

应用组件应 作为轻量级通道,禁止存储核心业务数据与状态

分离关注点

界面组件职责边界:

  • 仅处理 UI 渲染和用户输入
  • 代码量控制在 500 行以内
  • 通过委托处理业务逻辑
// 正例:Activity 仅处理 UI 逻辑
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate() {
        viewModel.uiState.observe(this) { state ->
            updateUI(state) // UI 更新逻辑
        }
    }

    private fun onRefreshClicked() = viewModel.loadData()
}
// 反例:Activity 包含数据加载逻辑
class BadActivity : AppCompatActivity() {
    override fun onCreate() {
        // 错误:混合网络请求与UI逻辑
        CoroutineScope(Dispatchers.IO).launch {
            val data = fetchData()
            runOnUiThread { updateUI(data) }
        }
    }
}

数据模型驱动界面

数据模型是最好是持久性模型,数据模型代表应用的数据独立于界面元素和其他组件,与界面和应用组件的生命周期无关,在操作系统销毁应用进程时会被移除

  • 持久化数据层优势
    • ✅ 进程终止时数据不丢失
    • ✅ 弱网络环境下可用
    • ✅ 业务逻辑可测试性提升 60%+
    • ✅ 数据源统一管理

单一数据源

在应用中定义新数据类型时,应为其指定单一数据源 SSOT, 指的是该数据的持有者,只有持有者可以修改或转换数据。
为实现这一点,SSOT 会以不可变类型公开数据,并提供可供其他类型调用的函数或事件来修改数据

  • 单一数据源优势
    • ✅ 集中管理特定类型数据的所有更改
    • ✅ 保护数据,防止其他类型篡改
    • ✅ 便于跟踪数据更改,更易发现 bug

在离线优先应用中,数据库通常作为应用数据的单一数据源;在其他情况下,单一数据源也可以是 ViewModel 甚至界面

实施要点:

  • 每个数据类型明确唯一持有者
  • 通过不可变接口暴露数据
  • 修改操作集中化管理
graph LR
    A[UI事件] --> B(ViewModel)
    B --> C{业务逻辑}
    C --> D[Repository]
    D --> E[[本地数据库]]
    D --> F[[远程API]]
    E -->|数据变更| D
    F -->|网络响应| D
    D -->|更新数据| B
    B -->|状态更新| A

单向数据流

单一数据源原常与单向数据流 UDF 模式结合使用。在 UDF 模式下,状态仅沿一个方向流动,而修改数据的事件则朝相反方向流动

在 Android 中,状态或数据通常从分区层次结构中较高的分区类型流向较低的分区类型,事件则通常在较低分区类型触发,直至到达 SSOT 对应的相关数据类型。
例如,应用数据一般从数据源流向界面,而用户事件(如按钮点击)则从界面流向 SSOT,在 SSOT 中修改应用数据并以不可变类型公开。

该模式能更好地保证数据一致性减少错误便于调试,同时具备 SSOT 模式的所有优势

典型数据流

  1. 用户触发 UI 事件
  2. ViewModel 处理事件
  3. Repository 更新持久层
  4. 数据库通知变更
  5. UI 自动同步新状态

单向数据流优势

  • ✅ 数据流向可追踪
  • ✅ 状态变更可预测
  • ✅ 减少竞态条件风险

分层架构设计

基于上述常见架构原则,每个应用至少应包含两个层:

  • 界面层 (必须):负责在屏幕上显示应用数据
  • 数据层 (必须):包含应用的业务逻辑,并公开应用数据
  • 领域层 (可选):复杂的业务逻辑
  • 本地数据源 (可选):为数据层提供数据
  • 远程数据源 (可选):为数据层提供数据

推荐分层结构:

app/
├─ ui/       # 界面层
├─ domain/   # 领域层(可选)
└─ data/     # 数据层

现代应用架构倡导采用以下方法及其他相关方法:

  • 反应式分层架构
  • 应用各层采用单向数据流 UDF
  • 界面层包含状态容器,用于管理界面复杂性
  • 运用协程和数据流
  • 遵循依赖项注入最佳实践

界面层

界面层(或呈现层)的 主要 功能是在屏幕上显示应用数据
无论数据变化是由用户交互(如按钮点击)还是外部输入(如网络响应)引起,界面都应及时更新以反映这些变化。

界面层组成部分:

组件 职责 推荐技术
Activity / Fragment 生命周期管理与界面容器 ViewBinding
ViewModel 状态管理与业务逻辑代理 ViewModel
UI Elements 具体界面元素渲染 Compose / XML Layout

代码规范:

  • 每个界面组件对应独立 ViewModel
  • 使用 StateFlow / LiveData 暴露状态
  • 事件处理使用 Channel / SharedFlow
class MainViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun loadData() = viewModelScope.launch {
        _uiState.value = Loading
        try {
            val data = repository.fetchData()
            _uiState.value = Success(data)
        } catch (e: Exception) {
            _uiState.value = Error(e)
        }
    }
}

详情参考:Jetpack 界面层 官方文档

数据层

应用的数据层 包含业务逻辑,这些业务逻辑决定了应用的价值,包括应用创建存储更改数据的规则。

数据层由多个仓库组成,每个仓库可包含零到多个数据源。针对应用中处理的每种不同类型的数据,应分别创建一个存储库类。例如,为电影相关数据创建
MoviesRepository 类,为付款相关数据创建 PaymentsRepository 类。

在典型架构中,数据层的仓库向应用的其他部分提供数据,同时依赖于数据源。

典型结构:

data/
├─ repository/
│  └─ UserRepository.kt
├─ model/
│  └─ User.kt
└─ datasource/
   ├─ local/
   └─ remote/

存储库类承担以下任务:

  • 向应用其他部分公开数据
  • 集中处理数据变化
  • 解决多个数据源之间的冲突
  • 对应用其他部分的数据源进行抽象化处理
  • 包含业务逻辑

Repository 模式:

class UserRepository(
    private val localSource: UserLocalDataSource,
    private val remoteSource: UserRemoteDataSource
) {
    suspend fun getUsers(): List<User> {
        return try {
            val remoteData = remoteSource.fetchUsers()
            localSource.cacheUsers(remoteData)
            remoteData
        } catch (e: IOException) {
            localSource.getCachedUsers()
        }
    }
}

数据源实现:

class UserRemoteDataSource(
    private val api: UserApi
) {
    suspend fun fetchUsers(): List<User> {
        return api.getUsers().mapToDomain()
    }
}

每个数据源类仅负责处理一个数据源,数据源可以是文件、网络来源或本地数据库。数据源类是应用与数据操作系统之间的桥梁

详情参考:Jetpack 数据层 官方文档

领域层

使用场景:

  • 复杂业务逻辑复用
  • 跨多个 ViewModel 的共享逻辑
  • 业务规则抽象

领域层是位于界面层与数据层之间的可选层。它负责封装复杂的业务逻辑,或供多个 ViewModel
复用的简单业务逻辑。

若添加了此层,领域层会向界面层提供依赖项,同时自身依赖于数据层。此层中的类通常称为“用例”或“交互方”,每个用例应仅负责单一功能。
例如,若多个ViewModel 依赖时区在屏幕上显示适当消息,
应用可能包含 GetTimeZoneUseCase 类

用例示例:

class GetUserProfileUseCase(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(userId: String): UserProfile {
        return userRepository.getProfile(userId)
            .validate()
            .transform()
    }
}

详情参考:Jetpack 领域层 官方文档


依赖管理方案

应用中的类需要依赖其他类才能正常工作,方案对比:

方案 优点 缺点
手动注入 简单直接 难以维护大型项目
Hilt 类型安全、自动装配 需要学习曲线
服务定位器 灵活配置 运行时错误风险
  • 依赖注入 (DI):依赖注入使类能够定义其依赖项,而无需自行构造。在运行时,由另一个类负责提供这些依赖项
  • 服务定位器:服务定位器模式提供一个注册表,类可从中获取其依赖项,而无需自行构造

借助这些模式可扩展代码,它们提供了清晰的依赖项管理方式,避免了代码复制和复杂性增加。此外,还能在测试和生产实现之间快速切换

建议在 Android 应用中采用依赖项注入模式,并使用 Hilt。它可自动遍历依赖项树构造对象,为依赖项提供编译时保证,并为 Android 框架类创建依赖项容器。
Hilt 最佳实践:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()

    @Provides
    fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
}

工程实践指南

编程具有创造性,构建 Android 应用也不例外。在多个 ActivityFragment
之间传递数据、检索远程数据并本地存储以支持离线使用,或处理复杂应用中的其他常见情况时,往往有多种解决方案。

虽然以下建议并非强制要求,但在大多数情况下,遵循这些建议可使代码库更强大、更具可测试性且更易维护:

  • 避免在应用组件中存储数据 不要将数据存储在应用组件中
  • 合理指定数据源 避免将应用的入口点(如 ActivityServiceBroadcast Receivers
    )指定为数据源,应仅让它们与其他组件协作,以检索与该入口点相关的数据子集。应用组件的生命周期取决于用户与设备的交互以及系统整体运行状况,通常较为短暂。
  • 减少对 Android 类的依赖 应用组件应是唯一依赖 Android 框架 SDK API(如 ContextToast)的类。将其他类与这些类分离,有助于提高可测试性,降低应用耦合度。
  • 明确模块职责界限 在应用的各个模块之间设定清晰的职责界限。例如,避免将从网络加载数据的代码分散在多个类或软件包中,也不要将不相关的职责(如数据缓存和数据绑定)定义在同一个类中。遵循推荐的应用架构有助于解决此问题。
  • 谨慎公开模块代码 尽量减少每个模块中代码的公开程度。例如,不要为提供模块内部实现细节而创建捷径。短期内可能节省时间,但随着代码库的发展,可能会引发技术问题。
  • 专注应用核心特色 专注于应用的独特核心功能,使应用脱颖而出。避免重复编写样板代码,应将时间和精力集中在让应用与众不同的方面,利用 Jetpack 库及其他建议库处理重复工作。
  • 确保各部分可独立测试 考虑如何使应用的每个部分可独立测试。例如,使用明确定义的 API 从网络获取数据,可更方便地测试将数据保留在本地数据库的模块。若将两个模块的逻辑混在一起,或网络代码分散在整个代码库中,测试难度将大大增加。
  • 类型负责并发政策 如果某种类型正在执行长时间运行的阻塞工作,应负责将该计算移至正确的线程。该类型应具备主线程安全性,即可以安全地从主线程调用而不会阻塞。
  • 保留相关和最新数据 尽可能保留更多相关和最新的数据,以确保即使用户设备处于离线模式,也能正常使用应用功能。要考虑到并非所有用户都能拥有稳定的高速网络连接,在拥挤区域网络信号可能不佳。

十大黄金准则:

  1. 数据存储 永远不在Activity中保留重要数据,使用StateFlow+ViewModel持久化界面状态
  2. 数据缓存 实现内存 + 磁盘二级缓存策略
  3. 线程安全 通过Coroutine+DispatchersCoroutineScope明确线程边界
  4. 响应式编程 采用Flow处理异步数据流,LiveData代替直接回调
  5. 模块化设计 按功能拆分独立feature模块
  6. 测试覆盖 实现分层测试策略,UI 层用EspressoViewModelJUnit
  7. 资源回收onDestroy中释放协程作用域
  8. 异常处理 全局异常监控(Crashlytics集成)
  9. 性能优化 使用Room+PagingPaging 3实现分页加载
  10. 持续演进 定期进行架构健康度评估

质量指标对比:

指标 传统架构 Jetpack 架构 提升幅度
崩溃率 2.1% 0.6% 71%↓
冷启动时间 1200ms 650ms 46%↓
代码复用率 38% 75% 97%↑
单元测试覆盖率 40% 85% 113%↑
CI/CD 通过率 82% 96% 17%↑

数据来源:Google 2023 Android 开发者调研报告

参考文献

Android Jetpack 使用入门

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容

  • 当我在做 Android 版本适配工作的时候很痛苦,那个时候我在想有没有一个文档,将所有的关于 Android 版...
    i小灰阅读 52,580评论 1 58
  • Android 14 应用适配指南:https://dev.mi.com/distribute/doc/detai...
    breaktian阅读 6,039评论 0 0
  • 背景 最近在准备面试,结合之前的工作经验和近期在网上收集的一些面试资料,准备将Android开发岗位的知识点做一个...
    hahaoop阅读 459评论 0 2
  • 概述 虚拟内存2.1 分页2.2 内存映射 内存不足时的处理3.1 kswapd3.2 LMK 虚拟机4.1 堆空...
    墨染书阅读 2,774评论 1 2
  • 目录介绍 01.学习JNI开发流程1.1 JNI开发概念1.2 JNI和NDK的关系1.3 JNI实践步骤1.4 ...
    杨充211阅读 371评论 0 2