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?