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

如何避免Jetpack Compose重组时重复执行if else代码块?

问题描述

我开发了一个名为AddOrEditCustomerRoute的Jetpack Compose可组合项,支持新增或编辑客户。定义了可空类型的existingCustomer状态(新增时为null),以及isCustomerAdded、isCustomerUpdated两个状态,用来通过if-else分支展示增/改客户的成功或失败Toast提示。

现在遇到的问题:完成新增或更新操作后,页面会触发两次重组,导致if-else代码块执行两次,Toast重复显示,且navigateBack也会执行两次。

我尝试用LaunchedEffect处理副作用,但没生效,也不知道怎么优化当前的状态设计,作为Compose新手没思路。


Screen代码

@Composable
fun AddOrEditCustomerRoute(
    modifier: Modifier = Modifier,
    customerId: Long? = null,
    viewModel: CustomerViewModel = hiltViewModel(),
    navigateBack: () -> Unit
) {

    val context = LocalContext.current
    viewModel.getCustomer(customerId)
    val existingCustomer = viewModel.existingCustomer.collectAsStateWithLifecycle()

    val isCustomerAdded = viewModel.isCustomerAdded.collectAsStateWithLifecycle()
    val isCustomerUpdated = viewModel.isCustomerUpdated.collectAsStateWithLifecycle()
    
    if (isCustomerAdded.value == true) {
        Toast.makeText(context, "Customer Added", Toast.LENGTH_SHORT).show()
        navigateBack.invoke()
    } else if (isCustomerAdded.value == false) {
        Toast.makeText(context, "Failed to add customer! Try again", Toast.LENGTH_SHORT).show()
    }

    if (isCustomerUpdated.value == true) {
        Toast.makeText(context, "CustomerUpdated", Toast.LENGTH_SHORT).show()
        navigateBack.invoke()
    } else if (isCustomerUpdated.value == false) {
        Toast.makeText(context, "Failed to update customer, Try again", Toast.LENGTH_SHORT)
            .show()
    }

    AddOrEditCustomerScreens(
        modifier = modifier,
        customer = existingCustomer.value,
        addCustomer = { name, phone, email ->
            viewModel.addCustomer(name, phone, email)
        },
        updateCustomer = {
            viewModel.updateCustomer(it)
        },
        navigateBack = navigateBack
    )
}

@Composable
fun AddOrEditCustomerScreens(
    modifier: Modifier = Modifier,
    customer: Customer? = null,
    addCustomer: (String, String, String) -> Unit,
    updateCustomer: (customer: Customer) -> Unit,
    navigateBack: () -> Unit
) {
    val isEditing = customer != null
    val title = if (isEditing) stringResource(id = R.string.update_customer) else stringResource(
        id = R.string.add_customer
    )
    val customerName = remember(customer) {
        mutableStateOf(customer?.name ?: "")
    }
    val phoneNumber = remember(customer) {
        mutableStateOf(customer?.phone ?: "")
    }
    val email = remember(customer) {
        mutableStateOf(customer?.email ?: "")
    }

    Scaffold(
        modifier = modifier.fillMaxSize(),
        topBar = {
            DefaultAppBar(title = title, navigateBack = navigateBack, showSearch = false)
        }
    ) { paddingValue ->
        Column(
            modifier = Modifier
                .padding(paddingValue)
                .padding(vertical = 12.dp, horizontal = 16.dp)
        ) {
            OutlinedTextField(
                value = customerName.value,
                onValueChange = { customerName.value = it },
                label = {
                    TextH70(text = stringResource(id = R.string.customer_name))
                },
                modifier = Modifier
                    .fillMaxWidth()
            )

            OutlinedTextField(
                value = phoneNumber.value,
                onValueChange = {
                    if (it.length <= MAX_PHONE_LIMIT) {
                        phoneNumber.value = it
                    }
                },
                label = {
                    TextH70(text = stringResource(id = R.string.mobile_number_optional))
                },
                keyboardOptions = KeyboardOptions.Default.copy(
                    keyboardType = KeyboardType.Number
                ),
                modifier = Modifier
                    .fillMaxWidth()
            )

            OutlinedTextField(
                value = email.value,
                onValueChange = { email.value = it },
                label = {
                    TextH70(text = stringResource(id = R.string.email_id))
                },
                modifier = Modifier
                    .fillMaxWidth()
            )

            Button(
                enabled = (customerName.value.isNotEmpty() && phoneNumber.value.isNotEmpty()),
                onClick = {
                    if (isEditing)
                        updateCustomer(
                            customer!!.copy(
                                name = customerName.value,
                                phone = phoneNumber.value,
                                email = email.value
                            )
                        )
                    else
                        addCustomer(customerName.value, phoneNumber.value, email.value)
                },
                modifier = Modifier
                    .padding(top = 24.dp)
                    .align(Alignment.End),
                colors = ButtonDefaults.buttonColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer
                ),
                shape = RoundedCornerShape(8.dp)
            ) {
                Icon(imageVector = Icons.Filled.PersonAdd, null)
                Spacer(modifier = Modifier.width(4.dp))
                TextH40(
                    title,
                    color = Color.White
                )
            }
        }
    }
}

