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

2

5

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.)?

Hut answered 28/1, 2020 at 10:16 Comment(0)
A
3

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.

Aerodynamics answered 28/1, 2020 at 10:34 Comment(3)
I feel that this shouldn't be an exceptional case, a lot of applications of not ALL need to access android framework with context, examples are endless. I hoped/figured that there is an architectural guideline of how to do this correctly. this is the architecture as i see it: view->viewModel->(fetch data with/) useCase->repository->Entity where usecase is plain code in domain that serves as single responsibility. When I want to to do one thing the usecase will handle it for me, but i dont see a way to avoid context therefor what would be a good architectural pattern to do this?Hut
Basically your domain should not have context in first place, your repository should have that followed by view model. That's why I provided two pointers with different scenarios. First regarding domain stuff is partially acceptable other than second one.Aerodynamics
Also note that domain should have code that is independent of platform (So that can be reused across platforms like business logic abstraction), in here context is purely dependent of android framework and can not be at domain, so you should have this implementation at repository or view model (app) level.Aerodynamics
L
3

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à!

Ludovico answered 24/3, 2021 at 16:59 Comment(4)
Great ! But how can you test your viewModel in a unitTest where there isn't context ?Zaporozhye
You'll need to mock the ListAllApplications usecase to provide an implementation that doesn't require Context. No change is necessary to MyViewModel, you just need to inject the MockListAllApplications instance to MyViewModel in your DILudovico
Isn't there a memory leak storing Context in the use case class and saving it in the ViewModel?Crawford
The Context you inject into the UseCase should be the Context of the Application. This is what Koin does with androidContext(). When you use the Application Context you shouldn't introduce any memory leak, because the Application instance will already live for as long as the UseCase instance lives. Indeed, if you were using the Context of a specific Activity, you would create a leak, but using Application context in this way is safeLudovico

© 2022 - 2024 — McMap. All rights reserved.