Mastering states in Kotlin and Jetpack

This tutoral offers people who are new to Kotlin and Jetpack the foundational knowledge on states and state hoisting using a hotel tip timer example.

· 3 min read
Wilson Ochieng

Wilson Ochieng

Android Expert developing mobile applications with 4+ Years of Experience.

topics

What is the state?

A state can be defined as any value in an application that can change over time.Most real world applications such as Gmail make use of states that allow users to perform actions such as attaching documents and deleting emails.In Jetpack,composables cannot automatically keep track of states unless you introduce the remember function.A good example to demonstrate this is when you enter text in a textfield.Here,the textfield is unable to keep track of text state.

Composition and recomposition

Keeping track of state is essential within a composable or outside a composable.By default, when a user enters data into a text composable for the first time,it will keep track of the data.However,when the UI changes,a recomposition is initiated that resets the text composable to its initial value.What does this then mean?It actually means that you will lose your data because the text composable is unable to remember anything!Therefore,the use of the remember function is inevitable to allow composables to keep track of state updates.

var jetpack by remember

The remember function allows you to store a value during composition and remember the value during recomposition.For instance,you can store a variable named jetpack in the remember function.

To be able to observe changes in your composable,use mutableStateOf function that will then trigger recomposition.

var jetpack by remember {
mutableStateOf(“ ”)
}

You can store the state locally when used within the same composable.However,in most large scale applications there is a need to share states within different composables that will introduce the need to hoist the states.

State Hoisting

State hoisting entails moving a state outside a composable.This is demonstrated in the editNumberField function below by hoisting the state and making it accessible to other composables.

@Composable
fun editNumberField() {
var jetpack by remember {
mutableStateOf(“ ”)
}

TextField()
value = jetpack,
OnValueChange =  {  jetpack = it}

}

To hoist the state,declare the jetpack variable as method declarations to enable the parent composable to pass the state to the child composables as shown below.

Parent composable

 
@Composable
fun editNumberField() {
jetpack:String,
onValueChanged:(String) -> Unit
var jetpack by remember {
mutableStateOf(“ ”)
}
TextField()
value = jetpack,
OnValueChange = onValueChanged

}

Other composable

@Composable

fun ShowNumber(value: String=0){

Text(
text = “You have entered $value”

)

}

States in real world application

A good example is a tip timer application that keeps track of state changes on its user interfaces.When a user enters the bill amount,it automatically calculates the tip amount.The tip percentage is set to 18%.

To explore states in a real world application,ensure you have installed the latest version of Android Studio.

Project Setup

1.Setup a new project and name it Hotel Tip Timer

2.In the project package,navigate to the MainActivity.kt and remove the default Hello World and replace with this code:

package com.example.tiptime
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.tiptime.ui.theme.TipTimeTheme
import java.text.NumberFormat

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContent {
            TipTimeTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                ) {
                    TipTimeLayout()
                }
            }
        }
    }
}

@Composable
fun TipTimeLayout() {
    var amountInput by remember { mutableStateOf("") }

    val amount = amountInput.toDoubleOrNull() ?: 0.0
    val tip = calculateTip(amount)

    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.Start)
        )
        EditNumberField(
            value = amountInput,
            onValueChanged = { amountInput = it },
            modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
        )
        Text(
            text = stringResource(R.string.tip_amount, tip),
            style = MaterialTheme.typography.displaySmall
        )
        Spacer(modifier = Modifier.height(150.dp))
    }
}

@Composable
fun EditNumberField(
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier
) {
    TextField(
        value = value,
        singleLine = true,
        modifier = modifier,
        onValueChange = onValueChanged,
        label = { Text(stringResource(R.string.bill_amount)) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
    )
}

private fun calculateTip(amount: Double, tipPercent: Double = 18.0): String {
    val tip = tipPercent / 100 * amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
    TipTimeTheme {
        TipTimeLayout()
    }
}


3.Run the application in an emulator in Android Studio or on a physical device.

Functional Hotel Tip Timer App

Image

A solid understanding of composition,recomposition,state and state hoisting will enable you to build complex and interactive android applications.







share

Wilson Ochieng

Android Expert developing mobile applications with 4+ Years of Experience.