Intro to state in Compose

1. Before you begin

This codelab teaches you about state, and how it can be used and manipulated by Jetpack Compose.

At its core, state in an app is any value that can change over time. This definition is very broad and includes everything from a database to a variable in your app. You learn more about databases in a later unit, but for now all you need to know is that a database is an organized collection of structured information, such as the files on your computer.

All Android apps display state to the user. A few examples of state in Android apps include:

  • A message that shows when a network connection can't be established.
  • Forms, such as registration forms. The state can be filled out and submitted.
  • Tappable controls, such as buttons. The state could be not tapped, being tapped (display animation), or tapped (an onClick action).

In this codelab, you explore how to use and think about state when you use Compose. To do this, you build a tip-calculator app called Tip Time with these built-in Compose UI elements:

  • A TextField composable to enter and edit text.
  • A Text composable to display text.
  • A Spacer composable to display empty space between the UI elements.

At the end of this codelab, you'll have built an interactive tip calculator that automatically calculates the tip amount when you enter the service amount. This image shows what the final app looks like:

761df483de663721.png

Prerequisites

  • Basic understanding of Compose, such as the @Composable annotation.
  • Basic familiarity with Compose layouts, such as the Row and Column layout composables.
  • Basic familiarity with modifiers, such as the Modifier.padding() function.
  • Familiarity with the Text composable.

What you'll learn

  • How to think about state in a UI.
  • How Compose uses state to display data.
  • How to add a text box to your app.
  • How to hoist a state.

What you'll build

  • A tip-calculator app called Tip Time that calculates a tip amount based on the service amount.

What you'll need

  • A computer with internet access and a web browser
  • Knowledge of Kotlin
  • Android Studio

2. Watch the code-along video (Optional)

If you'd like to watch one of the course instructors complete the codelab, play the below video.

It's recommended to expand the video to full screen (with this icon This symbol shows 4 corners on a square highlighted, to indicate full screen mode. in the lower right corner of the video) so you can see Android Studio and the code more clearly.

This step is optional. You can also skip the video and start the codelab instructions right away.

3. Get started

  1. Check out Google's online tip calculator. Please note that this is just an example and this is not the Android app you will be creating in this course.

46bf4366edc1055f.png 18da3c120daa0759.png

  1. Enter different values in the Bill and Tip boxes. The tip and total values change.

c0980ba3e9ebba02.png

Notice that the moment you enter the values, the Tip and the Total update. By the end of the following codelab you will be developing a similar tip calculator app in Android.

In this pathway, you'll build a simple tip calculator Android app.

Developers will often work in this way—get a simple version of the app ready and working (even if it doesn't look very good), and then add more features and make it more visually appealing later.

By the end of this codelab, your tip calculator app will look like these screenshots. When the user enters a Cost of Service, your app will display a suggested tip amount. The tip percentage is hardcoded to 15% for now. In the next codelab, you will continue to work on your app and add more features like setting a custom tip percentage.

aaf86be8d13431f5.png

761df483de663721.png

4. Create a project

Set up a project in Android Studio with the Empty Compose Activity template and the required string resources:

  1. In Android Studio, create a project with the Empty Compose Activity template, enter Tip Time as the name, and select API 21: Android 5.0 (Lollipop) or higher as the minimum SDK. The project files load.
  2. In the Project pane, click res > values > strings.xml. You should have a single string resource for the app name.
  3. In between the <resources> tags, enter these string resources:
<string name="calculate_tip">Calculate Tip</string>
<string name="cost_of_service">Cost of Service</string>
<string name="tip_amount">Tip amount: %s</string>

The strings.xml file should look like this code snippet:

strings.xml

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="cost_of_service">Cost of Service</string>
   <string name="tip_amount">Tip amount: %s</string>
</resources>

5. Add a screen title

In this section, you add a screen title to the app with the Text composable function.

Delete the Greeting() function and add a TipTimeScreen() function to add the UI elements required for the app:

  1. In the MainActivity.kt file, delete the Greeting() function:
// Delete this.
@Composable
fun Greeting(name: String) {
   //...
}
  1. In the onCreate() and DefaultPreview() functions, delete the Greeting() function calls:
// Delete this.
Greeting("Android")
  1. Below the onCreate() function, add a TipTimeScreen()composable function to represent the app screen:
@Composable
fun TipTimeScreen() {
}
  1. In the onCreate() function's Surface() block, call the TipTimeScreen() function:
override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeScreen()
           }
       }
   }
}
  1. In the DefaultPreview() function's TipTimeTheme block, call the TipTimeScreen() function:
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
   TipTimeTheme {
       TipTimeScreen()
   }
}

