如何编写一个用例从 Android 框架中检索数据

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

我正在将一个应用程序迁移到 MVVM 和干净的架构,但我遗漏了这个难题的一部分。

问题域:

列出设备上的所有应用程序并显示在片段中/Activity

设备应用程序由其包名称表示:

data class DeviceApp(val packageName: String)

这是列出设备应用程序的方式:

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
}

据我了解,调用 listAllApplications() 应该在 'Domain Layer' 内的 UseCase 中完成,它由 ViewModel.

调用

然而 listAllApplications 收到 Context,领域层应该只是纯代码。

在干净的架构/MVVM 中,我应该放在哪一层listAllApplications(context)

更一般地说,ViewModel 应该如何与需要 Context(位置等)的 Android 框架 API 交互?

Domain Layer should be plain code only.

正确!,但我认为它部分正确。现在考虑您的场景,您需要 context 在域级别。您不应该 context 在域级别,但根据您的需要,您应该选择其他架构模式或将其视为您正在执行此操作的例外情况。

考虑到您在域中使用上下文,尽管 activity context,您应该始终使用 applicationContext,因为之前的过程会持续存在。

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

每当您在 ViewModel 需要 Context 时,您可以从 UI 提供它作为方法参数 (即 viewModel.getLocation(context) 或者使用AndroidViewModel 作为 class 的父级 ViewModel (它提供 getApplication() public 方法通过 ViewModel 访问上下文) .

我想指出的是,确保你不会不小心在 ViewModel/Domain 层内全局持有任何 View/Context,因为它可能会造成内存泄漏或更糟的崩溃等灾难.

你可以用依赖注入非常干净地解决这个问题。如果您还没有使用 DI,您可能想要使用,因为它会大大简化您的清洁架构工作。

以下是我如何使用 Koin 进行 DI。

首先,将您的用例从函数转换为 class。这允许构造函数注入:

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

您现在在用例中引用了 context。伟大的!我们稍后会处理实际提供上下文的值。

现在您在想...但是用例不就是为了使用可重用的函数吗?用例 class 是什么人?

我们可以利用 operator fun 的奇迹来帮助我们。

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 是一个特殊的函数,它允许 class 的实例被调用,就好像它是一个函数一样。它有效地将我们的 class 转换为具有可注入构造函数的函数

这允许我们继续使用标准函数调用语法在 ViewModel 中调用我们的用例:

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

请注意,我们的 ListAllApplications 用例也被注入到 MyViewModel 的构造函数中,这意味着 ViewModel 仍然完全不知道 Context.

拼图的最后一块是将所有这些注入与 Koin 连接在一起:

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

如果您以前从未使用过 Koin,请不要担心,其他 DI 库可以让您做类似的事情。关键是你的 ListAllApplications 实例是由 Koin 构建的,它提供了 ContextandroidContext() 的实例。您的 MyViewModel 实例也由 Koin 构建,它为 ListAllApplications 的实例提供了 get().

最后你将 MyViewModel 注入到使用它的 Activity/Fragment 中。使用 Koin 就这么简单:

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

Et Voilà!