1

I'm transitioning from iOS SWIFT to Android Kotlin & Jetpack, and struggling a little with @composables, I'm not using activity/fragments. I'm creating separate ViewModel classes to hoist state & business logic but I'm struggling with context.

Within the ViewModel's there's a number of functions that require context, example registerReceiver, I'm also using a number of libraries such as MQTT client. From my research I shouldn't be using context in a ViewModel:

How to get Context in Android MVVM ViewModel

So my question is how do create ViewModel that requires context for dependent functions, or is my understanding of context or ViewModel wrong?

I know it's not recommended but for a few features I've created functions in the ViewModel which the composable calls and pass context = LocalContext.current

As an example within the ViewModel I've created the following:

fun registerScannerReceiver(context: Context) {
        val filter = IntentFilter(context.getString(R.string.datawedge_intent_filter_action))
        filter.addCategory(Intent.CATEGORY_DEFAULT)
        context.registerReceiver(reciver, filter)
    }

This works but it feels wrong any suggestions, recommendations or example code would be greatly appreciated.

Duncan Hill
  • 547
  • 2
  • 5
  • 16
  • https://developer.android.com/reference/androidx/lifecycle/AndroidViewModel Use this and get the context from the Application. Application is effectively a singleton and cannot be leaked so it’s safe for the ViewModel to hold a reference to it. – Tenfour04 Mar 05 '23 at 23:54

1 Answers1

0

You should never pass a Context from a Composable to a ViewModel. The ViewModel could outlive the Composable, resulting in a memory leak or an app crash. Here is the direct quote from the Android Team:

Caution: A ViewModel usually shouldn't reference a view, Lifecycle, or any class that may hold a reference to the activity context. Because the ViewModel lifecycle is larger than the UI's, holding a lifecycle-related API in the ViewModel could cause memory leaks.

There are several other ways you can solve it:

  1. If your Composable will only interact with one ViewModel, try passing the ViewModel as an argument to it from the higher level Composable.
@Composable
fun SubComponent(
    modifier: Modifier = Modifier,
    viewModel: MyViewModel,
) {...}
  1. If you're working with a Composable that supports multiple other ViewModels, you could pass an Application variable. Any context derived from the Application will have longer lifecycle, resulting in fewer crashes. I don't recommend this option, as it will still leak a Context object.
class MyViewModel(private val application: Application) : ViewModel() {
private val leakyContext = application.applicationContext as Context
...
}
  1. Don't use a ViewModel to build Intent. Separate it to a utility type class, so you can reuse it across your application. Call your newly created class from the Composable
class Util {
fun callIntent(context: Context, actionString: String) {
        val filter = IntentFilter(actionString)
        filter.addCategory(Intent.CATEGORY_DEFAULT)
        context.registerReceiver(receiver, filter)
        ...
    }
...
}

@Composable
fun SubComponent(
    modifier: Modifier = Modifier,
    viewModel: MyViewModel,
    util: Util = Util()
) {
private val context = LocalContext.current
util.callIntent(context,context.getString(R.string.datawedge_intent_filter_action))
...
}

I hope this helps explain the issues.

  • Robert thank you so much for your detailed reply it's helped a lot and I've refactored my application and it does feel better and cleaner. – Duncan Hill Mar 08 '23 at 18:51