目录
  1. 1. 一、企业级 Android 架构概述
    1. 1.1. 1.1 自顶向下的架构设计
    2. 1.2. 1.2 Gradle 多模块项目结构
    3. 1.3. 1.3 buildSrc 统一依赖管理
  2. 2. 二、Artifactory 依赖管理
    1. 2.1. 2.1 什么是 JFrog Artifactory
    2. 2.2. 2.2 Gradle Maven-Publish 插件配置
    3. 2.3. 2.3 消费内部库
    4. 2.4. 2.4 SNAPSHOT vs Release 版本策略
  3. 3. 三、数据层(Data Layer)设计
    1. 3.1. 3.1 Repository 模式 —— 单一数据源原则
    2. 3.2. 3.2 Room 作为本地缓存
    3. 3.3. 3.3 RemoteMediator —— 网络 + 本地分页
  4. 4. 四、领域层(Domain Layer)设计
    1. 4.1. 4.1 UseCase —— 封装单一业务操作
    2. 4.2. 4.2 数据模型分层映射
  5. 5. 五、UI 层(Presentation Layer)设计
    1. 5.1. 5.1 ViewModel with SavedStateHandle
    2. 5.2. 5.2 DataBinding vs ViewBinding
  6. 6. 六、依赖注入 —— Hilt 完整配置
    1. 6.1. 6.1 Hilt 基础配置
    2. 6.2. 6.2 模块声明
    3. 6.3. 6.3 ViewModel 的注入
    4. 6.4. 6.4 作用域注解
  7. 7. 七、CI/CD 集成
    1. 7.1. 7.1 Jenkins Pipeline 配置
    2. 7.2. 7.2 GitHub Actions 配置
  8. 8. 八、测试策略
    1. 8.1. 8.1 测试金字塔
    2. 8.2. 8.2 Repository 单元测试(Fake Data Source)
    3. 8.3. 8.3 ViewModel 测试(TestDispatcher)
  9. 9. 九、完整项目结构总结
  10. 10. 十、总结
  11. 11. 参考资源
【实战系列】MVVM之基于Artifactory和Jetpack高级架构

一、企业级 Android 架构概述

1.1 自顶向下的架构设计

企业级 Android 应用的架构不只是代码的组织方式,而是涵盖模块化、依赖管理、构建系统、CI/CD、测试策略的完整工程体系。一个成熟的企业应用架构,从下到上通常分为以下层次:

┌──────────────────────────────────────────────┐
│ 业务模块层 │
│ login │ home │ profile │ settings │
│ 各业务线独立模块,可独立开发、测试、部署 │
├──────────────────────────────────────────────┤
│ Common 公共层 │
│ common-ui │ common-utils │ common-widget │
│ 跨业务模块共享的 UI 组件和工具类 │
├──────────────────────────────────────────────┤
│ 网络层 (data) │
│ network │ api │ repository │
│ 统一网络请求、序列化、数据缓存、Repository │
├──────────────────────────────────────────────┤
│ Base 基础层 │
│ base-android │ base-common │
│ BaseActivity/Fragment/ViewModel 基类 │
├──────────────────────────────────────────────┤
│ JNI / Native 层 │
│ native-crypto │ native-codec │
│ 通过 JNI 与 C/C++ 交互 │
├──────────────────────────────────────────────┤
│ Android OS │
└──────────────────────────────────────────────┘

模块化原则

  • 高内聚:一个模块内所有类围绕一个功能或业务领域
  • 低耦合:模块之间通过接口/抽象类通信,不直接依赖具体实现
  • 单向依赖:上层依赖下层,下层不感知上层
  • 稳定抽象原则(SAP):越底层的模块越稳定,变更成本越高

1.2 Gradle 多模块项目结构

project-root/
├── app/ # 主应用模块(壳工程)
│ ├── src/
│ └── build.gradle.kts

├── features/ # 业务模块
│ ├── login/
│ │ └── build.gradle.kts
│ ├── home/
│ │ └── build.gradle.kts
│ └── profile/
│ └── build.gradle.kts

├── core/ # 核心模块
│ ├── common/
│ ├── network/
│ ├── database/
│ ├── ui-common/
│ └── base/

├── data/ # 数据层模块
│ ├── api/
│ ├── repository/
│ └── model/

