我将为您展示一个完整的 MVVM + Clean Architecture 的 Android 项目代码结构。这是一个简单的用户列表应用示例。
## 项目结构
```
app/
├── data/
│ ├── local/
│ │ ├── dao/
│ │ ├── database/
│ │ └── entity/
│ ├── remote/
│ │ ├── api/
│ │ └── dto/
│ ├── repository/
│ └── mapper/
├── domain/
│ ├── model/
│ ├── repository/
│ └── usecase/
├── presentation/
│ ├── ui/
│ │ ├── users/
│ │ └── userdetail/
│ ├── viewmodel/
│ └── utils/
└── di/
```
## 1. 依赖配置 (build.gradle.kts)
```kotlin
// app/build.gradle.kts
dependencies {
// Core
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.0")
// Compose
implementation("androidx.compose.ui:ui:1.5.4")
implementation("androidx.compose.ui:ui-tooling-preview:1.5.4")
implementation("androidx.compose.material3:material3:1.1.2")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
// Hilt
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// Room
implementation("androidx.room:room-runtime:2.6.0")
implementation("androidx.room:room-ktx:2.6.0")
kapt("androidx.room:room-compiler:2.6.0")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.5")
}
```
## 2. Domain Layer (核心业务逻辑)
### Domain Model
```kotlin
// domain/model/User.kt
data class User(
val id: Int,
val name: String,
val email: String,
val avatar: String,
val createdAt: String
)
```
### Repository Interface
```kotlin
// domain/repository/UserRepository.kt
interface UserRepository {
suspend fun getUsers(): Result<List<User>>
suspend fun getUserById(id: Int): Result<User>
suspend fun refreshUsers(): Result<Unit>
}
```
### Use Cases
```kotlin
// domain/usecase/GetUsersUseCase.kt
class GetUsersUseCase @Inject constructor(
private val userRepository: UserRepository
) {
suspend operator fun invoke(): Result<List<User>> {
return userRepository.getUsers()
}
}
// domain/usecase/GetUserDetailUseCase.kt
class GetUserDetailUseCase @Inject constructor(
private val userRepository: UserRepository
) {
suspend operator fun invoke(userId: Int): Result<User> {
return userRepository.getUserById(userId)
}
}
// domain/usecase/RefreshUsersUseCase.kt
class RefreshUsersUseCase @Inject constructor(
private val userRepository: UserRepository
) {
suspend operator fun invoke(): Result<Unit> {
return userRepository.refreshUsers()
}
}
```
## 3. Data Layer (数据处理)
### Remote Data Source
```kotlin
// data/remote/api/ApiService.kt
interface ApiService {
@GET("users")
suspend fun getUsers(): List<UserDto>
@GET("users/{id}")
suspend fun getUserById(@Path("id") id: Int): UserDto
}
// data/remote/dto/UserDto.kt
data class UserDto(
@SerializedName("id")
val id: Int,
@SerializedName("name")
val name: String,
@SerializedName("email")
val email: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("created_at")
val createdAt: String
)
```
### Local Data Source
```kotlin
// data/local/entity/UserEntity.kt
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey
val id: Int,
val name: String,
val email: String,
val avatar: String,
val createdAt: String
)
// data/local/dao/UserDao.kt
@Dao
interface UserDao {
@Query("SELECT * FROM users")
suspend fun getAllUsers(): List<UserEntity>
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: Int): UserEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<UserEntity>)
@Query("DELETE FROM users")
suspend fun deleteAllUsers()
}
// data/local/database/AppDatabase.kt
@Database(
entities = [UserEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
```
### Mapper
```kotlin
// data/mapper/UserMapper.kt
fun UserDto.toEntity(): UserEntity {
return UserEntity(
id = id,
name = name,
email = email,
avatar = avatar,
createdAt = createdAt
)
}
fun UserEntity.toDomain(): User {
return User(
id = id,
name = name,
email = email,
avatar = avatar,
createdAt = createdAt
)
}
fun UserDto.toDomain(): User {
return User(
id = id,
name = name,
email = email,
avatar = avatar,
createdAt = createdAt
)
}
```
### Repository Implementation
```kotlin
// data/repository/UserRepositoryImpl.kt
class UserRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserRepository {
override suspend fun getUsers(): Result<List<User>> = withContext(dispatcher) {
try {
// 首先尝试从本地数据库获取
val localUsers = userDao.getAllUsers()
if (localUsers.isNotEmpty()) {
Result.success(localUsers.map { it.toDomain() })
} else {
// 如果本地没有数据,从网络获取
val remoteUsers = apiService.getUsers()
userDao.insertUsers(remoteUsers.map { it.toEntity() })
Result.success(remoteUsers.map { it.toDomain() })
}
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun getUserById(id: Int): Result<User> = withContext(dispatcher) {
try {
// 先从本地获取
val localUser = userDao.getUserById(id)
if (localUser != null) {
Result.success(localUser.toDomain())
} else {
// 本地没有则从网络获取
val remoteUser = apiService.getUserById(id)
userDao.insertUsers(listOf(remoteUser.toEntity()))
Result.success(remoteUser.toDomain())
}
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun refreshUsers(): Result<Unit> = withContext(dispatcher) {
try {
val remoteUsers = apiService.getUsers()
userDao.deleteAllUsers()
userDao.insertUsers(remoteUsers.map { it.toEntity() })
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
```
## 4. Presentation Layer (UI层)
### ViewModels
```kotlin
// presentation/viewmodel/UsersViewModel.kt
@HiltViewModel
class UsersViewModel @Inject constructor(
private val getUsersUseCase: GetUsersUseCase,
private val refreshUsersUseCase: RefreshUsersUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(UsersUiState())
val uiState: StateFlow<UsersUiState> = _uiState.asStateFlow()
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
getUsersUseCase().fold(
onSuccess = { users ->
_uiState.update {
it.copy(
users = users,
isLoading = false,
error = null
)
}
},
onFailure = { exception ->
_uiState.update {
it.copy(
isLoading = false,
error = exception.message
)
}
}
)
}
}
fun refreshUsers() {
viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
refreshUsersUseCase().fold(
onSuccess = {
loadUsers()
_uiState.update { it.copy(isRefreshing = false) }
},
onFailure = { exception ->
_uiState.update {
it.copy(
isRefreshing = false,
error = exception.message
)
}
}
)
}
}
}
data class UsersUiState(
val users: List<User> = emptyList(),
val isLoading: Boolean = false,
val isRefreshing: Boolean = false,
val error: String? = null
)
// presentation/viewmodel/UserDetailViewModel.kt
@HiltViewModel
class UserDetailViewModel @Inject constructor(
private val getUserDetailUseCase: GetUserDetailUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId: Int = savedStateHandle.get<Int>("userId") ?: 0
private val _uiState = MutableStateFlow(UserDetailUiState())
val uiState: StateFlow<UserDetailUiState> = _uiState.asStateFlow()
init {
loadUserDetail()
}
private fun loadUserDetail() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
getUserDetailUseCase(userId).fold(
onSuccess = { user ->
_uiState.update {
it.copy(
user = user,
isLoading = false,
error = null
)
}
},
onFailure = { exception ->
_uiState.update {
it.copy(
isLoading = false,
error = exception.message
)
}
}
)
}
}
}
data class UserDetailUiState(
val user: User? = null,
val isLoading: Boolean = false,
val error: String? = null
)
```
### UI Screens
```kotlin
// presentation/ui/users/UsersScreen.kt
@Composable
fun UsersScreen(
onNavigateToDetail: (Int) -> Unit,
viewModel: UsersViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Users") }
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when {
uiState.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
uiState.error != null -> {
ErrorMessage(
message = uiState.error,
onRetry = { viewModel.loadUsers() },
modifier = Modifier.align(Alignment.Center)
)
}
else -> {
UsersList(
users = uiState.users,
onUserClick = onNavigateToDetail,
onRefresh = { viewModel.refreshUsers() },
isRefreshing = uiState.isRefreshing
)
}
}
}
}
}
@Composable
private fun UsersList(
users: List<User>,
onUserClick: (Int) -> Unit,
onRefresh: () -> Unit,
isRefreshing: Boolean
) {
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
onRefresh = onRefresh
)
Box(
modifier = Modifier.pullRefresh(pullRefreshState)
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(users) { user ->
UserItem(
user = user,
onClick = { onUserClick(user.id) }
)
}
}
PullRefreshIndicator(
refreshing = isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
@Composable
private fun UserItem(
user: User,
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = user.avatar,
contentDescription = "User avatar",
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = user.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
// presentation/ui/userdetail/UserDetailScreen.kt
@Composable
fun UserDetailScreen(
onNavigateBack: () -> Unit,
viewModel: UserDetailViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("User Detail") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when {
uiState.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
uiState.error != null -> {
ErrorMessage(
message = uiState.error,
onRetry = { /* Retry logic */ },
modifier = Modifier.align(Alignment.Center)
)
}
uiState.user != null -> {
UserDetailContent(user = uiState.user)
}
}
}
}
}
@Composable
private fun UserDetailContent(user: User) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = user.avatar,
contentDescription = "User avatar",
modifier = Modifier
.size(120.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = user.name,
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = user.email,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Account Information",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("User ID")
Text(user.id.toString())
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Created At")
Text(user.createdAt)
}
}
}
}
}
@Composable
private fun ErrorMessage(
message: String,
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onRetry) {
Text("Retry")
}
}
}
```
## 5. Dependency Injection (依赖注入)
```kotlin
// di/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// di/DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
@Singleton
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}
// di/RepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(
userRepositoryImpl: UserRepositoryImpl
): UserRepository
}
// di/DispatcherModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@MainDispatcher
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
// Qualifier annotations
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MainDispatcher
```
## 6. Navigation
```kotlin
// presentation/navigation/NavGraph.kt
@Composable
fun NavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "users"
) {
composable("users") {
UsersScreen(
onNavigateToDetail = { userId ->
navController.navigate("user_detail/$userId")
}
)
}
composable(
route = "user_detail/{userId}",
arguments = listOf(
navArgument("userId") {
type = NavType.IntType
}
)
) {
UserDetailScreen(
onNavigateBack = {
navController.popBackStack()
}
)
}
}
}
```
## 7. Application Class
```kotlin
// MyApplication.kt
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化其他库
}
}
```
## 8. MainActivity
```kotlin
// MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
val navController = rememberNavController()
NavGraph(navController = navController)
}
}
}
}
```
## 9. 测试示例
```kotlin
// domain/usecase/GetUsersUseCaseTest.kt
class GetUsersUseCaseTest {
@Mock
private lateinit var userRepository: UserRepository
private lateinit var getUsersUseCase: GetUsersUseCase
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
getUsersUseCase = GetUsersUseCase(userRepository)
}
@Test
fun `invoke should return users from repository`() = runTest {
// Given
val expectedUsers = listOf(
User(1, "John", "john@example.com", "", "2023-01-01"),
User(2, "Jane", "jane@example.com", "", "2023-01-02")
)
whenever(userRepository.getUsers()).thenReturn(Result.success(expectedUsers))
// When
val result = getUsersUseCase()
// Then
assertTrue(result.isSuccess)
assertEquals(expectedUsers, result.getOrNull())
}
}
// presentation/viewmodel/UsersViewModelTest.kt
class UsersViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Mock
private lateinit var getUsersUseCase: GetUsersUseCase
@Mock
private lateinit var refreshUsersUseCase: RefreshUsersUseCase
private lateinit var viewModel: UsersViewModel
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
}
@Test
fun `loadUsers should update ui state with users`() = runTest {
// Given
val users = listOf(
User(1, "John", "john@example.com", "", "2023-01-01")
)
whenever(getUsersUseCase()).thenReturn(Result.success(users))
// When
viewModel = UsersViewModel(getUsersUseCase, refreshUsersUseCase)
// Then
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertEquals(users, uiState.users)
assertNull(uiState.error)
}
}
```
这个完整的项目展示了:
1. **Clean Architecture 分层**:Domain、Data、Presentation 层分离
2. **MVVM 模式**:ViewModel 管理 UI 状态
3. **依赖注入**:使用 Hilt 进行依赖管理
4. **Repository 模式**:数据源抽象
5. **Use Cases**:业务逻辑封装
6. **Coroutines + Flow**:异步操作和响应式编程
7. **Jetpack Compose**:现代 UI 开发
8. **单一数据源**:结合本地和远程数据
9. **错误处理**:统一的错误处理机制
10. **测试**:单元测试示例
这种架构的优点:
- 高度可测试性
- 关注点分离
- 易于维护和扩展
- 符合 SOLID 原则
- 清晰的依赖关系