https://kotlinlang.org logo
#compose
Title
# compose
a

Anand Verma

02/03/2022, 5:03 AM
Hi, I’m new into compose. And I’ve been struggling with managing a lot of states for a UI form. Is there any smarter way through which I can create form & manage states smartly.
c

Colton Idle

02/03/2022, 5:44 AM
Do you have any code to show? I'm building an app that is similar to google forms, and so there are a lot of input fields, and so there is just inherently a lot of state to save.
m

mattinger

02/03/2022, 4:29 PM
@Anand Verma I would recommend going through the compose codelabs on managing state.
1
But as far as suggestions, i’d recommend creating some sort of UIState object that represents the current state. Let’s say you have a form with a text field and a checkbox:
Copy code
data class UIState(text: String, checked: Boolean)
You’d create a viewModel to hold this state:
Copy code
class MyViewModel: ViewModel() {
     private val _state = mutableStateOf(UIState("", false))
     val state: State<UIState> get() = _state

     var text: String
        get() { return _state.value.text }
        set(value) { _state.value = _state.value.copy(text=value) }

     var checked: Boolean
        get() { return _state.value.checked }
        set(value) { _state.value = _state.value.copy(checked=value) }
}
Then you can do something like this:
Copy code
MyScreen(
   text = viewModel.text,
   onTextChanged = { viewModel.text = it },
   checked = viewModel.checked,
   onCheckChanged = { viewModel.checked = it }
)
But the general idea is that 1. You create an immutable UIState object that has the current state of the entire form 2. You create a view model with a mutable state that holds the current UIState 3. As you mutate values in the UI, you make a copy of the state with the changes and push it into the view model
a

Anand Verma

02/04/2022, 4:40 AM
Hi @mattinger I’ve worked like that.
Copy code
class AddEventLeadUi {
    var leadName by mutableStateOf("")
    var leadNameError by mutableStateOf("")
    var emailAddress by mutableStateOf("")
    var emailAddressError by mutableStateOf("")
    var phoneNumber by mutableStateOf("")
    var phoneNumberError by mutableStateOf("")
    var program by mutableStateOf<Program?>(null)
    var programError by mutableStateOf("")
    var paymentProgram by mutableStateOf<StringDropdown?>(null)
    var paymentProgramError by mutableStateOf("")

    fun isValid(): Boolean {
        var isValid = true
        leadNameError = if (leadName.isValidFullName())
            ""
        else {
            isValid = false
            "Enter full name"
        }
        emailAddressError = if (emailAddress.isValidEmail())
            ""
        else {
            isValid = false
            "Enter valid email address"
        }
        phoneNumberError = if (phoneNumber.isValidMobileNumber())
            ""
        else {
            isValid = false
            "Enter valid mobile number"
        }
        programError = if (program == null) {
            isValid = false
            "Required"
        } else
            ""
        paymentProgramError = if (paymentProgram == null && program?.askFee == true) {
            isValid = false
            "Required"
        } else
            ""
        return isValid
    }
}
Copy code
data class AddEventLeadState(
    val request: Async<Int> = Uninitialized,
) : MvRxState

class AddEventLeadViewModel @AssistedInject constructor(
    @Assisted state: AddEventLeadState,
    private val leadRepository: LeadRepository
) : MvRxViewModel<AddEventLeadState>(state) {

    @AssistedFactory
    interface Factory : AssistedViewModelFactory<AddEventLeadViewModel, AddEventLeadState> {
        override fun create(state: AddEventLeadState): AddEventLeadViewModel
    }

    companion object :
        MavericksViewModelFactory<AddEventLeadViewModel, AddEventLeadState> by hiltMavericksViewModelFactory()

    val leadUi = AddEventLeadUi()

    fun createLead(eventSlug: String) =
        withState { state ->
            if (state.request is Loading)
                return@withState
            if (leadUi.isValid().not())
                return@withState
            suspend {
                leadRepository.createLead(
                    LeadEntity(
                        name = leadUi.leadName,
                        email = leadUi.emailAddress,
                        program = leadUi.program?.id.toString(),
                        mobile = leadUi.phoneNumber,
                        campaign = null,
                        medium = null,
                        paymentProgram = leadUi.paymentProgram?.key,
                        source = eventSlug,
                        mainLeadSource = "Events",
                        type = "events"
                    )
                )
            }.execute(<http://Dispatchers.IO|Dispatchers.IO>) {
                copy(request = it)
            }
        }
}
Copy code
@Composable
fun AddEventLeadScreen(
    event: EventData,
    navAction: EventNavAction,
    modifier: Modifier = Modifier,
    viewModel: AddEventLeadViewModel = mavericksViewModel()
) {
    val request = viewModel.collectAsState {
        it.request
    }.value
    if (request is Fail)
        showToast(request.error.message or stringResource(R.string.something_went_wrong))
    if (request is Success) {
        showToast(stringResource(R.string.lead_added_success))
        navAction.popUp()
    }
    Column(
        modifier = modifier.padding(20.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp)
    ) {
        Text(event.eventName, style = MaterialTheme.typography.h5)

        Text(stringResource(R.string.add_new_lead), style = MaterialTheme.typography.body1)

        CrmInputFieldText(
            "Lead Name",
            "Name",
            viewModel.leadUi.leadName,
            isRequired = true,
            imeAction = ImeAction.Next,
            singleLine = true,
            maxLength = 50,
            error = viewModel.leadUi.leadNameError
        ) {
            viewModel.leadUi.leadName = it
        }

        CrmInputFieldText(
            "Lead Email Address",
            "Email Address",
            viewModel.leadUi.emailAddress,
            isRequired = true,
            imeAction = ImeAction.Next,
            singleLine = true,
            maxLength = 50,
            error = viewModel.leadUi.emailAddressError
        ) {
            viewModel.leadUi.emailAddress = it
        }

        CrmInputFieldNumber(
            "Lead Phone Number",
            "Phone Name",
            viewModel.leadUi.phoneNumber,
            isRequired = true,
            imeAction = ImeAction.Next,
            maxLength = 10,
            error = viewModel.leadUi.phoneNumberError
        ) {
            viewModel.leadUi.phoneNumber = it
        }

        CrmInputSinglePick(
            "Program Name",
            "Select Program",
            event.programs,
            selectedItem = viewModel.leadUi.program,
            isRequired = true,
            error = viewModel.leadUi.programError
        ) {
            viewModel.leadUi.program = it
        }

        AnimatedVisibility(visible = viewModel.leadUi.program?.askFee == true) {
            CrmInputSinglePick(
                "Payment Program",
                "Select Payment Program",
                listOf(StringDropdown("PAP"), StringDropdown("Regular")),
                selectedItem = viewModel.leadUi.paymentProgram,
                isRequired = true,
                error = viewModel.leadUi.programError
            ) {
                viewModel.leadUi.paymentProgram = it
            }
        }

        CrmFilledLoadingButtons(
            stringResource(R.string.save_lead),
            request is Loading,
            modifier = Modifier.fillMaxWidth()
        ) {
            viewModel.createLead(event.eventSlug)
        }

        AnimatedVisibility(request !is Loading) {
            CrmTextButton(
                stringResource(R.string.cancel),
                modifier = Modifier.fillMaxWidth(),
                onCLick = navAction.popUp
            )
        }
    }
}
is there any drawback on this approach ?