ViewModel代码

@HiltViewModel
class CustomerViewModel @Inject constructor(
    private val getCustomersUseCase: GetCustomersUseCase,
    private val searchCustomersUseCase: SearchCustomersUseCase,
    private val getCustomerUseCase: GetCustomerUseCase,
    private val addCustomerUseCase: AddCustomerUseCase,
    private val updateCustomerUseCase: UpdateCustomerUseCase
) : ViewModel() {
    
private val _existingCustomer = MutableStateFlow<Customer?>(null)
val existingCustomer = _existingCustomer
    
private val _customers = MutableStateFlow<PagingData<Customer>>(PagingData.empty())
val customers = _customers
    
private val _isCustomerAdded = MutableStateFlow<Boolean?>(null)
val isCustomerAdded = _isCustomerAdded
    
private val _isCustomerUpdated = MutableStateFlow<Boolean?>(null)
val isCustomerUpdated = _isCustomerUpdated
    
init {
    fetchInitCustomers()
}
    
private fun fetchInitCustomers() {
    viewModelScope.launch {
        getCustomersUseCase.perform()
        .map { pagingData ->
            pagingData.map {
                it.toCustomer()
            }
        }.cachedIn(viewModelScope)
        .collect {
            _customers.value = it
        }
    }
}
    
fun searchCustomers(query: String) {
    viewModelScope.launch {
        searchCustomersUseCase.perform(query)
            .map { pagingData -> pagingData.map { it.toCustomer() } }.cachedIn(viewModelScope)
        .collect {
            customers.value = it
        }
    }
}
    
fun getCustomer(id: Long?) {
    viewModelScope.launch {
        if (id != null) {
            _existingCustomer.value = getCustomerUseCase.perform(id)
        } else {
            _existingCustomer.value = null
        }
    }
}
    
fun addCustomer(name: String, phone: String, email: String) {
    viewModelScope.launch {
        val request = AddCustomerRequest(
            name = name,
            phone = phone,
            email = email,
            amountDue = null
        )
        _isCustomerAdded.value = addCustomerUseCase.perform(request)
    }
}
    
fun updateCustomer(customer: Customer) {
    viewModelScope.launch {
        val request = UpdateCustomerRequest(
            id = customer.id,
            name = customer.name,
            phone = customer.phone,
            email = customer.email,
            amountDue = customer.amountDue
        )
        _isCustomerUpdated.value = updateCustomerUseCase.perform(customer.id, request)
        }
    }
} 

尝试的LaunchedEffect代码

LaunchedEffect(isCustomerAdded) {
    if (isCustomerAdded.value == true) {
        Toast.makeText(context, "Customer Added", Toast.LENGTH_SHORT).show()
            navigateBack.invoke()
        } else if (isCustomerAdded.value == false) {
            Toast.makeText(context, "Failed to add customer! Try again", Toast.LENGTH_SHORT).show()
        }
}

LaunchedEffect(isCustomerUpdated) {
    if (isCustomerUpdated.value == true) {
        Toast.makeText(context, "CustomerUpdated", Toast.LENGTH_SHORT).show()
            navigateBack.invoke()
    } else if (isCustomerUpdated.value == false) {
        Toast.makeText(context, "Failed to update customer, Try again", Toast.LENGTH_SHORT)
        .show()
    }
}

解决方案

1. 核心问题分析

出现重复执行的原因有两个:

  • 直接在Composable的重组路径里执行Toast和导航操作,每次重组都会跑一遍这段逻辑,而Compose的重组可能因为多个状态变化(比如existingCustomer和isCustomerAdded同时更新)触发多次。
  • 你的LaunchedEffect没生效,是因为传入的key是isCustomerAdded这个State对象本身,而不是它的value,而且执行完后没有重置状态,导致下次重组时还会触发。

2. 修复步骤

第一步:重置状态,避免重复触发

在ViewModel里,每次执行完新增/更新后,要把_isCustomerAdded和_isCustomerUpdated重置回null,这样状态只会触发一次响应。修改ViewModel的addCustomer和updateCustomer方法:

