Android Jetpack 应用架构指南
本指南涵盖 Android 应用开发的最佳实践和推荐架构,助力开发者构建健壮高效的应用程序。。
📌 前置要求
本文假设您已具备 Android 框架基础知识。若需系统学习 Android 开发,建议先完成《Android 基础知识》
目录
文档修订记录
版本号 | 修订日期 | 修订内容 | 修订人 |
---|---|---|---|
v1.0 | 2025-02-02 | 初始版本发布 | 小李子 |
责任豁免
- 文档性质 本文档系个人学术研究产物,不隶属于任何官方实体
- 使用建议 仅供自学者参考学习
需要权威资料的请移步 官方社区文档。参考文献
协作通道
- 邮件支持 740091075@qq.com
- 评论讨论 谢谢你的支持!
新架构设计背景
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 模式的所有优势
典型数据流:
- 用户触发 UI 事件
- ViewModel 处理事件
- Repository 更新持久层
- 数据库通知变更
- 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 应用也不例外。在多个 Activity
或 Fragment
之间传递数据、检索远程数据并本地存储以支持离线使用,或处理复杂应用中的其他常见情况时,往往有多种解决方案。
虽然以下建议并非强制要求,但在大多数情况下,遵循这些建议可使代码库更强大、更具可测试性且更易维护:
- 避免在应用组件中存储数据 不要将数据存储在应用组件中
-
合理指定数据源 避免将应用的入口点(如
Activity
、Service
和Broadcast Receivers
)指定为数据源,应仅让它们与其他组件协作,以检索与该入口点相关的数据子集。应用组件的生命周期取决于用户与设备的交互以及系统整体运行状况,通常较为短暂。 -
减少对 Android 类的依赖 应用组件应是唯一依赖 Android 框架 SDK API(如
Context
或Toast
)的类。将其他类与这些类分离,有助于提高可测试性,降低应用耦合度。 - 明确模块职责界限 在应用的各个模块之间设定清晰的职责界限。例如,避免将从网络加载数据的代码分散在多个类或软件包中,也不要将不相关的职责(如数据缓存和数据绑定)定义在同一个类中。遵循推荐的应用架构有助于解决此问题。
- 谨慎公开模块代码 尽量减少每个模块中代码的公开程度。例如,不要为提供模块内部实现细节而创建捷径。短期内可能节省时间,但随着代码库的发展,可能会引发技术问题。
- 专注应用核心特色 专注于应用的独特核心功能,使应用脱颖而出。避免重复编写样板代码,应将时间和精力集中在让应用与众不同的方面,利用 Jetpack 库及其他建议库处理重复工作。
- 确保各部分可独立测试 考虑如何使应用的每个部分可独立测试。例如,使用明确定义的 API 从网络获取数据,可更方便地测试将数据保留在本地数据库的模块。若将两个模块的逻辑混在一起,或网络代码分散在整个代码库中,测试难度将大大增加。
- 类型负责并发政策 如果某种类型正在执行长时间运行的阻塞工作,应负责将该计算移至正确的线程。该类型应具备主线程安全性,即可以安全地从主线程调用而不会阻塞。
- 保留相关和最新数据 尽可能保留更多相关和最新的数据,以确保即使用户设备处于离线模式,也能正常使用应用功能。要考虑到并非所有用户都能拥有稳定的高速网络连接,在拥挤区域网络信号可能不佳。
十大黄金准则:
-
数据存储 永远不在
Activity
中保留重要数据,使用StateFlow
+ViewModel
持久化界面状态 - 数据缓存 实现内存 + 磁盘二级缓存策略
-
线程安全 通过
Coroutine
+Dispatchers
、CoroutineScope
明确线程边界 -
响应式编程 采用
Flow
处理异步数据流,LiveData
代替直接回调 -
模块化设计 按功能拆分独立
feature
模块 -
测试覆盖 实现分层测试策略,UI 层用
Espresso
,ViewModel
用JUnit
-
资源回收 在
onDestroy
中释放协程作用域 -
异常处理 全局异常监控(
Crashlytics
集成) -
性能优化 使用
Room
+Paging
、Paging 3
实现分页加载 - 持续演进 定期进行架构健康度评估
质量指标对比:
指标 | 传统架构 | Jetpack 架构 | 提升幅度 |
---|---|---|---|
崩溃率 | 2.1% | 0.6% | 71%↓ |
冷启动时间 | 1200ms | 650ms | 46%↓ |
代码复用率 | 38% | 75% | 97%↑ |
单元测试覆盖率 | 40% | 85% | 113%↑ |
CI/CD 通过率 | 82% | 96% | 17%↑ |
数据来源:Google 2023 Android 开发者调研报告