├── domain/ # 领域层(可选但强烈推荐)
│ ├── usecase/
│ └── model/

├── buildSrc/ # Gradle 构建逻辑源码
│ └── src/main/kotlin/
│ ├── Dependencies.kt # 统一依赖版本管理
│ ├── Config.kt # 统一构建配置
│ └── Android.kt # Android 特定配置

├── settings.gradle.kts # 模块声明
├── build.gradle.kts # 根项目构建脚本
└── gradle.properties # Gradle 属性

settings.gradle.kts 模块注册

rootProject.name = "EnterpriseApp"

include(":app")

// Features
include(":features:login")
include(":features:home")
include(":features:profile")

// Core
include(":core:common")
include(":core:network")
include(":core:database")
include(":core:ui-common")
include(":core:base")

// Data
include(":data:api")
include(":data:repository")
include(":data:model")

// Domain
include(":domain:usecase")
include(":domain:model")

1.3 buildSrc 统一依赖管理

buildSrc/src/main/kotlin/Dependencies.kt

object Versions {
const val kotlin = "1.9.22"
const val agp = "8.2.2"
const val coroutines = "1.7.3"
const val lifecycle = "2.7.0"
const val navigation = "2.7.7"
const val room = "2.6.1"
const val hilt = "2.50"
const val retrofit = "2.9.0"
const val okhttp = "4.12.0"
const val moshi = "1.15.0"
const val glide = "4.16.0"
const val compose = "1.5.4"
const val composeCompiler = "1.5.8"
const val paging = "3.2.1"
const val datastore = "1.0.0"
const val workmanager = "2.9.0"
const val junit = "4.13.2"
const val mockk = "1.13.9"
const val turbine = "1.0.0"
const val espresso = "3.5.1"
const val timber = "5.0.1"
}

object Libs {
// Kotlin
const val kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"

// AndroidX Lifecycle
const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}"
const val lifecycleLiveData = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycle}"
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
const val lifecycleRuntimeCompose = "androidx.lifecycle:lifecycle-runtime-compose:${Versions.lifecycle}"
const val lifecycleSavedState = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.lifecycle}"

// Navigation
const val navigationFragment = "androidx.navigation:navigation-fragment-ktx:${Versions.navigation}"
const val navigationUi = "androidx.navigation:navigation-ui-ktx:${Versions.navigation}"

// Room
const val roomRuntime = "androidx.room:room-runtime:${Versions.room}"
const val roomKtx = "androidx.room:room-ktx:${Versions.room}"
const val roomCompiler = "androidx.room:room-compiler:${Versions.room}"
const val roomPaging = "androidx.room:room-paging:${Versions.room}"

// Hilt
const val hiltAndroid = "com.google.dagger:hilt-android:${Versions.hilt}"
const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"

// Retrofit + OkHttp
const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
const val retrofitMoshi = "com.squareup.retrofit2:converter-moshi:${Versions.retrofit}"
const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}"
const val okhttpLogging = "com.squareup.okhttp3:logging-interceptor:${Versions.okhttp}"

// Paging
const val pagingRuntime = "androidx.paging:paging-runtime-ktx:${Versions.paging}"
const val pagingCompose = "androidx.paging:paging-compose:${Versions.paging}"

// Testing
const val junit = "junit:junit:${Versions.junit}"
const val mockk = "io.mockk:mockk:${Versions.mockk}"
const val turbine = "app.cash.turbine:turbine:${Versions.turbine}"
const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}"
const val espresso = "androidx.test.espresso:espresso-core:${Versions.espresso}"

// Logging
const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
}

二、Artifactory 依赖管理

2.1 什么是 JFrog Artifactory

Artifactory 是 JFrog 开发的企业级二进制仓库管理器,支持 Maven、Gradle、Docker、npm、PyPI 等多种仓库格式。在企业 Android 开发中,它充当内部 Maven 仓库的角色。

核心价值

  • 内部库的版本管理和分发(不用 JitPack 或 Git Submodule)
  • 构建缓存加速(代理中央仓库,第一次拉取后局域网级速度)
  • 安全控制(公司代码不发布到公共仓库)
  • 构建产物管理(APK/AAB 的版本化存储)

2.2 Gradle Maven-Publish 插件配置

发布内部库到 Artifactory

