Reputation: 1807
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
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 fun
s 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
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