Search code examples
androidxmlkotlinandroid-jetpack-composestate-management

How to avoid the if else code block to run when recomposition happens?


There is a AddOrUpdateCustomerRoute composable , where user can add a new customer or update a existing customer, i have a nullable existingcustomer state( nullable in case of a new customer), and there are two more state isCustomerUpdated or isCustomerAdded, and i have used if else to show a toast message for both success and failure on adding and updating the customer, the issue is when the customer is updated, or even if new customer is added, the recomposition happens twice and the if else code block triggers twice and show the toast message twice and navigateback twice on successful updation or addition

here the code for 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
                )
            }
        }
    }
}

here' is the viewModel code

@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)
        }
    }
} 

and i think the extraction of state can be done better here, but i just can't wrap my head around how to do that, new to compose and this thing would have been a piece of cake in xml, but doing things in compose is new to me, as i am trying to learn it

I tried using side effect like LauncedEffect, but it didn't help at all, the code didn't even run so couldn't find much about what can i do here

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()
    }
}

Solution

  • Try adding .value to LaunchedEffect key.

    LaunchedEffect(isCustomerAdded.value)
    

    Or keep LaunchedEffect but switch to property delegation by using by.

    val isCustomerAdded by viewModel.isCustomerAdded.collectAsStateWithLifecycle()