copper-leaf / ballast

Opinionated Application State Management framework for Kotlin Multiplatform
https://copper-leaf.github.io/ballast/
BSD 3-Clause "New" or "Revised" License
144 stars 11 forks source link

Send inputs on ViewModel initialization #22

Closed iruizmar closed 2 years ago

iruizmar commented 2 years ago

Sometimes there's the need to launch an Input right on ViewModel init. For example, when you need to fetch some data to shown on the screen.

Checking the examples, I've seen that the way it's been done is to use the Activities onCreate, but in other architectures like a pure Compose app, using lifecycle is not really the way. Also, delegating this kind of internal actions to the view doesn't look completely correct.

Another approach I've been using is to use the init block of the ViewModel, but this is a bit cumbersome since, ideally, you will declare different ViewModels for each platform and you'll have to replicate this logic in each of them:

class MyViewModel(
    config: BallastViewModelConfiguration<MyContract.Input, MyContract.Event, MyContract.State>,
) : AndroidViewModel<MyContract.Input, MyContract.Event, MyContract.State>(config) {
    init {
        trySend(MyContract.Input.MyInput)
    }
}

My proposal is to be able to indicate a list of desired Inputs to be launched on ViewModel initialization. You can see how some other state management libraries are doing it here.

Ideally this will be set in the InputHandler since this is the class written in common code.

cjbrooks12 commented 2 years ago

This probably doesn't need a new API for it, you can write a pretty simple Interceptor to achieve this behavior (here's the docs, something like this:

public class BootstrapInterceptor<Inputs : Any, Events : Any, State : Any>(
    private val getInitialInput: suspend () -> Inputs,
) : BallastInterceptor<Inputs, Events, State> {

    override fun BallastInterceptorScope<Inputs, Events, State>.start(notifications: Flow<BallastNotification<Inputs, Events, State>>) {
        launch(start = CoroutineStart.UNDISPATCHED) {
            // wait for the BallastNotification.ViewModelStarted to be sent
            notifications
                .filterIsInstance<BallastNotification.ViewModelStarted<Inputs, Events, State>>()
                .first()

            // generate an Input
            val initialInput = getInitialInput()

            // post the Input back to the VM
            sendToQueue(
                Queued.HandleInput(null, initialInput)
            )
        }
    }
}

You could of course adapt the behavior to wait for the initial State to be set as well, by also waiting for BallastNotification.StateChanged to be sent, or any other logic you might need for it.

I had gone back and forth on whether this is something I wanted to include directly in the core library, and while I'm definitely not opposed to having this if there's enough demand for this use-case, there are several reasons why I chose not to include it yet:

iruizmar commented 2 years ago

Thanks for the deep explanation! Knowing that there's a way to do it, it works for me. Thank you :)