Jetpack Compose 入门系列(三):MVVM + 协程 实现网络请求列表
本文将通过一个完整的小 Demo,演示如何使用 Jetpack Compose + MVVM + Kotlin 协程 调用后台 API 并展示列表数据。
一、技术栈一览
| 模块 | 方案 |
|---|---|
| UI | Jetpack Compose |
| 架构 | MVVM |
| 异步 | Kotlin Coroutines + Flow |
| 网络 | Retrofit + OkHttp |
| 依赖注入 | Hilt(可选,本文使用手动注入保持精简) |
| 数据源 | JSONPlaceholder 免费 API |
二、项目结构
bash
app/src/main/java/com/example/composedemo/
├── data/
│ ├── model/
│ │ └── User.kt # 数据实体
│ ├── remote/
│ │ └── ApiService.kt # Retrofit 接口定义
│ └── repository/
│ └── UserRepository.kt # 数据仓库
├── ui/
│ ├── screen/
│ │ └── UserListScreen.kt # Compose 界面
│ └── theme/
│ └── Theme.kt # 主题配置
├── viewmodel/
│ └── UserListViewModel.kt # ViewModel 层
└── MainActivity.kt # 入口 Activity
三、依赖配置
gradle/libs.versions.toml 依赖版本指定和声明:
ini
[versions]
agp = "9.2.1"
coreKtx = "1.10.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
kotlin = "2.2.10"
composeBom = "2026.02.01"
# Network
okhttp = "5.3.2"
retrofit = "3.0.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
# ViewModel
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
# Network - OkHttp
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
# Network - Retrofit
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
app/build.gradle.kts 核心依赖:
kotlin
dependencies {
// BOM(Bill of Materials):统一管理 Compose 版本,避免版本冲突 implementation(platform(libs.androidx.compose.bom))
// UI 基础
implementation(libs.androidx.compose.ui)
// UI 图形工具
implementation(libs.androidx.compose.ui.graphics)
// UI 预览(只在 debug 模式下需要) implementation(libs.androidx.compose.ui.tooling.preview)
// Material 3 设计系统 implementation(libs.androidx.compose.material3)
// Activity Compose(setContent 扩展) implementation(libs.androidx.activity.compose)
// 调试工具
debugImplementation(libs.androidx.compose.ui.tooling)
// ViewModel
implementation(libs.androidx.lifecycle.viewmodel.compose)
// Network
// ========== 网络请求库 ==========
// OkHttp:HTTP 客户端,Retrofit 的底层实现
implementation(libs.okhttp)
// Retrofit:类型安全的 HTTP 客户端,用于定义和调用 REST API
implementation(libs.retrofit)
// Retrofit Gson 转换器:将 JSON 响应自动转换为 Kotlin 数据类
implementation(libs.retrofit.converter.gson)
}
四、Data 层实现
4.1 数据实体
kotlin
data class User(
val id: Int,
val name: String,
val email: String,
val company: Company
)
data class Company(
val name: String,
val catchPhrase: String
)
使用
data class自动生成equals()、hashCode()、toString(),保持简洁。
4.2 Retrofit API 接口
kotlin
interface ApiService {
@GET("/users")
suspend fun getUsers(): List<User>
companion object {
private const val BASE_URL = "https://jsonplaceholder.typicode.com"
fun create(): ApiService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder().build())
.build()
.create(ApiService::class.java)
}
}
}
关键规范:
- 网络请求方法使用
suspend修饰,配合协程异步执行Retrofit.Builder封装在companion object中,避免外部直接构造- URL 常量使用
const val而非普通val
4.3 Repository 层
kotlin
class UserRepository(
private val apiService: ApiService = ApiService.create()
) {
suspend fun getUsers(): Result<List<User>> = runCatching {
apiService.getUsers()
}
}
规范要点:
- Repository 返回
Result<T>类型,统一错误处理- 使用
runCatching自动捕获异常,无需手动 try-catch- API 服务通过默认参数注入,方便单测替换
五、ViewModel 层实现
kotlin
class UserListViewModel(
private val repository: UserRepository = UserRepository()
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_uiState.value = UiState.Loading
repository.getUsers()
.onSuccess { users ->
_uiState.value = UiState.Success(users)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message.orEmpty())
}
}
}
}
sealed interface UiState {
data object Loading : UiState
data class Success(val users: List<User>) : UiState
data class Error(val message: String) : UiState
}
规范要点:
_uiState为私有可变,对外暴露只读StateFlow(封装可见性)viewModelScope.launch自动跟随 ViewModel 生命周期UiState使用sealed interface(Kotlin 1.5+),替代旧的sealed class- 初始状态在
init块中触发,保证页面进入即加载
六、Compose UI 层实现
6.1 列表页面
kotlin
@Composable
fun UserListScreen(
viewModel: UserListViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is UiState.Loading -> LoadingView()
is UiState.Error -> ErrorView((uiState as UiState.Error).message) {
viewModel.loadUsers()
}
is UiState.Success -> UserList((uiState as UiState.Success).users)
}
}
规范要点:
collectAsStateWithLifecycle替代旧的collectAsState,生命周期更安全(AndroidX Lifecycle 2.6+)- 使用
when表达式替代if-else链,sealed interface编译器会检查是否穷举
6.2 列表项
kotlin
@Composable
private fun UserList(users: List<User>) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(users, key = { it.id }) { user ->
UserCard(user)
}
}
}
@Composable
private fun UserCard(user: User) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = user.name, style = MaterialTheme.typography.titleMedium)
Text(text = user.email, style = MaterialTheme.typography.bodyMedium)
Text(
text = user.company.name,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
规范要点:
items(key = { it.id })提供稳定 key,避免列表滚动时不必要的重组- 内部
@Composable使用private限定,限制作用域- 使用 Material 3 的
CardDefaults.cardElevation和MaterialTheme.typography新 API
6.3 加载 & 错误视图
kotlin
@Composable
private fun LoadingView() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
@Composable
private fun ErrorView(
message: String,
onRetry: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = message, color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onRetry) {
Text("重试")
}
}
}
七、入口 Activity
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface {
UserListScreen()
}
}
}
}
}
八、数据流向总结
kotlin
Activity → setContent → Compose UI
│
▼
┌── ViewModel (StateFlow<UiState>) ──┐
│ │ │
│ viewModelScope.launch │
│ │ │
│ ▼ │
│ Repository (suspend fun) │
│ │ │
│ ▼ │
│ Retrofit API (suspend) │
└────────────────────────────────────┘
│
▼
StateFlow 发射新状态
│
▼
collectAsStateWithLifecycle 收集
│
▼
when(state) → 渲染 Loading/Error/Success
九、核心规范速查
| 规则 | 做法 |
|---|---|
| 异步方法 | suspend + viewModelScope.launch |
| 状态管理 | MutableStateFlow 私有,暴露 StateFlow |
| 错误处理 | runCatching 返回 Result<T> |
| 状态密封 | sealed interface 替代 sealed class |
| 列表 key | items(key = { it.id }) 必须提供 |
| 生命周期 | 使用 collectAsStateWithLifecycle |
| 可见性 | 内部 Composable 加 private |
| 常量定义 | const val 用于基本类型常量 |
源码获取:本文所有代码可直接复制到新建的 Compose 项目中运行,无需额外配置。