// core/network/build.gradle.kts
plugins {
id("com.android.library")
id("kotlin-android")
id("maven-publish") // Gradle 内置的 Maven 发布插件
}

// 发布配置
afterEvaluate {
publishing {
publications {
create<MavenPublication>("release") {
from(components["release"])

groupId = "com.enterprise.core"
artifactId = "network"
version = findProperty("network.version") as String? ?: "1.0.0-SNAPSHOT"

pom {
name.set("Enterprise Network Library")
description.set("统一网络请求层,封装 Retrofit + OkHttp")
developers {
developer {
id.set("android-team")
name.set("Android Team")
}
}
}
}
}

repositories {
maven {
name = "Artifactory"
url = uri(if ((version as String).endsWith("SNAPSHOT")) {
"https://artifactory.enterprise.com/artifactory/libs-snapshot-local"
} else {
"https://artifactory.enterprise.com/artifactory/libs-release-local"
})
credentials {
username = findProperty("artifactory.user") as String?
?: System.getenv("ARTIFACTORY_USER")
password = findProperty("artifactory.password") as String?
?: System.getenv("ARTIFACTORY_PASSWORD")
}
}
}
}
}

2.3 消费内部库

// app/build.gradle.kts
repositories {
// 内部 Artifactory 替换公共仓库
maven {
url = uri("https://artifactory.enterprise.com/artifactory/libs-release")
credentials {
username = System.getenv("ARTIFACTORY_USER")
password = System.getenv("ARTIFACTORY_PASSWORD")
}
}
// Artifactory 的虚拟仓库:聚合 internal + jcenter + mavenCentral + google
maven {
url = uri("https://artifactory.enterprise.com/artifactory/virtual-android")
credentials {
username = System.getenv("ARTIFACTORY_USER")
password = System.getenv("ARTIFACTORY_PASSWORD")
}
}
}

dependencies {
// 内部库
implementation("com.enterprise.core:network:2.3.1")
implementation("com.enterprise.core:database:1.5.0")
implementation("com.enterprise.common:ui-widgets:3.1.0")

// 外部库
implementation(Libs.retrofit)
implementation(Libs.okhttp)
}

2.4 SNAPSHOT vs Release 版本策略

开发阶段:
feature branch → 1.0.0-SNAPSHOT (每日自动发布)
pull request → CI 自动使用最新的 SNAPSHOT 构建验证

测试阶段:
release candidate → 1.0.0-rc1 → Artifactory libs-release

发布阶段:
tag v1.0.0 → 1.0.0 → Artifactory libs-release (不可变)
后续补丁 → 1.0.1 → libs-release

Gradle 中的 SNAPSHOT 更新策略

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
// ...
}
}

// 在 gradle.properties 中控制 SNAPSHOT 更新频率
// systemProp.org.gradle.internal.publish.checksums.insecure=never
// 默认:Gradle 24 小时内不重复检查 SNAPSHOT
// 强制刷新:./gradlew build --refresh-dependencies

三、数据层(Data Layer)设计

3.1 Repository 模式 —— 单一数据源原则

Repository 是数据层的核心抽象,遵循单一数据源原则(Single Source of Truth, SSOT)

// data/repository/src/main/kotlin/UserRepository.kt
class UserRepositoryImpl(
private val api: UserApi,
private val dao: UserDao,
private val prefs: UserPreferences
) : UserRepository {

/**
* 获取用户信息的数据流策略:
* 1. 首先返回 Room 中的缓存数据
* 2. 随后发起网络请求刷新
* 3. 网络数据写入 Room,自动更新 UI
*/
override fun getUser(userId: String): Flow<Resource<User>> = flow {
emit(Resource.Loading)

// 1. 先从本地数据库获取
val cached = dao.getUser(userId)
if (cached != null) {
emit(Resource.Success(cached.toDomain()))
}

// 2. 从网络获取最新数据
try {
val response = api.getUser(userId)
if (response.isSuccessful && response.body() != null) {
val remote = response.body()!!.toEntity()
dao.upsertUser(remote) // 写入 Room
// Room 的 Flow 会自动触发 UI 更新
} else {
if (cached == null) {
emit(Resource.Error("用户不存在"))
}
}
} catch (e: IOException) {
if (cached == null) {
emit(Resource.Error("网络连接失败"))
}
// 有缓存的情况下不报错,继续显示缓存
}
}

override fun observeUser(userId: String): Flow<User> {
return dao.observeUser(userId).map { it.toDomain() }
}
}

