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