CodeNewbie Community 🌱

Cover image for Handling Text in Jetpack compose with Android
Tristan
Tristan

Posted on

Handling Text in Jetpack compose with Android

Introduction

  • New Android tutorial every Monday and Thursday, so buckle up and lets learn some Android.

Getting started

  • While handling text in Jetpack compose is not a new topic, I want to introduce everyone to a more systematic approach to handling text input. Hopefully making things a little less chaotic. Our approach is going to have 4 steps to it:

1) State

2) Style

3) Errors

4) Hoisting the state

1) State

  • In this section we can start by creating a basic TextField that allows us to capture user input:
@Composable
fun SimpleInput(viewModel:TestViewModel = viewModel()){
    var text by remember { mutableStateOf("") }

    TextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )


}
Enter fullscreen mode Exit fullscreen mode
  • Lets break down the code block above and start with, var text by remember { mutableStateOf("") }.

var : indicates this property is mutable(it can be changed)

by : declares a delegate property, which means the getter and setter for this property will be defined by what comes after the keyword.

remember : Keeps the value consistent across recompositions.

mutableStateOf() : returns a MutableState, which is a value we change and Compose will automatically observe changes made to it.

  • HERE is a link to a blog post doing a more indepth dive on the topics mentioned above

TextField : is the composable that will be shown to the user and be used to capture user input. We have passed it 3 parameter: 1)value : the input text to be shown in the TextField, 2)onValueChange : The callback that is triggered when the input service updates the text. An updated text comes as a parameter of the callback. If we translate that into non tech jargon, we get, onValueChange is a parameter that takes a function which will be called each time the user changes the input inside the TextField. The function takes in a single parameter, which is the text being typed.

  • If the syntax of { text = it } confuses you, you need to read up on lambda functions of Kotlin. For now just know that the {} brackets count as a function and it represents the single parameter of the words being typed

2) Style

  • Now we can go ahead and add any types of styles we like, which is really up the individual programmer. So yours might be different than mine.
TextField(
        //state
        value = text,
        onValueChange = { text = it },
        //style
        singleLine = true,
        label = { Text("Label") },
        modifier = Modifier.padding(start = 0.dp,40.dp,0.dp,0.dp),
        textStyle = TextStyle(fontSize = 26.sp),
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Text
        ),

    )
Enter fullscreen mode Exit fullscreen mode
  • The above styles are all fairly self explanitory, however, I would recommend that you play around with the different types of
    keyboardOptions and notice how the keyboards can change depending on the keyboardType

  • At this point we will have a working TextField, so feel free to run your application and make sure that everything work correctly and move on to the next step.

3) Errors

  • This step is us defining how the error is going to look. and we can do so with, isError : which is used to indicate when everything should turn red and trailingIcon which we use to show the actual error icon. Implementing those two parameters into our TextField looks something like this:
TextField(
        //state
        value = text,
        onValueChange = { text = it },
        //style
        singleLine = true,
        label = { Text("Label") },
        modifier = Modifier.padding(start = 0.dp,40.dp,0.dp,0.dp),
        textStyle = TextStyle(fontSize = 26.sp),
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Text
        ),
        //errors
        isError = true,
        trailingIcon = {
            Icon(Icons.Filled.Error, "error has occurred")
        }

    )

Enter fullscreen mode Exit fullscreen mode
  • For right now us setting isError = true is fine but in the next section we will use a conditional to determine its value.

4) Hoisting the state

  • In a real world application we want to store the state in a ViewModel and have it updated through a function we define. So create a new file and call it TestViewModel(you can call it whatever you want):
class TestViewModel:ViewModel() {

    private val _uiState = mutableStateOf("")
    val uiState: State<String> = _uiState


 fun updateMethod(text:String){
        _uiState.value = text
    }

Enter fullscreen mode Exit fullscreen mode
  • By us creating and using this ViewModel class we are abiding by the recommended app architecture which is found HERE. We are currently implementing the UI layer for our app.