3.2 Room 作为本地缓存

// data/database/src/main/kotlin/UserDao.kt
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUser(userId: String): UserEntity?

@Query("SELECT * FROM users WHERE id = :userId")
fun observeUser(userId: String): Flow<UserEntity?>

@Upsert
suspend fun upsertUser(user: UserEntity)

@Query("DELETE FROM users")
suspend fun clearAll()
}

// data/database/src/main/kotlin/UserEntity.kt
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
@ColumnInfo(name = "username") val username: String,
@ColumnInfo(name = "avatar_url") val avatarUrl: String?,
@ColumnInfo(name = "bio") val bio: String?,
@ColumnInfo(name = "updated_at") val updatedAt: Long
)

3.3 RemoteMediator —— 网络 + 本地分页

RemoteMediator(Paging 3)结合了网络请求和本地缓存的分页:

// data/repository/src/main/kotlin/UserRemoteMediator.kt
@OptIn(ExperimentalPagingApi::class)
class UserRemoteMediator(
private val api: UserApi,
private val db: AppDatabase
) : RemoteMediator<Int, UserEntity>() {

override suspend fun load(
loadType: LoadType,
state: PagingState<Int, UserEntity>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> 0
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
lastItem?.nextPage ?: return MediatorResult.Success(
endOfPaginationReached = true)
}
}

return try {
val response = api.getUsers(page = page, perPage = state.config.pageSize)
val users = response.body()?.map { it.toEntity() } ?: emptyList()

db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.userDao().clearAll()
}
db.userDao().insertAll(users)
}

MediatorResult.Success(
endOfPaginationReached = users.size < state.config.pageSize
)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
}

四、领域层(Domain Layer)设计

4.1 UseCase —— 封装单一业务操作

UseCase 是 Clean Architecture 的核心,每个 UseCase 代表一个单一的业务操作:

// domain/usecase/src/main/kotlin/LoginUseCase.kt
class LoginUseCase(
private val authRepository: AuthRepository,
private val analyticsTracker: AnalyticsTracker,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
/**
* 每个 UseCase 返回 Result 类型,由调用方决定如何处理错误
*/
suspend operator fun invoke(
username: String,
password: String
): Result<User> = withContext(dispatcher) {
// 1. 输入验证(业务规则)
if (username.length < 3) {
return@withContext Result.failure(ValidationException("用户名至少3个字符"))
}
if (password.length < 6) {
return@withContext Result.failure(ValidationException("密码至少6个字符"))
}

// 2. 调用 Repository
val result = authRepository.login(username, password)

// 3. 副作用(埋点)
result.fold(
onSuccess = { user ->
analyticsTracker.logEvent("login_success", mapOf(
"user_id" to user.id,
"method" to "password"
))
},
onFailure = { error ->
analyticsTracker.logEvent("login_failed", mapOf(
"error" to (error.message ?: "unknown"),
"username" to username
))
}
)

result
}
}

4.2 数据模型分层映射

┌─────────────────┐
│ UI Model │ ← presentation 层
│ (LoginUiState) │ 用于渲染,包含 UI 特定信息
├─────────────────┤
│ Domain Model │ ← domain 层
│ (User, Token) │ 纯业务模型,不依赖任何框架
├─────────────────┤
│ Data Model │ ← data 层
│ (UserEntity, │ 数据库实体、API 响应 DTO
│ UserDto) │
└─────────────────┘

模型映射扩展函数

// data/model → domain/model
fun UserDto.toDomain(): User = User(
id = this.id,
username = this.username,
avatarUrl = this.avatar_url,
bio = this.bio
)

// domain/model → data/entity
fun User.toEntity(): UserEntity = UserEntity(
id = this.id,
username = this.username,
avatarUrl = this.avatarUrl,
bio = this.bio,
updatedAt = System.currentTimeMillis()
)

// domain/model → ui/model
fun User.toUiState(): UserUiState = UserUiState(
id = this.id,
displayName = this.username,
avatar = this.avatarUrl ?: DEFAULT_AVATAR_URL,
bioDisplay = this.bio ?: "这个人很懒,什么都没写"
)

