Bird
Raised Fist0
Android Kotlinmobile~20 mins

State hoisting pattern in Android Kotlin - Mini App: Build & Ship

Choose your learning style10 modes available
Build: NameInputScreen
A simple screen where the user can enter their name. The text input state is managed outside the input composable using state hoisting pattern.
Target UI
-------------------------
| Enter your name:      |
| [               ]     |
|                       |
| [Submit]              |
-------------------------
Create a composable function that displays a TextField and a Button.
The TextField's text state should be passed in and updated via parameters (state hoisting).
The parent composable should hold the state of the text input.
When the user taps Submit, show a Toast with the entered name.
Starter Code
Android Kotlin
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp

@Composable
fun NameInputScreen() {
    // TODO: Hold the text state here

    Column(modifier = Modifier.padding(16.dp)) {
        // TODO: Call NameInput with state and onValueChange
        // TODO: Handle Submit button click to show Toast
    }
}

@Composable
fun NameInput(name: String, onNameChange: (String) -> Unit, onSubmit: () -> Unit) {
    Column {
        TextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Enter your name") }
        )
        Button(onClick = onSubmit, modifier = Modifier.padding(top = 8.dp)) {
            Text("Submit")
        }
    }
}
Task 1
Task 2
Task 3
Solution
Android Kotlin
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp

@Composable
fun NameInputScreen() {
    val context = LocalContext.current
    val nameState = remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        NameInput(
            name = nameState.value,
            onNameChange = { newName -> nameState.value = newName },
            onSubmit = {
                Toast.makeText(context, "Hello, ${nameState.value}!", Toast.LENGTH_SHORT).show()
            }
        )
    }
}

@Composable
fun NameInput(name: String, onNameChange: (String) -> Unit, onSubmit: () -> Unit) {
    Column {
        TextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Enter your name") }
        )
        Button(onClick = onSubmit, modifier = Modifier.padding(top = 8.dp)) {
            Text("Submit")
        }
    }
}

This example uses the state hoisting pattern by keeping the nameState in the NameInputScreen composable. The NameInput composable receives the current name and a function to update it as parameters. This way, NameInput is stateless and reusable.

When the user types, onNameChange updates the state in the parent. When the user taps Submit, a Toast shows the entered name using the current state.

This pattern helps separate UI from state management, making the code easier to test and maintain.

Final Result
Completed Screen
-------------------------
| Enter your name:      |
| [John Doe       ]     |
|                       |
| [Submit]              |
-------------------------
User types text in the input field, the text updates immediately.
User taps Submit button, a Toast appears saying 'Hello, John Doe!' (or the entered name).
Stretch Goal
Add a clear button inside the TextField to clear the input text.
💡 Hint
Use the trailingIcon parameter of TextField to add an IconButton that calls onNameChange("") when clicked.

Practice

(1/5)
1. What is the main purpose of state hoisting in Android Kotlin UI development?
easy
A. To avoid using ViewModel for state management
B. To keep all state inside UI components for faster rendering
C. To make UI components dependent on global variables
D. To move state out of UI components to make them simpler and reusable

Solution

  1. Step 1: Understand state hoisting concept

    State hoisting means moving state out of UI components so they don't manage their own state internally.
  2. Step 2: Identify the benefit of moving state out

    This makes UI components simpler, easier to reuse, and testable by passing state and events as parameters.
  3. Final Answer:

    To move state out of UI components to make them simpler and reusable -> Option D
  4. Quick Check:

    State hoisting = move state out [OK]
Hint: State hoisting means lifting state up from UI [OK]
Common Mistakes:
  • Thinking state should stay inside UI components
  • Confusing state hoisting with global variables
  • Believing ViewModel is not related to state hoisting
2. Which of the following is the correct way to pass state to a composable function following the state hoisting pattern?
easy
A. fun Greeting() { var name = "John"; Text("Hello $name") }
B. fun Greeting() { val name = remember { mutableStateOf("John") }; Text("Hello ${name.value}") }
C. fun Greeting(name: String) { Text("Hello $name") }
D. fun Greeting() { Text("Hello") }

