0

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?

Stelios Papamichail
  • 955
  • 2
  • 19
  • 57
  • My suggestion is that `getAccountDetails` only updates mutable variables of a single class object, in this case live data won't work. Make sure that in this line `accountDetails.postValue(response.data!!)` you're not posting the same object. If this didn't help, it's hard to say with a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – Phil Dukhov Apr 08 '22 at 13:13
  • @PylypDukhov I added the InputField composable that I forgot to include when I first posted the question. I'm pretty sure that the issue lies somewhere inside it since it's a hot mess (hate it with a burning passion tbh) – Stelios Papamichail Apr 12 '22 at 11:22
  • Check out [this](https://stackoverflow.com/a/71629294/3585796) and [this](https://stackoverflow.com/a/70074376/3585796). – Phil Dukhov Apr 12 '22 at 11:38

0 Answers0