Display the screen title

Implement the TipTimeScreen() function to display the screen title:

  1. In the TipTimeScreen() function, add a Column element. The elements are in a vertical column, which is why you use a Column element.
  2. In the Column block, pass in a modifier named parameter set to a Modifier.padding function that accepts a 32.dp argument:
Column(
   modifier = Modifier.padding(32.dp)
) {}
  1. Import these functions and this property:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
  1. In the Column function, pass in a verticalArrangement named argument set to an Arrangement.spacedBy function that accepts an 8.dp argument:
Column(
   modifier = Modifier.padding(32.dp),
   verticalArrangement = Arrangement.spacedBy(8.dp)
) {}

This adds a fixed 8dp space between child elements.

  1. Import the following:
import androidx.compose.foundation.layout.Arrangement
  1. Add a Text element that takes a text named parameter set to a stringResource(R.string.calculate_tip) function, a fontSize named parameter set to a 24.sp value, and a modifier named argument set to Modifier.align(Alignment.CenterHorizontally) function:
Text(
   text = stringResource(R.string.calculate_tip),
   fontSize = 24.sp,
   modifier = Modifier.align(Alignment.CenterHorizontally)
)
  1. Import these imports:
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Alignment
  1. In the Design pane, click Build & Refresh. You should see Calculate Tip appear as the title of the screen, which is the text element that you added.

da56236494529e77.png

Add the TextField composable

In this section, you add the UI element that lets the user enter the cost of service in the app. You can see what it looks like in this image:

58671affa01fb9e1.png

The TextField composable function lets the user enter text in an app. For example, notice the text box on the login screen of the Gmail app in this image:

30d9c9123b5d26fe.png

Add the TextField composable to the app:

  1. In the Column block after the Text element, add a Spacer() composable function with a 16dp height.
@Composable
fun TipTimeScreen() {
   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           ...
       )
       Spacer(Modifier.height(16.dp))
   }
}

This displays an empty 16dp space after the screen title.

  1. Import these functions:
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
  1. In the MainActivity.kt file, add an EditNumberField() composable function.
  2. In the body of the EditNumberField() function, add a TextField that accepts a value named parameter set to an empty string and an onValueChange named parameter set to an empty lambda expression:
@Composable
fun EditNumberField() {
   TextField(
      value = "",
      onValueChange = {},
   )
}
  1. Notice the parameters that you passed:
  • The value parameter is a text box that displays the string value that you pass here.
  • The onValueChange parameter is the lambda callback that's triggered when the user enters text in the text box.
  1. Import this function:
import androidx.compose.material.TextField
  1. On the line after the Spacer() composable function, call the EditNumberField() function:
@Composable
fun TipTimeScreen() {
   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           ...
       )
       Spacer(Modifier.height(16.dp))
       EditNumberField()
   }
}

This displays the text box on the screen.

  1. In the Design pane, click be24da86724b252c.pngBuild & refresh. You should see the Calculate Tip screen title and an empty text box with a 16dp space between them.

1ff60ec32d3b15c1.png

6. Use state in Compose

State in an app is any value that can change over time. In this app, the state is the cost of service.

Add a variable to store state:

  1. At the beginning of the EditNumberField() function, use the val keyword to add an amountInput variable assigned to a static "0" value:
val amountInput = "0"

This is the state of the app for the cost of service.

  1. Set the value named parameter to an amountInput value:
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. Build and run the app again. The text box displays the value set to the state variable as you can see in this image:

ba0f07ef1162855b.png

  1. Enter a different value. The hardcoded state remains unchanged because the TextField composable doesn't update itself. It updates when its value parameter changes, which is set to the amountInput property.

The amountInput variable represents the state of the text box. Having a hardcoded state isn't useful because it can't be modified and it doesn't reflect user input. You need to update the state of the app when the user updates the cost of service.