五、UI 层(Presentation Layer)设计

5.1 ViewModel with SavedStateHandle

// features/profile/src/main/kotlin/ProfileViewModel.kt
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase,
private val updateProfileUseCase: UpdateProfileUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {

// 从 SavedStateHandle 恢复状态(进程死亡后恢复)
private val userId: String = savedStateHandle.get<String>("userId")
?: savedStateHandle["myUserId"] ?: ""

// 使用 StateFlow 暴露 UI 状态
private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()

// 使用 SharedFlow 暴露一次性事件
private val _events = MutableSharedFlow<ProfileEvent>()
val events: SharedFlow<ProfileEvent> = _events.asSharedFlow()

init {
loadProfile()
}

fun loadProfile() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }

getUserUseCase(userId)
.onSuccess { user ->
_uiState.update {
it.copy(
isLoading = false,
user = user.toUiState(),
error = null
)
}
}
.onFailure { error ->
_uiState.update {
it.copy(
isLoading = false,
error = error.message ?: "加载失败"
)
}
_events.emit(ProfileEvent.ShowSnackbar(error.message ?: "加载失败"))
}
}
}

/**
* 更新状态并持久化到 SavedStateHandle
*/
fun onUserIdChanged(newUserId: String) {
savedStateHandle["userId"] = newUserId
loadProfile()
}
}

// UI State
data class ProfileUiState(
val user: UserUiState? = null,
val isLoading: Boolean = false,
val error: String? = null
)

// One-shot events
sealed class ProfileEvent {
data class ShowSnackbar(val message: String) : ProfileEvent()
data class NavigateTo(val destination: Int) : ProfileEvent()
}

5.2 DataBinding vs ViewBinding

ViewBinding(推荐用于简单场景):

// 无反射,编译时类型安全,APK 体积更小
class ProfileFragment : Fragment() {
private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!
private val viewModel: ProfileViewModel by viewModels()

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentProfileBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
binding.tvName.text = state.user?.displayName
binding.progressBar.isVisible = state.isLoading
}
}
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null // 防止内存泄漏
}
}

DataBinding(用于需要双向绑定的复杂表单):

<!-- layout/profile_fragment.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewModel" type="com.example.ProfileViewModel" />
<variable name="state" type="com.example.ProfileUiState" />
</data>

<LinearLayout ...>
<TextView
android:text="@{state.user.displayName}"
android:visibility="@{state.isLoading ? View.GONE : View.VISIBLE}" />

<EditText
android:text="@={viewModel.nickname}" />
</LinearLayout>
</layout>

六、依赖注入 —— Hilt 完整配置

6.1 Hilt 基础配置

// app/src/main/kotlin/MyApplication.kt
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Hilt 自动生成 Hilt_MyApplication,注入所有 @Singleton 依赖
}
}

// app/src/main/kotlin/MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// Hilt 自动注入
@Inject lateinit var analyticsTracker: AnalyticsTracker

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Hilt 在 super.onCreate() 之前已完成注入
}
}

6.2 模块声明

// core/network/src/main/kotlin/di/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun provideOkHttpClient(
authInterceptor: AuthInterceptor,
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}

@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}

@Provides
@Singleton
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
}

// core/database/src/main/kotlin/di/DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
)
.addMigrations(AppDatabase.MIGRATION_1_2)
.build()
}

@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}

// data/repository/src/main/kotlin/di/RepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {

@Provides
@Singleton
fun provideUserRepository(
api: UserApi,
dao: UserDao,
prefs: UserPreferences
): UserRepository {
return UserRepositoryImpl(api, dao, prefs)
}
}

6.3 ViewModel 的注入

// features/profile/src/main/kotlin/ProfileViewModel.kt
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase,
private val updateProfileUseCase: UpdateProfileUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ...
}

// Fragment 中使用
@AndroidEntryPoint
class ProfileFragment : Fragment() {
private val viewModel: ProfileViewModel by viewModels()
// Hilt 自动解析 ViewModel 的构造参数
}

6.4 作用域注解

作用域 注解 生命周期
应用 @Singleton Application 存活期间
ViewModel @ViewModelScoped ViewModel 存活期间
Activity @ActivityScoped Activity 存活期间
Fragment @FragmentScoped Fragment 存活期间
自定义 @ServiceScoped 自定义组件存活期间

