3

I've seen some Jetpack Compose projects and I've seen two types of managing states, not realizing which one is better.

For example, let's assume: the input state. I've seen people manage this state in the UI, using remember to save the state of the value.

Another way I've seen is to create this mutableState in the ViewModel and store/use it from there. What's the best way to do this?

z.g.y
  • 5,512
  • 4
  • 10
  • 36
R0ck
  • 409
  • 1
  • 15

2 Answers2

4

In addition to @Thracian's answer.

Let me share my thought process based on my current level of experience in Jetpack Compose. Just a disclaimer, I'm still in the learning curve.

IMO, theres no such thing as "best", things in our field evolves, what might be considered "best" today may become obsolete tomorrow, but there are certain practices that are "recommended", approved and adopted by the community which might save you from dealing with some pitfalls (e.g unwanted re-compositions, infinite navhost calls( you already dealt with this) etc..), but its up to you if you will follow it or not.

So what your'e trying to understand is called State Hoisting. The way I could explain this is by just simply sampling a scenario (again this is based on my own experience with how I apply my knowledge in Jetpack Compose).

Consider a Login use-case with 3 different levels of complexity

  • A Login UI prototype : — Just showcasing your potential Login Screen design and user interaction
  • Login UI Mock-up : — With a bit of validation and some toast showing a negative scenario, just an advance version of the prototype
  • A fully working Login module — where you have to construct view models, bind things to lifecycles, perform concurrent operations etc..

At this point, you already have an idea the different levels of state management based on the use-case above.

For a Login prototype, I won't be needing a state class or a view model, since its just a prototype

@Composable
fun LoginScreen() {
    
    val userName by remember { <mutable string state username> }
    val password by remember { <mutable string state password> }

    Column {
        Text(text = username)
        Text(text = password)

        Button("Login")
    }
}

and because its a very simple UI(composable), I only need to specify basic structure of a composable using remember + state, showcasing an input is happening.

For the Login mock-up with simple validation, we utilized the recommended state hoisting using a class,

class LoginState {

    var event;
    var mutableUserNameState;
    var mutablePasswordState;

    fun onUserNameInput() {...}
    fun onPasswordInput() {...}

    fun onValidate() {
        if (not valid) {
            event.emit(ShowToast("Not Valid"))
        } else {
            event.emit(ShowToast("Valid"))
        }
    }
}

@Composable
fun LoginScreen() {

    val loginState by remember { LoginState }

    LaunchedEffect() {
        event.observe {
            it.ShowToast()
        }
    }
    Column {
        Text(text = loginState.mutableUserNameState, onInput = { loginState.onUserNameInput()} )
        Text(text = loginState.mutablePasswordState, onInput = { loginState.onPasswordInput()} )

        Button(loginState.onValidate)
    }
}

Now for a full blown Login Module, where your'e also taking lifecylce scopes into consideration

class LoginViewModel(
    val userRepository: UserRepository // injected by your D.I framework
): ViewModel {

    var event;
    var mutableUserNameState;
    var mutablePasswordState;

    fun onUserNameInput() {...}
    fun onPasswordInput() {...}

    fun onValidateViaNetwork() {
        // do a non-blocking call to a server
        viewModelScope.launch {
            var isUserValid = userRepository.validate(username, password)
            if (isUserValid) {
                event.emit(ShowToast("Valid"))
            } else {
                event.emit(ShowToast("Not Valid"))
            }
        }
    }
}

@Composable
fun LoginScreen() {

    val userNameState by viewModel.mutableUserNameState
    val passwordState by viewModel.mutablePasswordState
    
    LaunchedEffect() {
        event.observe {
            it.ShowToast()
        }
    }

    Column {
        Text(text = userNameState, onInput = { viewModel.onUserNameInput()} )
        Text(text = passwordState, onInput = { viewModel.onPasswordInput()} )

        Button(viewModel.onValidateViaNetwork)
    }
}

Again, this is just based on my experience and how I decide on hoisting my states. As for the snippets I included, I tried to make them as pseudo as possible without making them look out of context so they are not compilable. Also mock and prototype are considered the same, I just used them in conjunction to put things into context.

z.g.y
  • 5,512
  • 4
  • 10
  • 36
  • 1
    Thanks for that examples! I started with Jetpack Compose in July, so I am also in a learning curve and I have a lot to learn. It's good to know that is recommended and what's not. I was with doubts about the way that is more recommended, but seems that both that I mentioned are fine. Which one is best for testing? – R0ck Nov 29 '22 at 16:08
  • And when to use collectAsStateWithLifecycle or collectAsState()? – R0ck Nov 29 '22 at 16:11
  • Thank you and your'e welcome, I'm honestly not that very good at "testing", "testing" itself is a big topic but I think I can safely say either of them are good for testing, it depends on your situation. No offense, forget about thinking the "best", just keep on learning and always keep in mind certain discipline such as YAGNI and always stay pragmatic. Like you I'm still getting deeper to compose I can't expound very much the different between `collectAsStateWithLifecycle` and `collectAsState()`,but the former is tied to lifecycle I think.. and I usually see them being used from a ViewModel. – z.g.y Nov 29 '22 at 16:13
2

It depends on your preference. Using states inside a Composable if you are building a standalone Composable or a library is preferred. Any class you see with rememberXState() keeps state variable. For instance scrollState()

@Composable
fun rememberScrollState(initial: Int = 0): ScrollState {
    return rememberSaveable(saver = ScrollState.Saver) {
        ScrollState(initial = initial)
    }
} 


@Stable
class ScrollState(initial: Int) : ScrollableState {

/**
 * current scroll position value in pixels
 */
var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
    private set

// rest of the code }

This is a common approach in Jetpack Compose. I use this approach in libraries i build, for instance in this image crop library, i keep state and Animatable. Animatable which is low level default animation class also has hold its own states.

@Suppress("NotCloseable")
class Animatable<T, V : AnimationVector>(
    initialValue: T,
    val typeConverter: TwoWayConverter<T, V>,
    private val visibilityThreshold: T? = null
) {

internal val internalState = AnimationState(
    typeConverter = typeConverter,
    initialValue = initialValue
)

/**
 * Current value of the animation.
 */
val value: T
    get() = internalState.value

/**
 * Velocity vector of the animation (in the form of [AnimationVector].
 */
val velocityVector: V
    get() = internalState.velocityVector

/**
 * Returns the velocity, converted from [velocityVector].
 */
val velocity: T
    get() = typeConverter.convertFromVector(velocityVector)

/**
 * Indicates whether the animation is running.
 */
var isRunning: Boolean by mutableStateOf(false)
    private set

/**
 * The target of the current animation. If the animation finishes un-interrupted, it will
 * reach this target value.
 */
var targetValue: T by mutableStateOf(initialValue)
    private set

}

and so on. This approach is doing for ui components that don't involve business logic but Ui logic.

When you need to update your Ui based on business logic like search or getting results from an API you should use a Presenter class which can be ViewModel too.

Last but least people are now questioning whether there should be a ViewModel with Jetpack Compose since we can use states with an AAC ViewModel. And cashapp introduced molecule library, you can check it out either.

Also this link about state holders is good source to read

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • That documentation from Android Developers seems good, I will for sure take a look at it. Thanks for your opinion! I will read that documentation, analyze molecule library and, in the end, follow what I think is the best practice. – R0ck Nov 29 '22 at 15:48