7. The Composition

The composables in your app describe a UI that shows a column with some text, a spacer, and a text box. The text shows a Calculate tip title, the Spacer is 16dp high, and the text box shows a 0 value or whatever the default value is.

Compose is a declarative UI framework, meaning that you declare how the UI should look in your code. If you wanted your text box to show a 100 value initially, you'd set the initial value in the code for the composables to a 100 value.

What happens if you want your UI to change while the app is running or as the user interacts with the app? For example, what if you wanted to update the amountInput variable with the value entered by the user and display it in the text box? That's when you rely on a process called recomposition to update the Composition of the app.

The Composition is a description of the UI built by Compose when it executes composables. Compose apps call composable functions to transform data into UI. If a state change happens, Compose re-executes the affected composable functions with the new state, which creates an updated UI—this is called recomposition. Compose schedules a recomposition for you.

When Compose runs your composables for the first time during initial composition, it keeps track of the composables that you call to describe your UI in a Composition. Recomposition is when Compose re-executes the composables that may have changed in response to data changes and then updates the Composition to reflect any changes.

The Composition can only be produced by an initial composition and updated by recomposition. The only way to modify the Composition is through recomposition. To do this, Compose needs to know what state to track so that it can schedule the recomposition when it receives an update. In your case, it's the amountInput variable, so whenever its value changes, Compose schedules a recomposition.

You use the State and MutableState types in Compose to make state in your app observable, or tracked, by Compose. The State type is immutable, so you can only read the value in it, while the MutableState type is mutable. You can use the mutableStateOf function to create an observable MutableState. It receives an initial value as a parameter that is wrapped in a State object, which then makes its value observable.

The value returned by the mutableStateOf() function:

  • Holds state, which is the cost of service.
  • Is mutable, so the value can be changed.
  • Is observable, so Compose observes any changes to the value and triggers a recomposition to update the UI.

Add a cost-of-service state:

  1. In the EditNumberField() function, change the val keyword before the amountInput state variable to the var keyword:
var amountInput = "0"

This makes it mutable.

  1. Use the MutableState<String> type instead of the hardcoded String variable so that Compose knows to track the amountInput state and then pass in a "0" string, which is the initial default value for the amountInput state variable:
var amountInput: MutableState<String> = mutableStateOf("0")

The amountInput initialization can also be written like this with type inference:

var amountInput = mutableStateOf("0")

The mutableStateOf() function receives an initial "0" value as a parameter that's wrapped in a State object, which then makes its value observable. This results in this compilation warning in Android Studio, but you fix it soon:

Creating a state object during composition without using remember.
  1. in the TextField composable function, use the amountInput.value property:
TextField(
   value = amountInput.value,
   onValueChange = { },
)

Compose keeps track of each composable that reads state value properties and triggers a recomposition when its value changes.

The onValueChange callback is triggered when the text box's input changes. In the lambda expression, the it variable contains the new value.

  1. In the onValueChange named parameter's lambda expression, set the amountInput.value property to the it variable:
@Composable
fun EditNumberField() {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
   )
}

You are updating the state of the TextField (that is the amountInput variable), when the TextField notifies you that there is a change in the text through onValueChange callback function.

  1. Run the app and enter text in the text box. The text box still shows a 0 value as you can see in this image:

6cb691703cc7ecbf.gif

When the user enters text in the text box, the onValueChange callback is called and the amountInput variable is updated with the new value. The amountInput state is tracked by Compose, so the moment that its value changes, recomposition is scheduled and the EditNumberField() composable function is executed again. In that composable function, the amountInput variable is reset to its initial 0 value. Thus, the text box shows a 0 value.

With the code you added, state changes cause recompositions to be scheduled.

However, you need a way to preserve the value of the amountInput variable across recompositions so that it's not reset to a 0 value each time that the EditNumberField() function recomposes. You resolve this issue in the next section.

8. Use remember function to save state

Composable methods can be called many times because of recomposition. The composable resets its state during recomposition if it's not saved.

Composable functions can store an object across recompositions with the remember. A value computed by the remember function is stored in the Composition during initial composition and the stored value is returned during recomposition. Usually remember and mutableStateOf functions are used together in composable functions to have the state and its updates be reflected properly in the UI.

