A colleague of mine has written the following code:
Viewmodel:
class CallMeBackViewModel(
private val ewayControllerUseCase: EwayControllerUseCase,
private val accountControllerUseCase: AccountControllerUseCase
) : ViewModel() {
val accountDetails = MutableLiveData<AccountDetailsResponse>()
val contactForm = CallMeBackForm()
init {
val accountId = App.instance.appContext.getPreferenceThroughKeystore(PreferenceKeys.ACCOUNT_ID.key)
contactForm.timeframe = "09:00 - 11:00"
if (accountId != "null") {
getAccountDetails(accountId)
}
}
private fun getAccountDetails(accountId: String) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = accountControllerUseCase.getAccountDetails(accountId)) {
is ResponseResult.Success -> {
accountDetails.postValue(response.data!!)
fillContactFormByAccount(response.data!!)
}
is ResponseResult.Error -> {
Log.d("GET_ACCOUNT_DETAILS_ERROR", "Network Call Failed With Exception: " + response.exception.message)
}
}
}
}
private fun fillContactFormByAccount(accountDetailsResponse: AccountDetailsResponse) {
contactForm.firstName = accountDetailsResponse.firstName!!
contactForm.lastName = accountDetailsResponse.lastName!!
contactForm.phoneNumber = accountDetailsResponse.phone1!!
contactForm.accountId = accountDetailsResponse.accountId!!
}
@ExperimentalFoundationApi
@ExperimentalMaterialApi
fun submitCallbackForm(activity: MainActivity) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = ewayControllerUseCase.submitCallbackForm(contactForm)) {
is ResponseResult.Success -> {
onSuccessfulSubmission(activity)
}
is ResponseResult.Error -> {
Log.d("CALLBACK_FORM_SUBMISSION_ERROR", "Network Call Failed With Exception: " + response.exception.message)
}
}
}
}
@ExperimentalFoundationApi
@ExperimentalMaterialApi
private suspend fun onSuccessfulSubmission(activity: MainActivity) {
withContext(Dispatchers.Main) {
activity.onBackPressed()
}
}
}
He basically fetches some form data and when received, updates the accountDetails live data with them. Now in the composable screen he observes this field as a state like so :
Screen:
@ExperimentalFoundationApi
@ExperimentalMaterialApi
@Composable
fun ContactCallMeBack() {
val viewModel: CallMeBackViewModel = remember { getKoin().get() }
val accountDetails by viewModel.accountDetails.observeAsState(null)
val canProceed = remember { mutableStateOf(false) }
val textChangedToggle = remember { mutableStateOf(false) }
val activity = LocalContext.current as MainActivity
LaunchedEffect(textChangedToggle.value) {
canProceed.value = canProceed(viewModel.contactForm)
}
Column(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
TopBarPadding(true)
Spacer(modifier = Modifier.padding(24.dp))
if (accountDetails != null) {
Form(accountDetails, viewModel, textChangedToggle)
}
else {
Form(accountDetails, viewModel, textChangedToggle)
}
Spacer(modifier = Modifier.padding(42.fixedDp()))
MyButton(text = "Καλέστε με",
buttonType = if (canProceed.value) {
MyButtonType.PRIMARY
} else {
MyButtonType.DISABLED
},
onClick = { viewModel.submitCallbackForm(activity) })
Spacer(modifier = Modifier.padding(49.fixedDp()))
}
}
@Composable
private fun Form(
accountDetails: AccountDetailsResponse?,
viewModel: CallMeBackViewModel,
textChangedToggle: MutableState<Boolean>
) {
Column(modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(13.dp))
.background(Color.White)
.padding(start = 16.dp, end = 16.dp, top = 22.dp, bottom = 24.dp)
) {
InputField(label = "Κωδικός Συνδρομητή",
initialValue = accountDetails?.accountId ?: "",
onTextChangeCallback = {
viewModel.contactForm.accountId = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Όνομα",
initialValue = accountDetails?.firstName ?: "",
onTextChangeCallback = {
viewModel.contactForm.firstName = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Επώνυμο",
initialValue = accountDetails?.lastName ?: "",
onTextChangeCallback = {
viewModel.contactForm.lastName = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Τηλέφωνο",
initialValue = accountDetails?.phone1 ?: "",
onTextChangeCallback = {
viewModel.contactForm.phoneNumber = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
MyDropdown(label = "Διαθεσιμότητα", list = mapOf(
1 to "09:00 - 11:00",
2 to "11:00 - 13:00",
3 to "13:00 - 15:00",
4 to "15:00 - 17:00"
),
defaultSelected = 1,
disableSelectionReset = true,
onSelectionChangeCallbackValue = { viewModel.contactForm.timeframe = it }
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Μήνυμα", singleLine = false,
onTextChangeCallback = {
viewModel.contactForm.reason = it
triggerCheck(textChangedToggle)
}
)
}
}
private fun canProceed(form: CallMeBackForm): Boolean {
return (form.firstName != "" &&
form.lastName != "" &&
form.phoneNumber != "" &&
form.reason != "")
}
private fun triggerCheck(state: MutableState<Boolean>) {
state.value = !state.value
}
InputField composable:
@Composable
fun InputField(
label: String,
enabled: Boolean = true,
isPassword: Boolean = false,
infoButton: (() -> Unit)? = null,
placeholder: String = LocalContext.current.getString(R.string.input_placeholder),
canReveal: Boolean = false,
phonePrefix: Boolean = false,
singleLine: Boolean = true,
optional: Boolean = false,
initialValue: String = "",
onTextChangeCallback: (String) -> Unit = { },
numbersOnly: Boolean = false
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 10.fixedDp())
) {
Text(
text = label,
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 10.fixedDp())
)
if (optional) {
Text(
text = LocalContext.current.getString(R.string.optional),
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 10.fixedDp()),
color = colorResource(id = R.color.input_field_optional_text)
)
}
if (infoButton != null) {
InfoButton(
buttonColor = colorResource(id = R.color.Orange_100),
onClick = infoButton
)
}
}
Spacer(modifier = Modifier.padding(10.fixedDp()))
val textFieldValue = remember { mutableStateOf(TextFieldValue(initialValue)) }
val textFieldPasswordMode = remember { mutableStateOf(isPassword) }
val modifier = if (singleLine) {
Modifier
.clip(RoundedCornerShape(6.dp))
.fillMaxWidth()
.background(if (enabled) Color.White else colorResource(id = R.color.Grey_10))
} else {
Modifier
.clip(RoundedCornerShape(6.dp))
.fillMaxWidth()
.requiredHeight(100.dp)
.background(if (enabled) Color.White else colorResource(id = R.color.Grey_10))
}
var isTextFieldFocused by remember { mutableStateOf(false) }
ShadowWrapper(
cardElevation = 1.dp,
border = BorderStroke(
1.dp,
if (isTextFieldFocused && enabled) colorResource(id = R.color.Orange_100)
else colorResource(id = R.color.my_card_border)
),
shadowShapeRadius = 6.dp,
shadowElevation = 1.dp
) {
TextField(
value = textFieldValue.value,
onValueChange = {
textFieldValue.value = it
onTextChangeCallback.invoke(it.text)
},
modifier = modifier
.onFocusChanged { isTextFieldFocused = it.hasFocus },
keyboardOptions = if (numbersOnly) {
KeyboardOptions(keyboardType = KeyboardType.Phone)
} else {
KeyboardOptions(keyboardType = KeyboardType.Text)
},
enabled = enabled,
colors = TextFieldDefaults.textFieldColors(
disabledTextColor = colorResource(id = R.color.Grey_60),
focusedIndicatorColor = Color.Transparent,
backgroundColor = if (enabled) colorResource(id = R.color.white) else colorResource(
id = R.color.Grey_10
),
textColor = colorResource(id = R.color.black),
unfocusedIndicatorColor = colorResource(id = R.color.white),
disabledIndicatorColor = colorResource(id = R.color.white),
cursorColor = colorResource(id = R.color.black)
),
singleLine = singleLine,
placeholder = {
Text(
text = placeholder,
fontStyle = FontStyle.Italic,
fontSize = 16.sp,
lineHeight = 20.sp,
overflow = TextOverflow.Visible
)
},
readOnly = !enabled,
visualTransformation = if (textFieldPasswordMode.value) PasswordVisualTransformation() else VisualTransformation.None,
leadingIcon = if (phonePrefix) {
{
PhonePrefix(R.drawable.greek_flag, "+30")
}
} else null,
trailingIcon = if (canReveal) {
{
RevealingEye(state = textFieldPasswordMode)
}
} else null,
shape = RoundedCornerShape(6.dp),
)
}
}
}
I noticed the seemingly useless if statement around the form, but it seems to be the only thing forcing the recomposition of the Form composable. I thought that when a new accountDetails instance is posted inside an observable state, that this would trigger any composables that depend on it to recompose. What am I missing?