BennyP
BennyP

Reputation: 1807

How to write a use case that retrieves data from Android framework with Context

I am migrating an application to MVVM and clean architecture, and I am missing one part of the puzzle.

The problem domain:

List all applications on device and display them in the Fragment / Activity

A device app is represented by its package name:

data class DeviceApp(val packageName: String)

This is how the device apps are listed:

private fun listAllApplications(context: Context): List<DeviceApp> {
    val ans = mutableListOf<DeviceApp>()

    val packageManager: PackageManager = context.applicationContext.packageManager
    val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
    for (applicationInfo in packages) {
        val packageName = applicationInfo.packageName
        ans.add(DeviceApp(packageName))
    }

    return ans
}

As I understand, calling listAllApplications() should be done in a UseCase inside the 'Domain Layer', which is called by a ViewModel.

However listAllApplications receives a Context, and the Domain Layer should be plain code only.

In clean architecture / MVVM, in which layer should I put listAllApplications(context)?

And more generally, how should the ViewModel interact with Android framework APIs that require Context (location, etc.)?

Upvotes: 5

Views: 4906

Answers (2)

Luke Needham
Luke Needham

Reputation: 4019

You can solve this problem very cleanly with dependency-injection. If you aren't already using DI, you probably want to be, as it will greatly simplify your clean-architecture endeavours.

Here's how I'd do this with Koin for DI.

First, convert your usecase from a function to a class. This allows for constructor injection:

class ListAllApplications(private val context: Context) {
  ...
}

You now have a reference to context inside your usecase. Great! We'll deal with actually providing the value of context in a moment.

Now you're thinking... but aren't usecases meant to use reusable functions? What's the guy on about with usecases being classes?

We can leverage the miracle that is operator funs to help us here.

class ListAllApplications(private val context: Context) {
  operator fun invoke(): List<DeviceApp> {
    val ans = mutableListOf<DeviceApp>()

    val packageManager: PackageManager = context.applicationContext.packageManager
    val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
    for (applicationInfo in packages) {
        val packageName = applicationInfo.packageName
        ans.add(DeviceApp(packageName))
    }

    return ans
  }
}

invoke is a special function which allows an instance of a class to be invoked as if it were a function. It effectively transforms our class into a function with an injectable constructor 🤯

And this allows us to continue to invoke our usecase in the ViewModel with the standard function invocation syntax:

class MyViewModel(private val listAllApplications: ListAllApplications): ViewModel {
  init {
    val res = listAllApplications()
  }
}

Notice that our ListAllApplications usecase is also being injected into the constructor of MyViewModel, meaning that the ViewModel remains entirely unaware of Context.

The final piece of the puzzle is wiring all this injection together with Koin:

object KoinModule {
  private val useCases = module {
    single { ListAllApplications(androidContext()) }
  }
  private val viewModels = module {
    viewModel { MyViewModel(get()) }
  }
}

Don't worry if you've never used Koin before, other DI libraries will let you do similar things. The key is that your ListAllApplications instance is being constructed by Koin, which provides an instance of Context with androidContext(). Your MyViewModel instance is also being constructed by Koin, which provides the instance of ListAllApplications with get().

Finally you inject MyViewModel into the Activity/Fragment which uses it. With Koin that's as simple as:

class MyFragment : Fragment {
  private val viewModel: MyViewModel by viewModel()
}

Et Voilà!

Upvotes: 3

Jeel Vankhede
Jeel Vankhede

Reputation: 12138

Domain Layer should be plain code only.

That's correct!, but in my opinion it's partially correct. Now considering your scenario you need context at domain level. You shouldn't have context at domain level but in your need you should either choose other architecture pattern or consider it as exceptional case that you're doing this.

Considering you're using context at domain, you should always use applicationContext in spite of activity context, because earlier persists through out process.

How should the ViewModel interact with android framework APIs that require Context (location, etc.)?

Whenever you need Context at ViewModel either you can provide it from UI as method parameter (I.e. viewModel.getLocation(context)) or else use AndroidViewModel as your parent class for ViewModel (it provides getApplication() public method to access context through out ViewModel).

All I would like to point you out is that make sure you don't accidentally hold any View/Context globally inside ViewModel/Domain Layer, because it can make catastrophe like memory leaking or crashes at worse.

Upvotes: 3

Related Questions