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

如何在Kotlin Compose中通过数据类实现Composable代码复用?

Kotlin Compose 通用Composable实现,消除数据类差异导致的代码冗余

你有两个逻辑完全一致的Composable函数,仅接收的ArrayList数据类型不同,可通过以下步骤实现通用版本,彻底消除代码冗余:

步骤1:定义通用数据接口

由于HealthCategoryDetailItems和HealthTagDetailItems的核心字段完全一致,先提取一个接口,包含Composable中需要用到的所有属性:

interface HealthDetailItem {
    val articleTypeId: Int?
    val date: String?
    val image: String?
    val title: String?
}

步骤2:修改数据类实现接口

让两个数据类都实现上述接口,统一属性的访问方式:

修改HealthCategoryDetailItems

data class HealthCategoryDetailItems(
    @SerializedName("article_type_id") override val articleTypeId: Int? = null,
    @SerializedName("date") override val date: String? = null,
    @SerializedName("image") override val image: String? = null,
    @SerializedName("id") val id: Int? = null,
    @SerializedName("title") override val title: String? = null
) : HealthDetailItem

修改HealthTagDetailItems

data class HealthTagDetailItems(
    @SerializedName("article_type_id") override val articleTypeId: Int? = null,
    @SerializedName("date") override val date: String? = null,
    @SerializedName("image") override val image: String? = null,
    @SerializedName("id") val id: Int? = null,
    @SerializedName("title") override val title: String? = null
) : HealthDetailItem

步骤3:编写泛型通用Composable

创建一个泛型Composable,约束泛型必须实现HealthDetailItem接口,这样就能兼容两种数据类型:

@Composable
fun <T : HealthDetailItem> GenericHealthItemsCategory(
    items: ArrayList<T>,
    onClick: (Int?, String?) -> Unit // 抽离点击逻辑,让外部页面自行实现
) {
    val boxQueryWidth = ScreenQuery(
        limits = arrayListOf(
            ScreenQueryLimit(
                max = 321,
                value = 135,
            ),
            ScreenQueryLimit(
                min = 321,
                max = 361,
                value = 160,
            )
        ),
        direction = ScreenDirection.HORIZONTAL,
        defaultValue = 170
    ).value().dp

    val itemsList = items.chunked(2)

    itemsList.forEach { listItem ->
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(bottom = 15.dp),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            listItem.forEach { item ->
                HealthItem(
                    width = boxQueryWidth,
                    imageSize = boxQueryWidth,
                    layoutPadding = 0.dp,
                    itemId = item.articleTypeId,
                    itemName = item.title ?: "",
                    image = item.image ?: "",
                    onClick = onClick,
                    detailText = item.date,
                    detailTextColor = MaterialTheme.colors.grayColor,
                    shouldShowAdditionalImage = item.articleTypeId == 0,
                    additionalImg = painterResource(id = R.drawable.ic_home_content_recipe),
                    content = {}
                )
            }
        }
    }
}

步骤4:替换原有Composable使用通用版本

原有的两个Composable可以直接简化为调用通用版本,或者直接在页面中使用GenericHealthItemsCategory:

替换ItemsCategory

@Composable
fun ItemsCategory(
    items: ArrayList<HealthCategoryDetailItems>,
    onClick: (Int?, String?) -> Unit
) {
    GenericHealthItemsCategory(items, onClick)
}

替换TagDetailItem

@Composable
fun TagDetailItem(
    items: ArrayList<HealthTagDetailItems>,
    onClick: (Int?, String?) -> Unit
) {
    GenericHealthItemsCategory(items, onClick)
}

可选:ViewModel层抽象优化

如果两个ViewModel的逻辑也高度相似,可以进一步抽象基类ViewModel,减少重复代码:

// 定义Response基接口
interface BaseResponse {
    val isError: Int?
}

// 抽象基类ViewModel
abstract class BaseHealthDetailViewModel<ResponseModel : BaseResponse> : ViewModel() {
    protected abstract val useCase: UseCase<*, Resource<ResponseModel>>
    private val _state = mutableStateOf(DetailScreenState())
    val state: State<DetailScreenState> = _state

    data class DetailScreenState(
        val isLoading: Boolean = false,
        val error: Boolean = false,
        val response: ResponseModel? = null
    )

    protected fun handleResult(result: Resource<ResponseModel>) {
        when (result) {
            is Resource.Success -> {
                _state.update {
                    it.copy(
                        isLoading = false,
                        response = result.data,
                        error = result.data?.isError == 1
                    )
                }
            }
            is Resource.Loading -> _state.update { it.copy(isLoading = true) }
            is Resource.Error -> _state.update { it.copy(isLoading = false, error = true) }
        }
    }
}

// 让两个ResponseModel实现BaseResponse
data class HealthCategoryDetailResponseModel(
    @SerializedName("is_error") override val isError: Int? = null,
    @SerializedName("data") var data: HealthCategoryDetailData? = HealthCategoryDetailData(),
    @SerializedName("description") var description: String? = null
) : BaseResponse

data class HealthContentDetailTagResponseModel(
    @SerializedName("is_error") override val isError: Int? = null,
    @SerializedName("data") var data: Data? = Data(),
    @SerializedName("description") var description: String? = null
) : BaseResponse

// 子类ViewModel实现
class HealthCategoryDetailViewModel(
    private val useCase: HealthCategoryDetailUseCase
) : BaseHealthDetailViewModel<HealthCategoryDetailResponseModel>() {
    override val useCase = useCase

    fun loadData(request: HealthCategoryDetailRequestModel) {
        viewModelScope.launch {
            useCase.invoke(request).collect { handleResult(it) }
        }
    }
}

class HealthTagDetailViewModel(
    private val useCase: TagDetailUseCase
) : BaseHealthDetailViewModel<HealthContentDetailTagResponseModel>() {
    override val useCase = useCase

    fun loadData(request: HealthContentDetailTagRequestModel) {
        viewModelScope.launch {
            useCase.invoke(request).collect { handleResult(it) }
        }
    }
}

内容的提问来源于stack exchange,提问作者NewPartizal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 02:59:56