如何在Android MVVM ViewModel中获取Context

Vin*_*ams 43 android mvvm android-context dagger-2

我试图在我的Android应用程序中实现MVVM模式.我已经读过ViewModels不应该包含任何特定于Android的代码(以使测试更容易),但是我需要使用上下文来处理各种事情(从xml获取资源,初始化首选项等).做这个的最好方式是什么?我看到它AndroidViewModel有一个对应用程序上下文的引用,但是它包含特定于android的代码,因此我不确定它是否应该在ViewModel中.这些也与Activity生命周期事件有关,但我使用匕首来管理组件的范围,所以我不确定这会如何影响它.我是MVVM模式和Dagger的新手,所以任何帮助都表示赞赏!

小智 32

您可以使用Application它通过提供的上下文AndroidViewModel,你应该继承AndroidViewModel这简直是一个ViewModel包含一个Application参考.

  • 但是使用“AndroidViewModel”是一个好的做法吗?使用时需要注意什么以避免内存泄漏或不一致? (7认同)

Jac*_*key 23

并不是ViewModels不应该包含Android特定的代码来使测试更容易,因为它是使测试更容易的抽象.

ViewModel之所以不应该包含Context的实例或类似于上下文的其他对象的原因是因为它具有与活动和片段不同的生命周期.

我的意思是,假设您在应用上进行了轮换更改.这会导致您的Activity和Fragment自行销毁,因此它会重新创建.ViewModel意味着在此状态期间保持不变,因此如果它仍然在被破坏的Activity中持有View或Context,则可能会发生崩溃和其他异常.

至于你应该怎么做你想做的事情,MVVM和ViewModel与JetPack的数据绑定组件配合得非常好.对于大多数情况,您通常会存储String,int或etc等,您可以使用Databinding使Views直接显示它,因此不需要将值存储在ViewModel中.

但是如果你不想要Databinding,你仍然可以在构造函数或方法中传递Context来访问Resources.只是不要在ViewModel中包含该Context的实例.

  • 如果我想根据值形式视图模型切换文本视图中的文本,字符串需要本地化,所以我需要在我的视图模型中获取资源,没有上下文我将如何访问资源? (3认同)
  • @SrishtiRoy 如果您使用数据绑定,则可以根据视图模型中的值轻松切换 TextView 的文本。无需访问 ViewModel 中的 Context,因为所有这些都发生在布局文件中。但是,如果您必须在 ViewModel 中使用 Context,那么您应该考虑使用 AndroidViewModel 而不是 ViewModel。AndroidViewModel 包含您可以使用 getApplication() 调用的应用程序上下文,因此如果您的 ViewModel 需要上下文,它应该可以满足您的上下文需求。 (3认同)
  • @Pacerier您误解了 ViewModel 的主要目的。这是一个关注点分离问题。ViewModel 不应保留对任何视图的引用,因为它的职责是维护视图层显示的数据。UI 组件(又名视图)由视图层维护,Android 系统将在必要时重新创建视图。保留对旧视图的引用将与此行为发生冲突并导致内存泄漏。 (2认同)

Iva*_*sov 14

正如其他人所提到的AndroidViewModel,您可以从中获取应用程序,Context但从我在评论中收集的内容来看,您正试图@drawable从内部操作s,ViewModel这违背了 MVVM 的目的。

一般来说,需要Context在您的ViewModel几乎普遍存在的情况下建议您应该考虑重新考虑如何划分Views 和ViewModels.

与其ViewModel解析 drawable 并将它们提供给 Activity/Fragment,不如考虑让 Fragment/Activity 根据ViewModel. 比如说,您需要在视图中显示不同的可绘制对象以进行开/关状态——它ViewModel应该保持(可能是布尔值)状态,但相应View地选择可绘制对象是其业务。

数据绑定使它变得非常简单:

<ImageView
...
app:src="@{viewModel.isOn ? @drawable/switch_on : @drawable/switch_off}"
/>
Run Code Online (Sandbox Code Playgroud)

如果您有更多状态和可绘制对象,为了避免布局文件中的笨拙逻辑,您可以编写一个自定义BindingAdapter来将Enum值转换为R.drawable.*引用,例如:

enum class CatType { NYAN, GRUMPY, LOL }

class CatViewModel {
    val catType: LiveData<CatType> = ...
Run Code Online (Sandbox Code Playgroud)
// View-tier logic, takes the burden of knowing
// Contexts and R.** refs from the ViewModel
@BindingAdapter("bindCatImage")
fun bindCatImage(view: ImageView, catType: CatType) = view.apply {
    val resource = when (value) {
        CatType.NYAN -> R.drawable.cat_nyan
        CatType.GRUMPY -> R.drawable.cat_grumpy
        CatType.LOL -> R.drawable.cat_lol
    }
    setImageResource(resource)
}
Run Code Online (Sandbox Code Playgroud)
<ImageView
    bindCatType="@{vm.catType}"
    ... />
Run Code Online (Sandbox Code Playgroud)

如果您需要您的-- 中使用的Context某些组件,请在ViewModel外部创建该组件ViewModel并将其传入。您可以使用 DI 或单例,或Context在初始化ViewModelin Fragment/之前创建依赖组件Activity

何苦

Context是 Android 特定的东西,并且在ViewModels 中依赖它对于单元测试来说是笨拙的(当然,您可以将其AndroidJunitRunner用于特定于 android 的东西,但是拥有更清晰的代码而没有额外的依赖是有意义的)。如果您不依赖Context,则为ViewModel测试模拟所有内容会更容易。因此,经验法则是:除非有充分的理由,否则不要Context在 ViewModel 中使用。


dev*_*jay 13

对于Android体系结构组件视图模型,

将您的活动上下文传递给活动的ViewModel作为内存泄漏不是一个好习惯。

因此,要在您的ViewModel中获取上下文,ViewModel类应扩展Android View Model类。这样,您可以获取上下文,如下面的示例代码所示。

class ActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val context = getApplication<Application>().applicationContext

