StateFlows of Extension Lambdas

Hi, I’ve written something, and I can’t tell if it’s awful or beautiful.
Please help me identify if the following is a perfect storm of Kotlin features or an anti-pattern in the making.

We all know that data flow in an MVVM architectured application should be unidirectional. The View should be able to access the ViewModel, but the ViewModel should have no knowledge of the View. But sometimes you need to access the Context for things such as navigation or acquiring a resource.

The other day, I wrote this in my ViewModel.

private val _command: MutableStateFlow<MyActivity.() -> Unit> = MutableStateFlow { }
val command: StateFlow<MyActivity.() -> Unit> = _command

private suspend fun someFunction() {
    _command.emit { // this: MyActivity
        // access public members of MyActivity to do something e.g.
        onBackPressed()
    }
}

Then, in MyActivity, I have a Coroutine block:

lifecycleScope.launch(dispatcher.default) { // dispatcher is injected via Hilt
    viewModel.command
        .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
        .collectLatest { lambda -> this@MyActivity.apply { lambda() } }
}

So what does this do? Well, the ViewModel can now emit a lambda function which is run from the scope of MyActivity, which gives the contents of the lambda access to all non-private members of MyActivity, despite being in the ViewModel class. This means that custom activity functions, navigation, resource lookups, etc are all doable without holding a permanent reference or risking a context leak.

The ViewModel remains blind to the Activity instance, except for defining the type for the extension function. If MyActivity exists and is listening, it runs the lambda when it changes. If not, nothing happens. Since MyActivity is only reading from the ViewModel, this maintains the unidirectional flow.

My main concern then would be the type. It’s not instance data, but if you wanted to share the ViewModel across multiple Views, a generic definition or interface defining the accessible members would need to be used.

This still feels wrong though. Thoughts?

1 Like

It definitely feels strange :wink:

I’m not the Android developer, so I don’t know if ViewModel could sometimes access the Context or it shouldn’t do this. But if you want to pass it from an activity then why you don’t simply create a setContext() function in ViewModel and just call it directly? This is not less unidirectional than your above solution. Flows and lambdas only make things more complicated and obscure, but they don’t change the flow of data.

1 Like

But sometimes you need to access the Context for things such as navigation or acquiring a resource.

Depending on Context is very different than depending on MyActivity. It’s not just about owning a reference, it’s about avoiding spaghetti code where everything has access to everything so it’s very easy to create classes that are confusing because they do a little of everything. You are very likely to end up with View responsibilities directly in your ViewModel.(btw, I never depend on Context in a ViewModel I always abstract what’s needed behind an interface. A class with a Context can do almost anything so keeping to a single responsibility is harder)

Well, the ViewModel can now emit a lambda function

This can be problematic. Lambda’s are completely opaque so you can’t filter based on data or use any other useful Flow operators. It’s hard to tell what references might be captured in the closure. Even though MyActivity won’t be doing anything but running the lambda, it still sets you up to need to change you code quite a bit if you suddenly need to add filtering internally in your view model. I’d recommend generally sticking to just passing immutable data through your Flows.

Also, because you are emitting through a MutableStateFlow, you lose the automatic lifespan functionalities inherent in Flow. Consider View code that comes and go while your Activity is up. While it’s gone, any resources it depends on can be freed. This is implicit when you depend on a Flow that is sourced from say a Room database generated Flow or a callbackFlow. But if you pipe all functionality through a single MutableStatFlow, then everything has to be active from the entire time the Activity is resumed.

My main concern then would be the type.

That would help a lot, but this ends up looking more like an awkward version of MVP (where presenter depends on interface abstraction of view) and you aren’t really following MVVM at all. It’s not really any different from actually having direct ownership of the Activity with a member property and just setting/unsetting the reference in onResume/onPause.

1 Like

I’ve played around with something similar for sending events from a view model to a view. A StateFlow is problematic because it will still contain its value if the Activity is destroyed and recreated. So when the Activity is created again it will run the lambda again. (I’ve looked at SharedFlows and they don’t help in this case.) You need a method on the view model that the view calls once it executes the lambda to tell the view model to set the StateFlow’s value to null.

And definitely define an interface with the methods your lambda can call.