https://kotlinlang.org logo
#ballast
Title
# ballast
u

ubuntudroid

11/03/2023, 10:01 AM
Is it possible to set a new event handler of an
AndroidViewModel
? I’m trying
attachEventHandler()
, but still only the previous handler is called. Background: I’m building with Compose and keep the ViewModel around longer than the screen, and I pass a
SnackbarHostState
to the EventHandler to display snackbars. That also means that I need to regularly update the SnackbarHostState to match the currently used one and thus also the EventHandler.
c

Casey Brooks

11/03/2023, 2:09 PM
You should be able to pass in a CoroutineScope to control the lifetime of the EventHandler. By default, it runs on the viewModelScope, but you can pass in a scope from
rememberCoroutineScope()
to only run the EventHandler when that UI is shown, for example
u

ubuntudroid

11/03/2023, 2:58 PM
Yes, I can pass a scope, but in this case this doesn’t help, because as the UI changes (and the ViewModel lives longer than the Screen) I also need to create a new handler as the handler holds a reference to the
SnackbarHostState
. An alternative, that just came to my mind, would be to pass the SnackbarHostState not via constructor, but via a public property and then use a
LaunchedEffect
to update the property whenever the composition changes. 🤔 One might run into subtle timing race conditions that way though, especially when trying to send stuff to the snackbar shortly before or during a recomposition.
Is it generally the right approach to pass the
SnackbarHostState
or a
Router
to the event handler? Or is that too tight coupling?
c

Casey Brooks

11/03/2023, 3:24 PM
You can pass the SnackbarHostState/Router directly to the InputHandler, but in general it’s better to handle that in the EventHandler to reduce coupling. That way you can have the VM send a generic navigation request or show a Snackbar message by sending an Event, and in tests you can swap out the EventHandler so you don’t need to actually depend on Compose or Navigation APIs in your tests. And just to be clear, something like this doesn’t seem to be working for you?
Copy code
@Composable
fun Content(router: Router<Screen>, vm: AndroidViewModel<
        ExampleContract.Inputs,
        ExampleContract.Events,
        ExampleContract.State,
        >) {
    val snackbarHostState = remember { SnackbarHostState() }
    
    LaunchedEffect(vm, router, snackbarHostState) {
        vm.attachEventHandler(this, ExampleEventHandler(router, snackbarHostState))
    }
    
    val uiState by vm.observeStates().collectAsState()
    
    Content(uiState, snackbarHostState) { vm.trySend(it) }
}
u

ubuntudroid

11/03/2023, 3:49 PM
No, unfortunately not - it looks like
attachEventHandler()
doesn’t really replace the event handler if there already is one in place. If I set a breakpoint into the event handler it always seems to be the “old” one with access to the “old”
SnackbarHostState
.
c

Casey Brooks

11/03/2023, 4:04 PM
Hmm, that’s strange. The EventHandler only really exists within the defined coroutineScope, it’s not really tightly coupled to the VM. Essentially,
attachEventHandler()
just collects a channel as a Flow within the CoroutineScope, and when the coroutineScope gets cancelled the EventHandler goes away. The issues with both this and the debugger issue are making me think that something in your setup is keeping CoroutineScopes active longer than they should be.
u

ubuntudroid

11/03/2023, 4:18 PM
ah, yes, the coroutine scope is the one from a wrapping Jetpack ViewModel. So my ViewModel isn’t as ephemereal as in your examples and docs. It’s basically a Jetpack ViewModel which forwards events and inputs etc. to the Ballast View Model which it holds as a property. The idea was, that I can keep the ViewModel around for configuration changes and process restoration and store state in it and still benefit from the nice MVI features a BallastViewModel provides. I hope this makes sense. 😅
c

Casey Brooks

11/03/2023, 4:22 PM
Yes, and that’s what I was trying to show with the snippet above. The VM is passed in (because it’s being managed by something that lives longer than that single
Content
composable function). But you want the EventHandler to run only within that Content function’s scope, so you need to create a new EventHandler and attach it to the VM on a coroutineScope that is not the same as the VM’s, or derived from it. In the snippet, that shorter-lived CoroutineScope comes from a
LaunchedEffect
, which should collect and process Events only as long as the
Content
function is in the Composition. Once you leave that screen, the VM doesn’t necessarily get cleared (since it was passed in from above and isn’t scoped to the screen), but the EventHandler does get cleared since it is scoped to the screen.
u

ubuntudroid

11/03/2023, 4:30 PM
Yeah, I got you, in theory that makes sense and I did more or less the same in my code. But it doesn’t seem to be how it works IRL for whatever reason. 😉 Anyway, I don’t want to keep you busy with this (although I very much appreciate your thorough responses here 🙏) - I’ll play around with the code a bit more and get back if/when I find a solution to this problem in my configuration.
c

Casey Brooks

11/03/2023, 4:32 PM
Also, if you want to use your own Jetpack ViewModel as the base class, yes you can definitely have that be a host to a Ballast VM, but I’d recommend looking at the source for how Ballast does this, rather than having it host a
BasicViewModel
. You can’t swap the EventHandler on a
BasicViewModel
(though maybe one should be able to. might be something for me to look into). Essentially,
BallastViewModelImpl
is the actual Ballast VM you should be embedding. All the other VM types provided by Ballast (AndroidViewModel, BasicViewModel, etc) just wrap that
u

ubuntudroid

11/03/2023, 4:34 PM
Good catch, right now I am using
AndroidViewModel
because in contrast to
BasicViewModel
it had the
attachEventHandler()
function. I’ll check whether
BallastViewModelImpl
can help.
3 Views