Use the remember function in EditNumberField() function:

  1. In the EditNumberField() function, initialize the amountInput variable with the by remember Kotlin property delegate, by surrounding the call to mutableStateOf() function with remember.
  2. In the mutableStateOf() function, pass in an empty string instead of a static "0" string:
var amountInput by remember { mutableStateOf("") }

Now the empty string is the initial default value for the amountInput variable. by is a Kotlin property delegation. The default getter and setter functions for the amountInput property are delegated to the remember class's getter and setter functions, respectively.

  1. Import these functions:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
  1. Import these functions manually:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Adding the delegate's getter and setter imports lets you read and set amountInput without referring to the MutableState's value property.

Updated EditNumberField() function should look like this:

@Composable
fun EditNumberField() {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
   )
}
  1. Run the app and enter some text in the text box. You should see the text that you entered now.

270943a84f18572d.png

9. State and recomposition in action

In this section, you set a breakpoint and debug the EditNumberField() composable function to see how initial composition and recomposition work.

Set a breakpoint and debug the app on an emulator or a device:

  1. In the EditNumberField() function next to the onValueChange named parameter, set a line breakpoint.
  2. In the navigation menu, click Debug ‘app'. The app launches on the emulator or device. The execution of your app pauses for the first time when the TextField element is created.

e225f2d67e9f2c40.png

  1. In the Debug pane, click 2a29a3bad712bec.png Resume Program. The text box is created.
  2. On the emulator or device, enter a letter in the text box. The execution of your app pauses again when it reaches the breakpoint that you set.

The moment that you enter the text, Compose triggers a recomposition and the onValueChange callback in the EditNumberField() function is called with the new data as you can see in this image:

1d5e08d32052d02e.png

  1. In the Debug pane, click 2a29a3bad712bec.png Resume Program. The text entered in the emulator or on the device displays next to the line with the breakpoint as seen in this image:

1f5db6ab5ca5b477.png

This is the state of the text field.

  1. Click 2a29a3bad712bec.png Resume Program. The value that you entered is displayed on the emulator or device.

10. Modify the appearance

In the previous section, you got the text field to work. In this section, you enhance the UI.

Add a label to the text box

Every text box should have a label that lets users know what information they can enter. In the first part of the following example image, the label text is in the middle of a text field and aligned with the input line. In the second part of the following example image, the label is moved higher in the text box when the user clicks in the text box to enter text. To learn more about text-field anatomy, see Anatomy.

a2afd6c7fc547b06.png

Modify the EditNumberField() function to add a label to the text field:

  1. In the EditNumberField() function's TextField() composable function , add a label named parameter set to an empty lambda expression:
TextField(
//...
   label = { }
)
  1. In the lambda expression, call the Text() function that accepts a stringResource(R.string.cost_of_service):
label = { Text(stringResource(R.string.cost_of_service)) }
  1. In the TextField() composable function, add a modifier named parameter set to a Modifier.fillMaxWidth():
TextField(
  // Other parameters
   modifier = Modifier.fillMaxWidth(),
)
  1. Import the following:
import androidx.compose.foundation.layout.fillMaxWidth
  1. In the TextField() composable function, add singleLine named parameter set to a true value:
TextField(
  // Other parameters
   singleLine = true,
)

This condenses the text box to a single, horizontally scrollable line from multiple lines.

  1. Add the keyboardOptions parameter set to a KeyboardOptions():
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions()
)

Android provides an option to configure the keyboard displayed on the screen to enter digits, email addresses, URLs, and passwords, to name a few. To learn more about other keyboard types, see KeyboardType.

  1. Set the keyboard type to number keyboard to input digits. Pass the KeyboardOptions function a keyboardType named parameter set to a KeyboardType.Number:
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)

The completed EditNumberField() function should look like this code snippet:

@Composable
fun EditNumberField() {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.cost_of_service)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)

   )
}
  1. Import the following:
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
  1. Run the app.

You can see the changes in this image:

48368bf5df67af37.png

11. Display the tip amount

In this section, you implement the main functionality of the app, which is the ability to calculate and display the tip amount.

At the end of this task, your app will look like this:

aaf86be8d13431f5.png

Calculate the tip amount

