Home > Enterprise >  State hoisting in Kotlin onSearchChange: (String) -> Unit
State hoisting in Kotlin onSearchChange: (String) -> Unit

Time:10-23

I am trying to use state hoisting in android


I am new to android development using jetpack compose

onSearchChange: (String) -> Unit,
onCategoryChange: (Category) -> Unit,
onProductSelect: (Product) -> Unit,
    
    
composable(Screen.Home.route) { MainPage(navController = navController, searchQuery = "",
                productCategories = categories, selectedCategory = Category("","",0),
                products = pros, /* what do I write here for the 3 lines above?? :( the onSearch,etc I have an error bc of them */
                )}

CodePudding user response:

To put into simple terms, state hoisting is having your state variables in the outer most composable possible, this way you can use the same value in more than one composable function, gives you better performance less mess and code reusability! Hoisting is one of the fundamentals of using Jetpack Compose, example below:

@Composable
fun OuterComposable(
    modifier: Modifier = Modifier    
) {

    // This is your state variable
    var input by remember { mutabelStateOf("") }

    InnerComposable(
        modifier = Modifier,
        text = input,
        onType = { input = it } // This will asign the string returned by said function to the "input" state variable
    )
}

@Composable
fun InnerComposable(
    modifier: Modifier = Modifier
    text: String,
    onType: (String) -> Unit
) {

    TextField(
        modifier = modifier,
        value = text,
        onValueChange = { onType(it) } // This returns what the user typed (function mentioned in the previous comment)
    )
}

With the code above, you basically have a text field in the "InnnerComposable" function which becomes usable in multiple places with different values. You can keep adding layers of composables, important thing is to keep the state variable at the highest possible function.

Hope the explanation was clear! :)

CodePudding user response:

In addition to the answer, apologies, this is a bit long, as Ill try to share how I design my "state hoisting"

Lets simply start first with the following:

A: First based on the Official Docs

State in an app is any value that can change over time. This is a very broad definition and encompasses everything from a Room database to a variable on a class.

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

  • A Snackbar that shows when a network connection can't be established.
  • A blog post and associated comments.
  • Ripple animations on buttons that play when a user clicks them.
  • Stickers that a user can draw on top of an image.

B: And personally, for me

"State Hoisting" is part of "State Management"

Now consider a very simple scenario, We have a LoginForm with 2 input fields, and have its basic states like the following

  • Input will be received from the user and will be stored in a mutableState variable named userName
  • Input will be received from the user and will be stored in a mutableState variable named password

We have defined 2 requirements above, without doing them, our LoginForm would be stateless

@Composable
fun LoginForm() {

    var userName by remember { mutableStateOf("")}
    var password by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentSize()
    ) {

        TextField(
            value = userName,
            onValueChange = {
                userName = it
            }
        )

        TextField(
            value = password,
            onValueChange = {
                password = it
            },
            visualTransformation = PasswordVisualTransformation()
        )
    }
}

So far, everything is working but nothing is "Hoisted", their states are handled inside the LoginForm composable.

State Hoisting Part 1: a LoginState class

Now apart from the 2 requirements above, lets add one additional requirement.

  • Validate user name and password
    • if login is invalid, show Toast "Sorry invalid login"
    • if login is valid, show Toast "Hello and Welcome to compose world"

This can be done inside the LoginForm composable, but its better to do the logic handling or any business logic in a separate class, leaving your UI intact independent of it

class LoginState {
    var userName by mutableStateOf("")
    var password by mutableStateOf("")


    fun validateAction() {
        if (userName == "Stack" && password == "Overflow") {
            // tell the ui to show Toast 
        } else {
            // tell the ui to show Toast
        }
    }
}

@Composable
fun LoginForm() {

    val loginState = remember { LoginState() }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentSize()
    ) {

        TextField(
            value = loginState.userName,
            onValueChange = {
                loginState.userName = it
            }
        )

        TextField(
            value = loginState.password,
            onValueChange = {
                loginState.password = it
            },
            visualTransformation = PasswordVisualTransformation()
        )
    }
}

Now everything is still working and with additional class where we hoisted our userName and password, and we included a validation functionality, nothing fancy, it will simply call something that will show Toast with a string message depending if the login is valid or not.

State Hoisting Part 2: a LoginViewModel class

Now apart from the 3 requirements above, lets adjust the validation requirement and add more

  • Validate user name and password
  • if login is invalid, show Toast "Sorry invalid login"
  • if login is valid, call a Post login network call and update your database
  • if Login is success from backend sever show a Toast "Welcome To World"
  • But when the app is minimized you have to dispose any current network call, no Toast should be shown.

The codes below won't simply work and not how you would define it in a real situation.

val viewModel = LoginViewModel()

data class UserLogin(
    val userName : String = "",
    val password : String = ""
)

class LoginViewModel (
    val loginRepository: LoginRepository
) {

    private val _loginFlow = MutableStateFlow(UserLogin())
    val loginFlow : StateFlow<UserLogin> = _loginFlow

    fun validateAction() {
        // ommited codes
    }

    fun onUserNameInput(userName: String) {
    }

    fun onPasswordInput(password: String) {
    }
}


@Composable
fun LoginForm() {

    val loginState by viewModel.loginFlow.collectAsStateWithLifecycle()

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentSize()
    ) {

        TextField(
            value = loginState.userName,
            onValueChange = {
                viewModel.onUserNameInput(it)
            }
        )

        TextField(
            value = loginState.password,
            onValueChange = {
                viewModel.onPasswordInput(it)
            },
            visualTransformation = PasswordVisualTransformation()
        )
    }
}

That's the most top level state hoisting where you would deal with network calls and database.

To summarize:

  • You don't need to consider hoisting up your mutableStates, if its just a simple composable doing simple thing.
  • But If the logic gets bigger consider using a State Class like the LoginState class
  • If you have to perform some network calls, database updates and making sure such use-cases are bound to a LifeCycle, consider hoisting using a ViewModel

Another thing to mention but out of topic is when you are hoisting states, there is a thing called scoped re-composition where you want a specific composable to get updated without affecting the others around, it is where you will think your composable designs on how you would handle mutableStates.

  • Related