Hello, I am trying to recreate the viewmodel from...
# compose-desktop
d
Hello, I am trying to recreate the viewmodel from android and I don't know how. I want to have a
viewModelScope
inside it and use that, so maybe using
rememberCoroutineScope()
with it, but I don't know how to do it properly.
I tried something like this(using kodein):
Copy code
@Composable
fun <VM : ViewModel> createViewModel(
    scope: CoroutineScope,
    typeToken: TypeToken<VM>
): VM {
    val viewModel: VM by localDI().Instance(typeToken)
    viewModel.initialise(scope)
    return viewModel
}

@Composable
inline fun <reified VM : ViewModel> createViewModel(
    scope: CoroutineScope = rememberCoroutineScope()
): VM {
    return createViewModel(scope, generic())
}
but I get an error:
Composable calls are not allowed inside the calculation parameter of inline fun <T> remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
This is the
ViewModel
I came up with:
Copy code
abstract class ViewModel {

    lateinit var scope: CoroutineScope

    open fun onCreate() {
        //todo
    }

    internal fun initialise(scope: CoroutineScope) {
        this.scope = scope
        onCreate()
    }

}
h
rememberCoroutineScope()
should be only used for your UI, eg onClick callbacks, snackbar etc. Don't use this scope for logic in your
ViewModel
, otherwise a recomposition could cancel suspended jobs in your ViewModel! For ViewModels, you could create its own
CoroutineScope
, eg:
CoroutineScope(Dispatchers.Default)
d
and when should I cancel them?
h
It depends on your architecture. Normally, your ViewModels survive until the application exit: https://developer.android.com/topic/libraries/architecture/viewmodel#lifecycle
d
are you sure? I thought they are tied to the lifecycle of the owner(like fragments) and they are cleared after the lifecycle is stopped(usually a little later, after the
onDestroy
is called)
Copy code
ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting the ViewModel. The ViewModel remains in memory until the Lifecycle it's scoped to goes away permanently: in the case of an activity, when it finishes, while in the case of a fragment, when it's detached.
I think I need to
cancel
the scope when the screen is no longer shown
h
On android, the fragment is the view, which will be destroyed, eg after a screen rotation. To keep the state, you usually use a ViewModel, which survives the recreation of the fragments.
d
But I have a navigation inside my app and these fragments are shown for a while and then gone forever
h
You could cancel/destroy your ViewModel right after dismissing their related views, but Compose recompose the views very often, and, if you have to fetch some data from disk/network, you probably don't want to fetch your data after each recomposition/dismissing etc. Often, you want to cache these IO operations. And you can do this with ViewModels.
d
isn't
rememberCoroutineScope
enough for that? I thought it remembers the
scope
through recompositions and cancels only when it is no longer composed
h
It depends. If you want to call any suspend function, eg to upload something, it will be immediately canceled after dismissing the view:
Copy code
@ExperimentalTime
@Composable
fun Content() {
    var show by remember { mutableStateOf(true) }
    if (show) {
        First { show = false }
    } else {
        Second()
    }
}

@ExperimentalTime
@Composable
fun First(onClick: () -> Unit) {
    val scope = rememberCoroutineScope()
    Button({
        scope.launch {
            something()
        }
        onClick()
    }) {
        Text("Hello")
    }
}

@ExperimentalTime
suspend fun something() {
    println("Start")
    delay(5.seconds)
    println("End")
}

@Composable
fun Second() {
    Text("Second")
}
End
will never be printed.
👍🏻 1
d
that's ok for me as requests should be canceled when exitting the screen
I mostly need the scope for
stateIn
so I can collect a flow for any changes while the view is shown(I also do some mapping and that's why
collectAsState
is not enough)
c
It doesn’t need to be so complicated, see this for an example ViewModel class, showing how to connect it up to live within the Composition’s CoroutineScope. https://kotlinlang.slack.com/archives/C01D6HTPATV/p1644259697620799?thread_ts=1644257413.142249&amp;cid=C01D6HTPATV And if you want to move the VM creation into Kodein, you just need to set up the binding as a Factory, where you pass in the
rememberCoroutineScope()
to Kodein https://kodein.org/Kodein-DI/?6.3/core#_factory_binding
d
I can now understand the problem @hfhbd, when the screen is in the backstack, the scope is canceled and I have to recreate the viewmodel and rerun methods when going back. Looks like I need a navigation component first and then implement the scope, and cancel the scope from the navigation logic. Thank you, is there a navigation library good for that?
@Casey Brooks that looks good, but I don't know what to do when I inject repositories in that
ViewModel
c
It’s just a class, you’d do the same thing you’d do with them on Android, inject the repository through the constructor, and use them within the
viewModelScope.launch { }
blocks
Copy code
interface SomeRepository {
    suspend fun getSavedCount(): Int
    suspend fun saveCount(count: Int)
}

class MyViewModel(
    val viewModelScope: CoroutineScope, // passed from Compose through Kodein's Factory
    val repository: SomeRepository, // passed normally from Kodein
) {
    data class State(
        val count: Int = 0
    )

    private val _state = MutableStateFlow(State())
    val state: StateFlow<State> get() = _state.asStateFlow()

    fun initialize() {
        viewModelScope.launch {
            val savedCount = repository.getSavedCount()
            _state.update { it.copy(count = savedCount) }
        }
    }

    fun increment() {
        viewModelScope.launch {
            val newValue = _state.updateAndGet { it.copy(count = it.count + 1) }
            repository.saveCount(newValue.count)
        }
    }

    fun decrement() {
        viewModelScope.launch {
            val newValue = _state.updateAndGet { it.copy(count = it.count - 1) }
            repository.saveCount(newValue.count)
        }
    }
}

val kodein = Kodein {
    bind<MyViewModel>() with factory { viewModelScope: CoroutineScope -> MyViewModel(viewModelScope, instance()) }
}
d
wait, where does
viewModelScope
come from?
c
It gets passed in from the composition. By setting up the ViewModel’s binding in Kodein with
factory
, you can pass some params in when you retrieve it (like Dagger’s
@AssistedInject
). So the
viewModelScope
gets passed in from the composition, while its other dependencies come from the Kodein container
Copy code
kotlin
@Composable
fun MyApp() {
    val viewModelScope = rememberCoroutineScope()
    val viewModel: MyViewModel = remember(viewModelScope) { kodein.direct.instance(arg = viewModelScope) }
    val vmState by viewModel.state.collectAsState()

    MyAppUi(
        state = vmState,
        onIncrement = { viewModel.increment() },
        onDecrement = { viewModel.decrement() },
    )
}
👍🏻 1