Define and implement a function that accepts the cost of service and tip percentage, and returns the tip amount:

  1. In the MainActivity.kt file after the EditNumberField() function, add a private calculateTip() function.
  2. Add amount and tipPercent named parameters, both of Double type. The amount parameter passes the cost of service.
  3. Set the tipPercent parameter to a 15.0 default argument value. This sets the default tip value to 15% for now. In the next codelab, you get the tip amount from the user:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
}
  1. In the the function body, use the val keyword to define a tip variable that divides the tipPercent parameter by a 100 value and multiplies the result by the amount parameter to calculate the tip:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
   val tip = tipPercent / 100 * amount
}

Now your app can calculate the tip, but you still need to format and display it with the NumberFormat class.

  1. On the next line of the calculateTip() function body, call the NumberFormat.getCurrencyInstance() function:
NumberFormat.getCurrencyInstance()

This gives you a number formatter that you can use to format numbers as currency.

  1. On the NumberFormat.getCurrencyInstance() function call chain the format() method and pass it the tip variable as a parameter:
NumberFormat.getCurrencyInstance().format(tip)
  1. When prompted by Android Studio, import this class.
import java.text.NumberFormat
  1. The last step is to return the formatted string from the function. Modify the function signature to return a String type. Add the return keyword in front of the NumberFormat statement:
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

Now the function returns a formatted string.

Use the calculateTip() function

The text entered by the user in the text field composable is returned to the onValueChange callback function as a String even though the user entered a number. To fix this, you need to convert the amountInput value, which contains the amount entered by the user.

  1. In EditNumberField() composable function, call the toDoubleOrNull function on the amountInput variable, to convert the String to a Double:
val amount = amountInput.toDoubleOrNull()

The toDoubleOrNull() function is a predefined Kotlin function that parses a string as a Double number and returns the result or null if the string isn't a valid representation of a number.

  1. At the end of the statement, add an ?: Elvis operator that returns a 0.0 value when amountInput is null:
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. After the amount variable, create another val variable called tip. Initialize it with the calculateTip(), passing the amount parameter.
val tip = calculateTip(amount)

The EditNumberField() function should look like this code snippet:

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

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

   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.cost_of_service)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

Show the calculated tip amount

You have written the function to calculate the tip amount, the next step is to add a Text composable to display the calculated tip amount:

97734d91a3844d22.png

  1. In the TipTimeScreen() function at the end of the Column() block, add a Spacer() composable and pass in a 24.dp height:
@Composable
fun TipTimeScreen() {
   Column(
       //...
   ) {
       Text(
           //...
       )
       //...
       EditNumberField()
       Spacer(Modifier.height(24.dp))
   }
}

This adds space after the text field.

  1. After the Spacer() composable, add the following Text composable:
Text(
   text = stringResource(R.string.tip_amount, ""),
   modifier = Modifier.align(Alignment.CenterHorizontally),
   fontSize = 20.sp,
   fontWeight = FontWeight.Bold
)

This code uses the tip_amount string resource to set text, but the tip amount isn't displayed; you fix this issue shortly. It centers the text on the screen in a 20.sp size with the font weight set to bold.

  1. Import the following imports:
import androidx.compose.ui.text.font.FontWeight

You need to access the amountInput variable in the TipTimeScreen function to calculate and display the tip amount, but the amountInput variable is the state of the text field defined in the EditNumberField() composable function, so you can't call it from the TipTimeScreen() function yet. This image illustrates the structure of the code:

5ec86acdbfa1907b.png

This structure won't let you display the tip amount in the new Text composable because the Text composable needs to access the amount variable calculated from the amountInput variable. You need to expose the amount variable to the TipTimeScreen() function. This image illustrates the desired code structure, which makes the EditNumberField() composable stateless:

e11d5bba4d8abd0d.png

This pattern is called state hoisting. In the next section, you hoist, or lift, the state from a composable to make it stateless.

12. State hoisting

In this section, you learn how to decide where to define your state in a way that you can reuse and share your composables.

In a composable function, you can define variables that hold state to display in the UI. For example, you defined the amountInput variable as state in the EditNumberField() composable.

When your app becomes more complex and other composables need access to the state within the EditNumberField() composable, you need to consider hoisting, or extracting, the state out of the EditNumberField() composable function.

