Skip to content

Instantly share code, notes, and snippets.

@weverb2
Created September 12, 2021 20:00
Show Gist options
  • Save weverb2/98b83f38df41caaa797f765202b5ae7a to your computer and use it in GitHub Desktop.
Save weverb2/98b83f38df41caaa797f765202b5ae7a to your computer and use it in GitHub Desktop.

Basic Form Validation Revisited: Jetpack Compose

It has been nearly 2 years since I posted my tutorial on Basic Form Validation with LiveData and Data Binding. In that time, Android has changed a lot. The major change being the release of Jetpack Compose. Jetpack Compose is a completely new way of writing Android UI Code that, in my opinion, makes writing Android fun again. The last time I felt I was having this much fun writing Android code was when I first started writing Kotlin code instead of Java.

I figured it would be a good idea to come back to one of my old posts and update it for this new world. The problems we as developers face have not changed, but the tools we have are quite a bit better this time around. So let's take a look at how we can ditch Data Binding and XML and still achieve a testable form in Jetpack Compose.

Composing the View

Previously the layout I used to demonstrate this form was a simple LinearLayout with an EditText and a Button. Let's go ahead and make that with Jetpack Compose:

@Composable
fun MyCoolForm() {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        OutlinedTextField(
            value = "",
            label = { Text("Email") },
            onValueChange = {},
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.size(4.dp))
        Button(enabled = false, onClick = {}) {
            Text(text = "Save")
        }
    }
}

When we run this above code, our view looks alright, but the TextField is unresponsive! This is due to the design of Jetpack Compose. There are plenty of articles and tutorials out there about why this is the case, so I won't spend the time to explain the why here, just how we get our TextField to change when the user types something.

Wiring State Updates

TextField needs a value property and an onValueChanged property, we could declare this local to the MyCoolForm composable function, but it is recommended to "hoist state" up to a higher level. This means our form doesn't really care about what it's values are or what happens when they change. This is a good thing for reusability as this form component we created could be used in different places in our application.

We can lift our state up just like TextField by declaring an emailText parameter and an onEmailChanged parameter. Our composable function now looks like this:

@Composable
fun MyCoolForm(emailText: String = "", onEmailChanged: (String) -> Unit = {}) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        OutlinedTextField(
            value = emailText,
            label = { Text("Email") },
            onValueChange = onEmailChanged,
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.size(4.dp))
        Button(enabled = false, onClick = {}) {
            Text(text = "Save")
        }
    }
}

(the default values are added as a convenience for Previews)

Now we need something that can handle those parameters and luckily we've got the perfect thing.

Dusting off the ViewModel

Since our form is trying to accomplish the same logic as the form from my post in 2019, we can reuse that ViewModel. Here is the ViewModel:

@HiltViewModel
class FormValidationViewModel @Inject constructor(): ViewModel() {

    val emailAddress = MutableLiveData<String>("")

    val valid = MediatorLiveData<Boolean>().apply {
        addSource(emailAddress) {
            val valid = isFormValid(it)
            Log.d(it, valid.toString())
            value = valid
        }
    }

    fun isFormValid(emailAddress: String): Boolean {
        return Patterns.EMAIL_ADDRESS.matcher(emailAddress).matches()
    }
}

(I've added Hilt here, mainly for my convenience, but it is really easy to use in a small test app like this. Read the setup guide here: Hilt Quick Start Guide)

In our Activity (or Fragment, or even another Composable) we can now handle the Form's state updates with the LiveData defined in the ViewModel

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel: FormValidationViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val emailState by viewModel.emailAddress.observeAsState() // 1
            MaterialTheme { 
                MyCoolForm(
                    emailText = emailState ?: "", // 2
                    onEmailChanged = { viewModel.emailAddress.postValue(it) } // 3
                )
            }
        }
    }
}

In the Activity, we are doing the following:

  1. Observing the LiveData emailAddress as state, this will be scoped to the Activity's Lifecycle in this case.
  2. Passing in the emailState value to the emailText field. This will set the text shown in the TextField
  3. Deciding what happens when we get an update to the TextField. Right now it is just posting the new value to the LiveData directly, but you could have the ViewModel preform more logic on it here as well.

We're not quite done yet. The save button is not being enabled when a value is entered. We need to observe a new piece of state, and update the enabled property on that button in the Form's composable. Luckily, that business logic lives in the ViewModel, so we just need to wire it up.

@Composable
fun MyCoolForm(
    emailText: String = "",
    onEmailChanged: (String) -> Unit = {},
    isFormValid: Boolean = false // added state
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        OutlinedTextField(
            value = emailText,
            label = { Text("Email") },
            onValueChange = onEmailChanged,
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.size(4.dp))
        Button(enabled = isFormValid, onClick = {}) { // utilize new state here
            Text(text = "Save")
        }
    }
}

In the MyCoolForm composable function you can see that the change is very similar to how the TextField is being updated. We just pass in whether the form is or is not valid, based on some logic our view does not need to care about, and just pass that information along to the enabled property of the button. Now let's head to the activity and finish wiring it up.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel: FormValidationViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val emailState by viewModel.emailAddress.observeAsState()
            val isFormValid by viewModel.valid.observeAsState() // Added new state to observe
            MaterialTheme {
                MyCoolForm(
                    emailText = emailState ?: "",
                    onEmailChanged = { viewModel.emailAddress.postValue(it) },
                    isFormValid = isFormValid ?: false // Passing in the value to the composable
                )
            }
        }
    }
}

Our form is now feature complete when comparing to the form from 2 years ago. This time we were able to write the whole feature entirely in Kotlin.

Wrapping Up

We were able to convert a simple screen that used DataBinding and LiveData into one that uses Jetpack Compose and the same LiveData and ViewModel. Hopefully this shows that using the tools in the Jetpack Libraries you are able to move to using Jetpack Compose without causing much trouble in your business logic. Jetpack Compose is the biggest step forward in writing Android Apps since the introduction of Kotlin and I am personally happy any time I get to write some UI in Compose. I hope you've learned something new from my post and please let me know in the comments if you have any questions or corrections.

Thanks for reading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment