You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 05:45:43