Jetpack Compose 入门系列(三):MVVM + 协程 实现网络请求列表

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.cardElevationMaterialTheme.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 项目中运行,无需额外配置。

相关推荐
LCG元13 小时前
MySQL慢查询分析与索引调优:从故障诊断到性能翻倍的进阶之路
android·前端·mysql
技术钱13 小时前
字符分割器组件的使用
android·python
小a彤13 小时前
atvoss:Vector 算子子程序模板库,让 Ascend C 开发效率提升 5 倍
android·c语言·数据库
oh_my_god13 小时前
Android 修改ntp网络校时服务器
android
jzlhll12313 小时前
android kotlin Flow:distinctUntilChangedBy + stateIn 的坑
android·开发语言·kotlin
美狐美颜sdk13 小时前
直播APP开发如何实现美颜功能?低成本美颜SDK方案推荐
android·人工智能·ios·第三方美颜sdk·视频美颜sdk
程序员JerrySUN14 小时前
Jetson边缘嵌入式实战课程第五讲:Jetson Secure Boot - 安全启动
android·linux·服务器·人工智能·安全·unity·游戏引擎
zh_xuan14 小时前
Android JNI 动态注册:获取系统内存页大小
android·cmake·jni·ndk·动态注册·内存页大小
CocoaKier15 小时前
X未提前通知,突然停用twitter授权登录域名,大量X三方登录异常!
android·ios