    //... ViewModel methods 

}
Run Code Online (Sandbox Code Playgroud)

  • 为什么不直接使用应用程序参数和普通的 ViewModel 呢?我认为“getApplication&lt;Application&gt;()”没有任何意义。它只是添加了样板文件。 (7认同)
  • 哦,我明白了,因为活动比其视图模型更频繁地被破坏(例如,当屏幕旋转时)。不幸的是,垃圾回收不会释放内存,因为视图模型仍然有对它的引用。 (4认同)
  • 快速提问:我们可以只使用变量“application”。使用“getApplication&lt;Application&gt;()”而不是使用传递给 ActivityViewModel 的“application”有什么意义吗?事实上,无论如何,它们都是同一个应用程序。 (3认同)
  • 为什么会出现内存泄漏呢? (2认同)

小智 12

在希尔特:

@Inject constructor(@ApplicationContext context : Context) 
Run Code Online (Sandbox Code Playgroud)


Vin*_*ams 11

我最终做的不是直接在ViewModel中使用Context,而是创建了像ResourceProvider这样的提供程序类,它可以为我提供所需的资源,并且我将这些提供程序类注入到我的ViewModel中

  • @IgorGanapolsky 虚拟机并不完全是域逻辑。领域逻辑是其他类,例如交互器和存储库等。虚拟机属于“粘合剂”类别,因为它们确实与您的域交互,但不是直接交互。如果您的虚拟机是您域的一部分,那么您应该重新考虑如何使用该模式,因为您赋予了它们太多的责任。 (2认同)

4gu*_*71n 9

TL;DR:通过 Dagger 在您的 ViewModel 中注入应用程序的上下文并使用它来加载资源。如果您需要加载图像,请通过数据绑定方法的参数传递 View 实例并使用该 View 上下文。

MVVM 是一个很好的架构,它绝对是 Android 开发的未来,但有一些东西仍然是绿色的。以 MVVM 架构中的层通信为例,我见过不同的开发人员(非常知名的开发人员)使用 LiveData 以不同的方式来通信不同的层。其中一些使用 LiveData 将 ViewModel 与 UI 进行通信,但随后它们使用回调接口与存储库进行通信,或者它们具有交互器/用例并使用 LiveData 与它们进行通信。点这里,是不是一切都100%的定义还没有

话虽如此,我解决您的具体问题的方法是通过 DI 提供应用程序的上下文,以便在我的 ViewModel 中使用,以从我的 strings.xml 中获取诸如 String 之类的东西

如果我正在处理图像加载,我会尝试从数据绑定适配器方法传递 View 对象并使用 View 的上下文来加载图像。为什么?因为如果您使用应用程序的上下文加载图像,某些技术(例如 Glide)可能会遇到问题。

希望能帮助到你!

  • TL;DR 应该位于顶部 (6认同)

Ale*_*ola 7

您可以getApplication().getApplicationContext()从ViewModel中访问应用程序上下文。这是访问资源,首选项等所需的。

  • ViewModel类没有getApplication方法。 (6认同)
  • 拥有应用程序上下文并不是什么大问题。您不想拥有活动/片段上下文,因为如果片段/活动被破坏并且视图模型仍然引用现在不存在的上下文,您就会感到无聊。但是你永远不会销毁应用程序上下文,但虚拟机仍然有对它的引用。正确的?您能想象一下您的应用程序退出但 Viewmodel 没有退出的场景吗?:) (3认同)
  • 不,但是`AndroidViewModel`可以 (2认同)

hum*_*olf 6

简短答案-不要这样做

为什么呢

它破坏了视图模型的全部目的

通过使用LiveData实例和各种其他推荐的方法,您几乎可以在视图模型中做的所有事情都可以在活动/片段中完成。

  • 那么为什么 AndroidViewModel 类还存在呢? (41认同)
  • @AlexBerdnikov 在视图模型中使用 Activity 上下文可能会导致内存泄漏。因此,通过使用 AndroidViewModel 类,您将获得应用程序上下文,这不会(希望)导致任何内存泄漏。因此,使用 AndroidViewModel 可能比向其传递活动上下文更好。但仍然这样做会使测试变得困难。这是我的看法。 (8认同)
  • @free_style 感谢您的澄清,但问题仍然存在:如果我们不能在 ViewModel 中保留上下文,为什么 AndroidViewModel 类甚至存在?它的全部目的是提供应用程序上下文,不是吗? (5认同)
  • @AlexBerdnikov MVVM 的目的是将视图(Activity/Fragment)与 ViewModel 隔离,甚至比 MVP 还要隔离。这样测试起来会更容易。 (2认同)
  • 我无法从存储库访问 res/raw 文件夹中的文件? (2认同)

小智 6

这是一种将 Context 引入 ViewModel 的方法

private val context = getApplication<Application>().applicationContext
Run Code Online (Sandbox Code Playgroud)


Epi*_*rce 5

具有对应用程序上下文的引用,但是它包含特定于 android 的代码

好消息,你可以使用 Mockito.mock(Context.class)在测试中并使上下文返回您想要的任何内容!

因此,只需ViewModel像往常一样使用 a ,并像往常一样通过 ViewModelProviders.Factory 为其提供 ApplicationContext 。