七、CI/CD 集成

7.1 Jenkins Pipeline 配置

// Jenkinsfile
pipeline {
agent any

environment {
ARTIFACTORY_URL = 'https://artifactory.enterprise.com/artifactory'
ARTIFACTORY_USER = credentials('artifactory-user')
ARTIFACTORY_PASSWORD = credentials('artifactory-password')
}

stages {
stage('Checkout') {
steps {
checkout scm
}
}

stage('Build') {
parallel {
stage('Debug') {
steps {
sh './gradlew assembleDebug -Partifactory.user=$ARTIFACTORY_USER -Partifactory.password=$ARTIFACTORY_PASSWORD'
}
}
stage('Lint') {
steps {
sh './gradlew lint'
}
}
}
}

stage('Unit Test') {
steps {
sh './gradlew testDebugUnitTest'
}
post {
always {
junit '**/build/test-results/**/*.xml'
}
}
}

stage('Instrumentation Test') {
steps {
sh './gradlew connectedAndroidTest'
}
}

stage('Static Analysis') {
parallel {
stage('Detekt') {
steps {
sh './gradlew detekt'
}
}
stage('KtLint') {
steps {
sh './gradlew ktlintCheck'
}
}
}
}

stage('Publish to Artifactory') {
when {
branch 'main'
}
steps {
sh './gradlew publishReleasePublicationToArtifactoryRepository'
}
}

stage('Assemble Release') {
when {
branch 'main'
}
steps {
sh './gradlew assembleRelease'
// 上传 APK/AAB 到 Artifactory 或 App Center
}
}
}
}

7.2 GitHub Actions 配置

# .github/workflows/android-ci.yml
name: Android CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
task: [assembleDebug, lintDebug, testDebugUnitTest]

steps:
- uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}

- name: Build
env:
ARTIFACTORY_USER: ${{ secrets.ARTIFACTORY_USER }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
run: ./gradlew ${{ matrix.task }}

publish:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish to Artifactory
run: ./gradlew publish

八、测试策略

8.1 测试金字塔

   ┌──────┐
│ UI │ < 10% (Espresso / Compose Testing)
┌┴──────┴┐
│ 集成测试│ < 20% (ViewModel + Fake Repo)
┌┴────────┴┐
│ 单元测试 │ > 70% (UseCase / Repository / Reducer)
└───────────┘

8.2 Repository 单元测试(Fake Data Source)

// data/repository/src/test/kotlin/UserRepositoryTest.kt
class UserRepositoryTest {

private val api: UserApi = mockk()
private val dao: UserDao = mockk()
private val prefs: UserPreferences = mockk()
private lateinit var repository: UserRepositoryImpl

@Before
fun setup() {
repository = UserRepositoryImpl(api, dao, prefs)
}

@Test
fun `getUser returns cached data when offline`() = runTest {
val cachedUser = UserEntity("1", "test", null, null, 0L)
coEvery { dao.getUser("1") } returns cachedUser
coEvery { api.getUser("1") } throws IOException("Network error")

val result = mutableListOf<Resource<User>>()
repository.getUser("1").collect { result.add(it) }

// 应该先返回 Loading,再返回缓存的 Success
assertTrue(result.any { it is Resource.Loading })
assertTrue(result.any { it is Resource.Success })
// 不应该有 Error(因为有缓存)
assertTrue(result.none { it is Resource.Error })
}

@Test
fun `getUser returns error when offline and no cache`() = runTest {
coEvery { dao.getUser("1") } returns null
coEvery { api.getUser("1") } throws IOException("Network error")

val result = mutableListOf<Resource<User>>()
repository.getUser("1").collect { result.add(it) }

assertTrue(result.any { it is Resource.Error })
}
}

8.3 ViewModel 测试(TestDispatcher)

// features/profile/src/test/kotlin/ProfileViewModelTest.kt
@OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewModelTest {

private val getUserUseCase: GetUserUseCase = mockk()
private val updateProfileUseCase: UpdateProfileUseCase = mockk()
private val savedStateHandle = SavedStateHandle()
private val testDispatcher = UnconfinedTestDispatcher()

private lateinit var viewModel: ProfileViewModel

@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
savedStateHandle["userId"] = "test_user"
viewModel = ProfileViewModel(
getUserUseCase,
updateProfileUseCase,
savedStateHandle
)
}

@After
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun `initial state shows loading then user data`() = runTest {
val user = User("test_user", "Test User", null, null)
coEvery { getUserUseCase("test_user") } returns Result.success(user)

viewModel.uiState.test {
// 初始状态
val initialState = awaitItem()
assertTrue(initialState.isLoading)

// 加载完成后
val successState = awaitItem()
assertFalse(successState.isLoading)
assertEquals("Test User", successState.user?.displayName)
assertNull(successState.error)

cancelAndConsumeRemainingEvents()
}
}

@Test
fun `error shows snackbar event`() = runTest {
coEvery { getUserUseCase("test_user") } returns
Result.failure(RuntimeException("Server error"))

viewModel.events.test {
viewModel.loadProfile()

val event = awaitItem()
assertTrue(event is ProfileEvent.ShowSnackbar)
assertEquals("Server error", (event as ProfileEvent.ShowSnackbar).message)

cancelAndConsumeRemainingEvents()
}
}
}