  • If you are unfamiliar with this architecture or just ViewModels in general than you are probably confused by the two variables, _uiState and uiState.

_uiState : a mutable state that will be used only by methods inside of the ViewModel class

uiState : is a non mutable state that will be exposed and used by our UI

  • All that sounds awesome, but why use two 2? It boils down to separation of concerns we want our UI to only focus on displaying UI and move all the state handling logic to the ViewModel, which will make for easier testing and cleaner code.

  • Now lets update our TextField to use our ViewModel:

@Composable
fun SimpleInput(viewModel:TestViewModel = viewModel()){

    TextField(
        //state
        value = viewModel.uiState.value,
        onValueChange = { viewModel.updateMethod(it) }

//everything else stays the same
}

Enter fullscreen mode Exit fullscreen mode
  • The only thing that might seems a little weird is the default value we are providing with viewModel:TestViewModel = viewModel() which just creates an instance of our ViewModel class

  • Now we can move on to error validation and creating a more realistic UIState

Error validation and UI state

  • Our current state is very simple and in a really project things get a lot more complicated, so lets create a state that would reflect that. Inside that same file as out ViewModel class, at the top of the file create your UI state class:
data class TestUiState(
    val text: String = "",
    val errorMessage: String? = null,

)
Enter fullscreen mode Exit fullscreen mode
  • If you are unfamiliar with Kotlin's data class please read the documentation, HERE. If you don't want to just know that data classes are used to hold data(UI data in our case) and they provide us with a lot of extra methods automatically.
  • Keen eyed viewers have probably notice that do to us using the val keywords our TestUiState class is immutable, why? Well, as the documentations states: The key benefit of this is that immutable objects provide guarantees regarding the state of the application at an instant in time. This frees up the UI to focus on a single role: to read the state and update its UI elements accordingly. As a result, you should never modify the UI state in the UI directly unless the UI itself is the sole source of its data. So basically immutability gives us separation of concerns and gives our UI a guaranteed state.

Final update to our ViewModel class:

class TestViewModel:ViewModel() {

    private val _uiState = mutableStateOf(TestUiState())
    val uiState: State<TestUiState> = _uiState

        fun updateMethod(text:String){
        _uiState.value = _uiState.value.copy(text = text)
    }


    fun validateText(text:String){
        if(text.isNullOrBlank()){
            _uiState.value = _uiState.value.copy(errorMessage= "Can not be blank")
        }else{
            _uiState.value = _uiState.value.copy(errorMessage= null)
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
  • With the code above I want to point out the use of copy() method. This is one of the method provided to us automatically when using a data class. when copy() is used it will create a shallow copy(clones all the values in the object but does not create an entire new object) of our TestUiState and only changes the parameters specified

Final update to our TextField:

@Composable
fun SimpleInput(viewModel:TestViewModel = viewModel()){
    val textState = viewModel.uiState.value.text
    val errorMessage = viewModel.uiState.value.errorMessage

    Column() {
        TextField(
            //state
            value = textState,
            onValueChange = { viewModel.updateMethod(it) },
            //style
            singleLine = true,
            label = { Text("Label") },
            modifier = Modifier.padding(start = 0.dp,40.dp,0.dp,0.dp),
            textStyle = TextStyle(fontSize = 26.sp),
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Text
            ),
            //errors
            isError = errorMessage !=null,
            trailingIcon = {
                if(errorMessage != null){
                    Icon(Icons.Filled.Error, "error has occurred")
                }
            }

        )
        Button(onClick = { viewModel.validateText(textState) }) {
            Text(text = "Validate")

        }
    }

}
Enter fullscreen mode Exit fullscreen mode
  • Besides us using a Column for styling and a Button to run the validateText() method not too much has changed. But I also want to point out the conditionals we added to the isError and trailingIcon:
//errors
            isError = errorMessage !=null,
            trailingIcon = {
                if(errorMessage != null){
                    Icon(Icons.Filled.Error, "error has occurred")
                }
            }
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Top comments (0)