Jetpack Compose+Retrofit实现HTTP API服务的最佳实践(购物车场景)
解决方案
你遇到的核心问题有两个:一是带构造参数的ViewModel在Jetpack Compose中无法正确实例化,二是扫描条形码后的商品数据获取逻辑未理顺,以下是完整的正确实现方案:
一、修正API层注解错误
你的ProductService中barcode是URL查询参数(格式为?barcode=xxx),需替换@Path为@Query:
interface ProductService { @GET("/product") suspend fun getProduct(@Query("barcode") barcode: String): Product }
二、实现ViewModel自定义工厂(解决构造参数注入)
Jetpack Compose中不能直接通过viewModel()获取带参数的ViewModel,必须自定义ViewModelProvider.Factory来创建实例:
class CartViewModelFactory( private val productRepository: ProductRepository ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(CartViewModel::class.java)) { return CartViewModel(productRepository) as T } throw IllegalArgumentException("未知的ViewModel类") } }
三、重构CartViewModel逻辑
整合扫描、API请求、购物车操作的完整流程,保留原有的数量递增逻辑,同时添加加载状态避免重复请求:
class CartViewModel( private val productRepository: ProductRepository ) : ViewModel() { // 用mutableStateListOf确保Compose能感知列表变化 private val _cart = mutableStateListOf<Product>() val cart: List<Product> get() = _cart // 加载状态,用于UI显示加载提示 private val _isLoading = mutableStateOf(false) val isLoading: Boolean get() = _isLoading.value /** * 扫描条形码后调用此方法,从API获取商品并添加到购物车 */ fun scanAndAddProduct(barcode: String) { if (_isLoading.value) return // 正在加载时拒绝重复请求 viewModelScope.launch { _isLoading.value = true try { val product = productRepository.getProduct(barcode) // 复用原有添加逻辑:商品已存在则增加数量,否则新增 val existingProduct = _cart.find { it.barcode == product.barcode } if (existingProduct != null) { existingProduct.quantity += product.quantity println("已增加${product.name}数量至${existingProduct.quantity}") } else { _cart.add(product) println("已添加${product.name}到购物车") } } catch (e: Exception) { // 此处可添加错误处理逻辑,比如给UI发送错误提示 println("获取商品失败:${e.message}") } finally { _isLoading.value = false } } } // 保留原有移除商品方法 fun remove(product: Product) { _cart.remove(product) println("已移除${product.name}") } }
四、在Compose中正确获取ViewModel
先构建Retrofit、Service、Repository实例,再通过自定义工厂获取ViewModel:
@Composable fun CartScreen() { // 构建Retrofit与Service实例(实际项目中可放入依赖注入容器) val retrofit = Retrofit.Builder() .baseUrl("你的API基础地址") .addConverterFactory(GsonConverterFactory.create()) .build() val productService = retrofit.create(ProductService::class.java) val productRepository = ProductRepository(productService) // 通过自定义工厂获取CartViewModel val viewModel: CartViewModel = viewModel( factory = CartViewModelFactory(productRepository) ) // UI实现 Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { // 加载提示 if (viewModel.isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) } // 购物车列表 LazyColumn(modifier = Modifier.weight(1f)) { items(viewModel.cart) { product -> CartItem( product = product, onRemoveClick = { viewModel.remove(product) } ) } } // 模拟扫码按钮(实际项目替换为真实扫码逻辑) Button( onClick = { viewModel.scanAndAddProduct("1") }, modifier = Modifier.fillMaxWidth().padding(top = 16.dp), enabled = !viewModel.isLoading ) { Text("扫描条形码") } } } @Composable fun CartItem(product: Product, onRemoveClick: () -> Unit) { Row( modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { // 商品图片显示 product.image?.let { Image( painter = it, contentDescription = product.name, modifier = Modifier.size(64.dp).clip(RoundedCornerShape(8.dp)) ) } ?: Box( modifier = Modifier.size(64.dp).background(Color.Gray.copy(alpha = 0.3f)).clip(RoundedCornerShape(8.dp)) ) Column(modifier = Modifier.weight(1f).padding(start = 16.dp)) { Text(text = product.name, fontSize = 16.sp) Text(text = "¥${product.price}", color = Color.Green, fontSize = 14.sp) Text(text = "数量:${product.quantity}", color = Color.Gray, fontSize = 12.sp) } IconButton(onClick = onRemoveClick) { Icon(Icons.Default.Delete, contentDescription = "移除商品") } } }
可选优化:用Hilt简化依赖注入
若项目已使用Hilt,可省略自定义工厂,通过注解实现自动注入:
// NetworkModule:提供Retrofit与Service实例 @Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides fun provideRetrofit(): Retrofit { return Retrofit.Builder() .baseUrl("你的API基础地址") .addConverterFactory(GsonConverterFactory.create()) .build() } @Provides fun provideProductService(retrofit: Retrofit): ProductService { return retrofit.create(ProductService::class.java) } } // ProductRepository:通过@Inject实现构造注入 class ProductRepository @Inject constructor( private val productService: ProductService ) { suspend fun getProduct(barcode: String): Product { return productService.getProduct(barcode) } } // CartViewModel:添加@HiltViewModel注解实现自动注入 @HiltViewModel class CartViewModel @Inject constructor( private val productRepository: ProductRepository ) : ViewModel() { // 逻辑与之前一致 } // Compose中获取ViewModel @Composable @HiltViewModel fun CartScreen() { val viewModel: CartViewModel = hiltViewModel() // UI逻辑与之前一致 }
内容的提问来源于stack exchange,提问作者VIEWVIEWVIEW
相关产品推荐
相关产品推荐