Understand stateful versus stateless composables

You should hoist the state when you need to:

  • Share the state with multiple composable functions.
  • Create a stateless composable that can be reused in your app.

When you extract state from a composable function, the resulting composable function is called stateless. That is, composable functions can be made stateless by extracting state from them.

A stateless composable is a composable that doesn't have a state, which means that it doesn't hold, define, or modify a new state. On the other hand, a stateful composable is a composable that owns a piece of state that can change over time.

State hoisting is a pattern of moving state up to a different function to make a component stateless.

When applied to composables, this often means introducing two parameters to the composable:

  • A value: T parameter, which is the current value to display.
  • An onValueChange: (T) -> Unit – callback lambda, which is triggered when the value changes so that the state can be updated elsewhere, such as when a user enters some text in the text box.

Hoist the state in EditNumberField() function:

  1. Update the EditNumberField() function definition, to hoist the state by adding the value and onValueChange parameters:
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit
)

The value parameter is of String type, and the onValueChange parameter is of (String) -> Unit type, so it's a function that takes a String value as input and has no return value. The onValueChange parameter is used as the onValueChange callback passed into the TextField composable.

  1. In the EditNumberField() function, update the TextField() composable function to use the passed in parameters:
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. Hoist the state, move the remembered state from the EditNumberField()function to the TipTimeScreen() function:
@Composable
fun TipTimeScreen() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)
  
   Column(
       //...
   ) {
       //...
   }
}
  1. You hoisted the state to TipTimeScreen(), now pass it to EditNumberField(). In the TipTimeScreen() function, update the EditNumberField() function call to use the hoisted state:
EditNumberField(value = amountInput,
   onValueChange = { amountInput = it }
)
  1. Use the tip property to display the tip amount. Update the Text composable's text parameter to use the tip variable as a parameter. This is called positional formatting.
Text(
   text = stringResource(R.string.tip_amount, tip),
   // Rest of the code
)

With positional formatting, you can display dynamic content in strings. For example, imagine that you want the Tip amount text box to display an xx.xx value that could be any amount calculated and formatted in your function. To accomplish this in the strings.xml file, you need to define the string resource with a placeholder argument, like this code snippet:

// No need to copy.

// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>


// In your Compose code
Text(
    text = stringResource(R.string.tip_amount, tip)
)

You can have multiple, and any type of, placeholder arguments. A string placeholder is %s. In Compose, you pass in the formatted tip as an argument to the stringResource() function.

The completed TipTimeScreen() and EditNumberField() functions should look like this code snippet:

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

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

   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           fontSize = 24.sp,
           modifier = Modifier.align(Alignment.CenterHorizontally)
       )
       Spacer(Modifier.height(16.dp))
       EditNumberField(value = amountInput,
           onValueChange = { amountInput = it }
       )
       Spacer(Modifier.height(24.dp))
       Text(
           text = stringResource(R.string.tip_amount, tip),
           modifier = Modifier.align(Alignment.CenterHorizontally),
           fontSize = 20.sp,
           fontWeight = FontWeight.Bold
       )
   }

}

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

To summarize, you hoisted the amountInput state from the EditNumberField() into the TipTimeScreen() composable. For the text box to work as before, you have to pass in two arguments to the EditNumberField() composable function: the amountInput value and the lambda callback that updates the amountInput value from the user's input. These changes let you calculate the tip from the amountInput property in the TipTimeScreen() to display it to the user.

  1. Run the app on the emulator or device and then enter a value in the Cost of Service text box. The tip amount of 15% is displayed as you can see in this image:

13. Get the solution code

To download the code for the finished codelab, you can use this git command:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout state

Alternatively you can download the repository as a zip file, unzip it, and open it in Android Studio.

If you want to see the solution code, view it on GitHub.

14. Conclusion

Congratulations! You completed this codelab and learned how to use state in a Compose app!

Summary

  • State in an app is any value that can change over time.
  • The Composition is a description of the UI built by Compose when it executes composables. Compose apps call composable functions to transform data into UI.
  • Initial composition is a creation of the UI by Compose when it executes composable functions the first time.
  • Recomposition is the process of running the same composables again to update the tree when their data changes.
  • State hoisting is a pattern of moving state up to make a component stateless.

Learn more