九、完整项目结构总结

最终的项目模块结构:

EnterpriseApp/
├── app/ # 壳工程
│ └── src/main/kotlin/com/enterprise/app/
│ ├── MyApplication.kt # @HiltAndroidApp
│ └── MainActivity.kt # @AndroidEntryPoint

├── features/ # 业务特性模块
│ ├── login/ # :features:login
│ ├── home/ # :features:home
│ └── profile/ # :features:profile
│ └── src/main/kotlin/.../
│ ├── ProfileFragment.kt # View (Fragment)
│ ├── ProfileViewModel.kt # ViewModel
│ └── ProfileUiState.kt # UI State

├── domain/ # 领域层
│ ├── model/ # :domain:model
│ │ └── User.kt # 纯领域模型
│ └── usecase/ # :domain:usecase
│ ├── GetUserUseCase.kt
│ └── LoginUseCase.kt

├── data/ # 数据层
│ ├── api/ # :data:api
│ │ └── UserApi.kt # Retrofit 接口
│ ├── model/ # :data:model
│ │ ├── UserDto.kt # API DTO
│ │ └── UserEntity.kt # Room Entity
│ └── repository/ # :data:repository
│ ├── UserRepository.kt # 接口
│ └── UserRepositoryImpl.kt # 实现

├── core/ # 核心模块
│ ├── base/ # :core:base
│ │ └── BaseViewModel.kt # ViewModel 基类
│ ├── common/ # :core:common
│ │ └── Resource.kt # Resource sealed class
│ ├── database/ # :core:database
│ │ ├── AppDatabase.kt # Room Database
│ │ └── di/DatabaseModule.kt # Hilt Module
│ ├── network/ # :core:network
│ │ ├── AuthInterceptor.kt
│ │ └── di/NetworkModule.kt # Hilt Module
│ └── ui-common/ # :core:ui-common
│ ├── BaseFragment.kt
│ └── extensions/ViewExt.kt

├── buildSrc/ # Gradle 构建逻辑
│ └── src/main/kotlin/
│ ├── Dependencies.kt
│ └── Config.kt

├── build.gradle.kts # 根构建脚本
├── settings.gradle.kts # 模块注册
└── gradle.properties # Gradle 配置

十、总结

企业级 MVVM + Jetpack 架构的精髓:

  1. 模块化:按功能和职责拆分模块,上层依赖下层,下层不感知上层
  2. Artifactory:内部库的统一仓库,版本管理 + 构建缓存 + 安全分发
  3. Repository 模式:单一数据源,Room 缓存 + 网络刷新,Paging 3 + RemoteMediator 分页
  4. UseCase:单一职责的业务操作,可独立测试,可组合
  5. Hilt:编译时 DI,多级作用域(Singleton / ViewModelScoped / FragmentScoped)
  6. StateFlow + SharedFlow:StateFlow 持久状态,SharedFlow 一次性事件
  7. CI/CD:Jenkins / GitHub Actions 自动化构建、测试、发布
  8. 测试金字塔:70% 单元测试(Fake Data Source),20% 集成测试,10% UI 测试

参考资源

打赏
  • 微信
  • 支付宝

评论