fun addCustomer(name: String, phone: String, email: String) {
    viewModelScope.launch {
        val request = AddCustomerRequest(
            name = name,
            phone = phone,
            email = email,
            amountDue = null
        )
        _isCustomerAdded.value = addCustomerUseCase.perform(request)
        // 执行完后延迟重置状态,确保Compose能收到状态变化
        delay(100)
        _isCustomerAdded.value = null
    }
}

fun updateCustomer(customer: Customer) {
    viewModelScope.launch {
        val request = UpdateCustomerRequest(
            id = customer.id,
            name = customer.name,
            phone = customer.phone,
            email = customer.email,
            amountDue = customer.amountDue
        )
        _isCustomerUpdated.value = updateCustomerUseCase.perform(customer.id, request)
        delay(100)
        _isCustomerUpdated.value = null
    }
}

第二步:正确使用LaunchedEffect

把LaunchedEffect的key改成状态的value,并且只在状态不为null时执行逻辑。修改AddOrEditCustomerRoute里的代码:

// 替换原来的if-else代码块
LaunchedEffect(isCustomerAdded.value) {
    when(isCustomerAdded.value) {
        true -> {
            Toast.makeText(context, "客户新增成功", Toast.LENGTH_SHORT).show()
            navigateBack()
        }
        false -> {
            Toast.makeText(context, "新增客户失败,请重试", Toast.LENGTH_SHORT).show()
        }
        // null时不做处理
        else -> {}
    }
}

LaunchedEffect(isCustomerUpdated.value) {
    when(isCustomerUpdated.value) {
        true -> {
            Toast.makeText(context, "客户更新成功", Toast.LENGTH_SHORT).show()
            navigateBack()
        }
        false -> {
            Toast.makeText(context, "更新客户失败,请重试", Toast.LENGTH_SHORT).show()
        }
        else -> {}
    }
}

第三步:优化状态设计(可选但推荐)

可以把新增和更新的状态合并成一个密封类,这样逻辑更清晰,避免两个状态互相干扰:

// 定义状态密封类
sealed class CustomerActionResult {
    object AddSuccess : CustomerActionResult()
    object AddFailure : CustomerActionResult()
    object UpdateSuccess : CustomerActionResult()
    object UpdateFailure : CustomerActionResult()
    object Idle : CustomerActionResult()
}

// ViewModel里替换原来的两个StateFlow
private val _actionResult = MutableStateFlow<CustomerActionResult>(CustomerActionResult.Idle)
val actionResult = _actionResult

// 修改addCustomer方法
fun addCustomer(name: String, phone: String, email: String) {
    viewModelScope.launch {
        val request = AddCustomerRequest(
            name = name,
            phone = phone,
            email = email,
            amountDue = null
        )
        val success = addCustomerUseCase.perform(request)
        _actionResult.value = if (success) CustomerActionResult.AddSuccess else CustomerActionResult.AddFailure
        delay(100)
        _actionResult.value = CustomerActionResult.Idle
    }
}

// 修改updateCustomer方法
fun updateCustomer(customer: Customer) {
    viewModelScope.launch {
        val request = UpdateCustomerRequest(
            id = customer.id,
            name = customer.name,
            phone = customer.phone,
            email = customer.email,
            amountDue = customer.amountDue
        )
        val success = updateCustomerUseCase.perform(customer.id, request)
        _actionResult.value = if (success) CustomerActionResult.UpdateSuccess else CustomerActionResult.UpdateFailure
        delay(100)
        _actionResult.value = CustomerActionResult.Idle
    }
}

然后在Compose里监听这个合并后的状态:

val actionResult = viewModel.actionResult.collectAsStateWithLifecycle()

LaunchedEffect(actionResult.value) {
    when(actionResult.value) {
        CustomerActionResult.AddSuccess -> {
            Toast.makeText(context, "客户新增成功", Toast.LENGTH_SHORT).show()
            navigateBack()
        }
        CustomerActionResult.AddFailure -> {
            Toast.makeText(context, "新增客户失败,请重试", Toast.LENGTH_SHORT).show()
        }
        CustomerActionResult.UpdateSuccess -> {
            Toast.makeText(context, "客户更新成功", Toast.LENGTH_SHORT).show()
            navigateBack()
        }
        CustomerActionResult.UpdateFailure -> {
            Toast.makeText(context, "更新客户失败,请重试", Toast.LENGTH_SHORT).show()
        }
        // Idle状态不处理
        CustomerActionResult.Idle -> {}
    }
}

3. 额外优化点

  • 把viewModel.getCustomer(customerId)放到LaunchedEffect里,避免每次重组都调用:
LaunchedEffect(customerId) {
    viewModel.getCustomer(customerId)
}

因为customerId是参数,只有当它变化时才需要重新获取客户信息,直接放在Composable根节点会每次重组都执行,浪费资源。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 18:35:55