Solution

  1. Step 1: Identify state hoisting signature

    State hoisting means the composable receives state as a parameter instead of owning it internally.
  2. Step 2: Check which function receives state as parameter

    fun Greeting(name: String) { Text("Hello $name") } passes name: String as parameter, which is correct for hoisting state.
  3. Final Answer:

    fun Greeting(name: String) { Text("Hello $name") } -> Option C
  4. Quick Check:

    State passed as parameter = fun Greeting(name: String) { Text("Hello $name") } [OK]
Hint: State hoisted means passed as parameter, not created inside [OK]
Common Mistakes:
  • Creating state inside composable instead of passing it
  • Not passing state at all
  • Using remember inside UI component for hoisted state
3. Given the following code, what will be the output when the button is clicked once?
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
  Text("Clicked $count times")
}
medium
A. Clicked 1 times
B. Clicked 0 times
C. Clicked times
D. Runtime error

Solution

  1. Step 1: Understand initial state value

    The variable count starts at 0 using remember and mutableStateOf.
  2. Step 2: Analyze button click effect

    When the button is clicked once, count++ increments count to 1, triggering recomposition.
  3. Final Answer:

    Clicked 1 times -> Option A
  4. Quick Check:

    Initial 0 + 1 click = 1 [OK]
Hint: Increment state on click updates UI text [OK]
Common Mistakes:
  • Thinking count stays 0 after click
  • Ignoring recomposition after state change
  • Expecting syntax or runtime error
4. Identify the error in this state hoisting example:
@Composable
fun Counter() {
  var count by remember { mutableStateOf(0) }
  CounterDisplay(count)
}

@Composable
fun CounterDisplay(count: Int) {
  Button(onClick = { count++ }) {
    Text("Count: $count")
  }
}
medium
A. count++ inside CounterDisplay does not update state in Counter
B. Button onClick should not modify count at all
C. remember should be used inside CounterDisplay instead
D. State should be mutable in CounterDisplay to increment

Solution

  1. Step 1: Analyze state ownership and mutation

    State count is owned in Counter and passed as immutable Int to CounterDisplay.
  2. Step 2: Understand why count++ in CounterDisplay fails

    Incrementing count inside CounterDisplay only changes local copy, not the original state in Counter.
  3. Final Answer:

    count++ inside CounterDisplay does not update state in Counter -> Option A
  4. Quick Check:

    Immutable parameter can't update hoisted state [OK]
Hint: Only owner can update hoisted state, not parameter [OK]
Common Mistakes:
  • Trying to mutate immutable parameter
  • Using remember in child composable incorrectly
  • Not passing event callback to update state
5. How should you modify the following code to correctly implement state hoisting so that CounterDisplay can update the count?
@Composable
fun Counter() {
  var count by remember { mutableStateOf(0) }
  CounterDisplay(count)
}

@Composable
fun CounterDisplay(count: Int) {
  Button(onClick = { count++ }) {
    Text("Count: $count")
  }
}
hard
A. Make count a var inside CounterDisplay and increment it there
B. Pass a lambda to CounterDisplay to update count in Counter
C. Use remember { mutableStateOf(0) } inside CounterDisplay instead
D. Remove count parameter and manage state only in CounterDisplay

Solution

  1. Step 1: Recognize state hoisting requires event callbacks

    To update hoisted state, child composable must receive a function to trigger state changes in the owner.
  2. Step 2: Modify CounterDisplay to accept update lambda

    Pass a lambda like onCountChange: () -> Unit to CounterDisplay and call it inside onClick.
  3. Final Answer:

    Pass a lambda to CounterDisplay to update count in Counter -> Option B
  4. Quick Check:

    State hoisting needs event callback to update state [OK]
Hint: Pass update function down to child to change hoisted state [OK]
Common Mistakes:
  • Trying to mutate parameter directly
  • Creating new state inside child composable
  • Removing state from owner composable
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard