一、企业级 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" ) include(":features:login" ) include(":features:home" ) include(":features:profile" ) include(":core:common" ) include(":core:network" ) include(":core:database" ) include(":core:ui-common" ) include(":core:base" ) include(":data:api" ) include(":data:repository" ) include(":data:model" ) 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 { 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} " 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} " const val navigationFragment = "androidx.navigation:navigation-fragment-ktx:${Versions.navigation} " const val navigationUi = "androidx.navigation:navigation-ui-ktx:${Versions.navigation} " 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} " const val hiltAndroid = "com.google.dagger:hilt-android:${Versions.hilt} " const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt} " 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} " const val pagingRuntime = "androidx.paging:paging-runtime-ktx:${Versions.paging} " const val pagingCompose = "androidx.paging:paging-compose:${Versions.paging} " 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} " 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 :
plugins { id("com.android.library" ) id("kotlin-android" ) id("maven-publish" ) } 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 消费内部库 repositories { maven { url = uri("https://artifactory.enterprise.com/artifactory/libs-release" ) credentials { username = System.getenv("ARTIFACTORY_USER" ) password = System.getenv("ARTIFACTORY_PASSWORD" ) } } 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 更新策略 :
dependencyResolutionManagement { repositories { } }
三、数据层(Data Layer)设计 3.1 Repository 模式 —— 单一数据源原则 Repository 是数据层的核心抽象,遵循单一数据源原则(Single Source of Truth, SSOT) :
class UserRepositoryImpl ( private val api: UserApi, private val dao: UserDao, private val prefs: UserPreferences ) : UserRepository { override fun getUser (userId: String ) : Flow<Resource<User>> = flow { emit(Resource.Loading) val cached = dao.getUser(userId) if (cached != null ) { emit(Resource.Success(cached.toDomain())) } try { val response = api.getUser(userId) if (response.isSuccessful && response.body() != null ) { val remote = response.body()!!.toEntity() dao.upsertUser(remote) } 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 作为本地缓存 @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 () } @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 )
RemoteMediator(Paging 3)结合了网络请求和本地缓存的分页:
@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 代表一个单一的业务操作:
class LoginUseCase ( private val authRepository: AuthRepository, private val analyticsTracker: AnalyticsTracker, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { suspend operator fun invoke ( username: String , password: String ) : Result<User> = withContext(dispatcher) { if (username.length < 3 ) { return @withContext Result.failure(ValidationException("用户名至少3个字符" )) } if (password.length < 6 ) { return @withContext Result.failure(ValidationException("密码至少6个字符" )) } val result = authRepository.login(username, password) 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) │ └─────────────────┘
模型映射扩展函数 :
fun UserDto.toDomain () : User = User( id = this .id, username = this .username, avatarUrl = this .avatar_url, bio = this .bio ) fun User.toEntity () : UserEntity = UserEntity( id = this .id, username = this .username, avatarUrl = this .avatarUrl, bio = this .bio, updatedAt = System.currentTimeMillis() ) 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 @HiltViewModel class ProfileViewModel @Inject constructor ( private val getUserUseCase: GetUserUseCase, private val updateProfileUseCase: UpdateProfileUseCase, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val userId: String = savedStateHandle.get <String>("userId" ) ?: savedStateHandle["myUserId" ] ?: "" private val _uiState = MutableStateFlow(ProfileUiState()) val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow() 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 ?: "加载失败" )) } } } fun onUserIdChanged (newUserId: String ) { savedStateHandle["userId" ] = newUserId loadProfile() } } data class ProfileUiState ( val user: UserUiState? = null , val isLoading: Boolean = false , val error: String? = null ) sealed class ProfileEvent { data class ShowSnackbar (val message: String) : ProfileEvent() data class NavigateTo (val destination: Int ) : ProfileEvent() }
5.2 DataBinding vs ViewBinding ViewBinding (推荐用于简单场景):
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 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 基础配置 @HiltAndroidApp class MyApplication : Application () { override fun onCreate () { super .onCreate() } } @AndroidEntryPoint class MainActivity : AppCompatActivity () { @Inject lateinit var analyticsTracker: AnalyticsTracker override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) } }
6.2 模块声明 @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) } } @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() } } @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 的注入 @HiltViewModel class ProfileViewModel @Inject constructor ( private val getUserUseCase: GetUserUseCase, private val updateProfileUseCase: UpdateProfileUseCase, private val savedStateHandle: SavedStateHandle ) : ViewModel() { } @AndroidEntryPoint class ProfileFragment : Fragment () { private val viewModel: ProfileViewModel by viewModels() }
6.4 作用域注解
作用域
注解
生命周期
应用
@Singleton
Application 存活期间
ViewModel
@ViewModelScoped
ViewModel 存活期间
Activity
@ActivityScoped
Activity 存活期间
Fragment
@FragmentScoped
Fragment 存活期间
自定义
@ServiceScoped
自定义组件存活期间
七、CI/CD 集成 7.1 Jenkins Pipeline 配置 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' } } } }
7.2 GitHub Actions 配置 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) 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) } assertTrue(result.any { it is Resource.Loading }) assertTrue(result.any { it is Resource.Success }) 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) @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 架构的精髓:
模块化 :按功能和职责拆分模块,上层依赖下层,下层不感知上层
Artifactory :内部库的统一仓库,版本管理 + 构建缓存 + 安全分发
Repository 模式 :单一数据源,Room 缓存 + 网络刷新,Paging 3 + RemoteMediator 分页
UseCase :单一职责的业务操作,可独立测试,可组合
Hilt :编译时 DI,多级作用域(Singleton / ViewModelScoped / FragmentScoped)
StateFlow + SharedFlow :StateFlow 持久状态,SharedFlow 一次性事件
CI/CD :Jenkins / GitHub Actions 自动化构建、测试、发布
测试金字塔 :70% 单元测试(Fake Data Source),20% 集成测试,10% UI 测试
参考资源