如何在 Android MVVM ViewModel 中获取上下文

Posted

技术标签:

【中文标题】如何在 Android MVVM ViewModel 中获取上下文【英文标题】:How to get Context in Android MVVM ViewModel 【发布时间】:2018-12-29 07:31:25 【问题描述】:

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

【问题讨论】:

以防万一有人尝试使用AndroidViewModel但得到Cannot create instance exception,那么您可以参考我的这个答案***.com/a/62626408/1055241 你不应该在 ViewModel 中使用 Context,而是创建一个 UseCase 来获取 Context @RubenCaster 你有任何示例或 GitHub 链接吗? @Parmesh 不,抱歉。它是一个私人项目 =( 【参考方案1】:

您可以使用由AndroidViewModel 提供的Application 上下文,您应该扩展AndroidViewModel,它只是一个包含Application 引用的ViewModel

【讨论】:

但是使用AndroidViewModel 是个好习惯吗?使用时需要注意什么以避免内存泄漏或不一致?【参考方案2】:

对于 Android 架构组件视图模型,

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

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

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

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

    //... ViewModel methods 


【讨论】:

为什么不直接使用应用参数和普通的ViewModel呢?我认为“getApplication()”没有任何意义。它只是添加样板。 为什么会是内存泄漏? 哦,我明白了,因为活动会比其视图模型更频繁地被销毁(例如,当屏幕旋转时)。不幸的是,垃圾回收不会释放内存,因为视图模型仍然有对它的引用。 快速提问:我们可以只使用变量application。使用getApplication&lt;Application&gt;() 而不是使用传递给ActivityViewModel 的application 有什么意义吗?实际上它们都是同一个应用程序。 @TheincredibleJan 我试过了,但它不起作用。不知何故无法实例化 ViewModel。但如果我们使用 AndroidViewModel 而不是 ViewModel,它就可以工作。如果我们使用 ViewModel,我猜ViewModelProvider 的依赖注入不起作用。【参考方案3】:

并不是说 ViewModel 不应该包含特定于 Android 的代码以使测试更容易,因为它是使测试更容易的抽象。

ViewModel 不应包含 Context 的实例或任何类似 View 或其他持有 Context 的对象的原因是因为它具有与活动和片段不同的生命周期。

我的意思是,假设您对应用进行了轮换更改。这会导致您的 Activity 和 Fragment 自行销毁,以便重新创建自己。 ViewModel 旨在在此状态下持续存在,因此如果它仍然持有已销毁 Activity 的视图或上下文,则可能会发生崩溃和其他异常。

至于你应该如何做你想做的事,MVVM 和 ViewModel 与 JetPack 的 Databinding 组件配合得非常好。 对于您通常会存储 String、int 等的大多数事情,您可以使用 Databinding 使 View 直接显示它,因此不需要将值存储在 ViewModel 中。

但如果您不想要数据绑定,您仍然可以在构造函数或方法中传递上下文来访问资源。只是不要在 ViewModel 中保存该上下文的实例。

【讨论】:

据我了解,包含特定于 android 的代码需要运行仪器测试,这比普通的 JUnit 测试要慢得多。我目前正在将数据绑定用于单击方法,但我看不出它如何帮助从 xml 获取资源或获取首选项。我刚刚意识到,对于偏好,我也需要在我的模型中添加一个上下文。我目前正在做的是让 Dagger 注入应用程序上下文(上下文模块从应用程序类中的静态方法获取它) @VincentWilliams 是的,使用 ViewModel 有助于将代码从 UI 组件中抽象出来,从而使您更容易进行测试。但是,我要说的是,不包括任何上下文、视图等的主要原因不是因为测试原因,而是因为 ViewModel 的生命周期可以帮助您避免崩溃和其他错误。至于数据绑定,这可以帮助您处理资源,因为您需要在代码中访问资源的大部分时间是由于需要将字符串、颜色、尺寸应用到您的布局中,而数据绑定可以直接完成。 如果我想根据值表单视图模型切换文本视图中的文本,字符串需要本地化,所以我需要在我的视图模型中获取资源,没有上下文我将如何访问资源? @SrishtiRoy 如果您使用数据绑定,则可以根据您的视图模型中的值轻松切换 TextView 的文本。无需访问 ViewModel 中的 Context,因为所有这些都发生在布局文件中。但是,如果您必须在 ViewModel 中使用 Context,那么您应该考虑使用 AndroidViewModel 而不是 ViewModel。 AndroidViewModel 包含您可以使用 getApplication() 调用的应用程序上下文,因此如果您的 ViewModel 需要上下文,它应该可以满足您的上下文需求。 @Pacerier 你误解了 ViewModel 的主要用途。这是一个关注点分离问题。 ViewModel 不应保留对任何视图的引用,因为它的职责是维护 View 层显示的数据。 UI 组件,也就是视图,由视图层维护,Android 系统将在必要时重新创建视图。保留对旧视图的引用会与此行为冲突并导致内存泄漏。【参考方案4】:

简答 - 不要这样做

为什么?

它违背了视图模型的全部目的

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

【讨论】:

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

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

【讨论】:

我在 AppModule 中将 ResourcesProvider 与 Dagger 一起使用。从 ResourcesProvider 或 AndroidViewModel 获取上下文的好方法是否更好地获取资源的上下文? @Vincent : 如何使用 resourceProvider 在 ViewModel 中获取 Drawable? @Vegeta 您可以在 ResourceProvider 类中添加类似 getDrawableRes(@DrawableRes int id) 的方法 这与 Clean Architecture 方法背道而驰,该方法指出框架依赖项不应跨界进入域逻辑 (ViewModels)。 @IgorGanapolsky VM 不完全是域逻辑。域逻辑是其他类,例如交互器和存储库等等。虚拟机属于“胶水”类别,因为它们确实与您的域交互,但不是直接交互。如果您的虚拟机是您域的一部分,那么您应该重新考虑如何使用该模式,因为您赋予它们太多责任。【参考方案6】:

正如其他人所提到的,您可以从 AndroidViewModel 获取应用程序 Context,但根据我在 cmets 中收集的信息,您正试图从您的 ViewModel 中操纵 @drawables违背了 MVVM 的目的。

一般来说,几乎普遍需要在ViewModel 中包含Context,这表明您应该考虑重新考虑如何在Views 和ViewModels 之间划分逻辑。

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

DataBinding 很容易:

<ImageView
...
app:src="@viewModel.isOn ? @drawable/switch_on : @drawable/switch_off"
/>

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

enum class CatType  NYAN, GRUMPY, LOL 

class CatViewModel 
    val catType: LiveData<CatType> = ...
// 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)

<ImageView
    bindCatType="@vm.catType"
    ... />

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

何必呢

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

【讨论】:

下午好。我想与您澄清一下,在不使用上下文的情况下从房间存储库中删除记录时如何实现确认显示?创建警报对话框时需要上下文。谢谢。 这是一个很好的答案 - 有没有办法将一些数据从活动或片段提供给我的绑定适配器?我有一些可绘制对象,但我使用上下文来获取它们,因此我在这里! @Chucky 你的意思是怎么样?我想您可以在布局中声明一个类型为android.graphics.drawable.Drawable 的变量,并在绑定对象上的片段/活动代码中手动设置它。但这听起来有点违背 ViewModel 的目的。如果这与您的视图的某些逻辑状态有关,我宁愿制作一个代表该状态的密封类结构并编写一个绑定器,将正确键入的状态映射到特定的可绘制对象。 @Tas 抱歉回复晚了。您可以公开显示警报的事件(例如,作为StateFlow&lt;Event&lt;String&gt;&gt;)并从您的片段中观察它——这样您就不必在虚拟机中拥有上下文。【参考方案7】:

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

MVVM 是一个很好的架构,它绝对是 Android 开发的未来,但有一些东西仍然是绿色的。以 MVVM 架构中的层通信为例,我见过不同的开发人员(非常知名的开发人员)使用 LiveData 以不同的方式通信不同的层。他们中的一些人使用 LiveData 将 ViewModel 与 UI 进行通信,但随后他们使用回调接口与 Repositories 进行通信,或者他们有 Interactors/UseCases 并使用 LiveData 与它们进行通信。这里的重点是,并非所有事情都是 100% 定义尚未

话虽如此,我针对您的特定问题的方法是通过 DI 提供应用程序的上下文,以在我的 ViewModel 中使用以从我的 strings.xml 中获取诸如 String 之类的内容

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

希望对你有帮助!

【讨论】:

TL;DR 应该在顶部 感谢您的回答。但是,如果你可以让你的 viewmodel 从 androidviewmodel 扩展并使用类本身提供的内置上下文,你为什么要使用 dagger 来注入上下文呢?特别是考虑到使 dagger 和 MVVM 一起工作的大量样板代码,其他解决方案似乎更加清晰。您对此有何看法?【参考方案8】:

引用了应用程序上下文,但其中包含特定于 android 的代码

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

所以只需像往常一样使用ViewModel,然后像往常一样通过 ViewModelProviders.Factory 为其提供 ApplicationContext。

【讨论】:

【参考方案9】:

您不应该在 ViewModel 中使用 Android 相关对象,因为使用 ViewModel 的目的是分离 java 代码和 Android 代码,以便您可以单独测试业务逻辑,并且您将拥有一个单独的 Android 组件层和你的业务逻辑和数据,你的 ViewModel 中不应该有上下文,因为它可能导致崩溃

【讨论】:

这是一个公平的观察,但一些后端库仍然需要应用程序上下文,例如 MediaStore。下面 4gus71n 的回答解释了如何妥协。 是的,您可以使用应用程序上下文,但不能使用活动上下文,因为应用程序上下文存在于整个应用程序生命周期中,而不是活动上下文,因为将活动上下文传递给任何异步进程都可能导致内存泄漏。我帖子中提到的 Context 是 Activity Context。但是您仍然应该注意不要将上下文传递给任何异步进程,即使它是应用程序上下文。【参考方案10】:

您可以从 ViewModel 中的 getApplication().getApplicationContext() 访问应用程序上下文。这是您访问资源、偏好等所需的内容。

【讨论】:

我想缩小我的问题范围。在视图模型中有上下文引用是不是很糟糕(这不会影响测试吗?)并且使用 AndroidViewModel 类会以任何方式影响 Dagger?它不是与活动生命周期相关联吗?我正在使用 Dagger 来控制组件的生命周期 ViewModel 类没有getApplication 方法。 不,但AndroidViewModel 可以 但是你需要在它的构造函数中传递Application实例,和从中访问Application实例是一样的 拥有应用程序上下文并不是什么大问题。您不希望有活动/片段上下文,因为如果片段/活动被破坏并且视图模型仍然具有对现在不存在的上下文的引用,那么您会感到无聊。但是您永远不会破坏 APPLICATION 上下文,但 VM 仍然具有对它的引用。正确的?你能想象一个你的应用程序退出但 Viewmodel 没有退出的场景吗? :)【参考方案11】:

我在使用ViewModel 类时无法获取SharedPreferences,因此我从上面的答案中获得了建议,并使用AndroidViewModel 执行了以下操作。现在一切看起来都很棒

对于AndroidViewModel

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;

public class HomeViewModel extends AndroidViewModel 

    private MutableLiveData<String> some_string;

    public HomeViewModel(Application application) 
        super(application);
        some_string = new MutableLiveData<>();
        Context context = getApplication().getApplicationContext();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        some_string.setValue("<your value here>"));
    


而在Fragment

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;


public class HomeFragment extends Fragment 


    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) 
        final View root = inflater.inflate(R.layout.fragment_home, container, false);
        HomeViewModel homeViewModel = ViewModelProviders.of(this).get(HomeViewModel.class);
        homeViewModel.getAddress().observe(getViewLifecycleOwner(), new Observer<String>() 
            @Override
            public void onChanged(@Nullable String address) 


            
        );
        return root;
    

【讨论】:

【参考方案12】:

使用刀柄

@Module
@InstallIn(SingletonComponent::class)
class AppModule 

    @Singleton
    @Provides
    fun provideContext(application: Application): Context = application.applicationContext

然后通过构造函数传递它

class MyRepository @Inject constructor(private val context: Context) 
...

【讨论】:

Hilt 有什么意义?它不像 Hilt 神奇地提供了上下文,你也可以在没有 Hilt 的情况下做到这一点【参考方案13】:

剑柄:

@Inject constructor(@ApplicationContext context : Context) 

【讨论】:

【参考方案14】:

使用以下模式:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application)
   body...

【讨论】:

简短、简单、精确【参考方案15】:

将 Context 注入 ViewModel 的问题在于,Context 可以随时更改,具体取决于屏幕旋转、夜间模式或系统语言,并且任何返回的资源都可以相应更改。 返回简单的资源 ID 会导致额外参数出现问题,例如 getString 替换。 返回高级结果并将渲染逻辑移至 Activity 会增加测试难度。

我的解决方案是让 ViewModel 生成并返回一个稍后通过 Activity 上下文运行的函数。 Kotlin 的语法糖让这变得异常简单!

ViewModel.kt:

// connectedStatus holds a function that calls Context methods
// `this` can be elided
val connectedStatus = MutableLiveData<Context.() -> String> 
  // initial value
  this.getString(R.string.connectionStatusWaiting)

connectedStatus.postValue 
  this.getString(R.string.connectionStatusConnected, brand)

Activity.kt  // is a Context

override fun onCreate(_: Bundle?) 
  connectionViewModel.connectedStatus.observe(this)  it ->
   // runs the posted value with the given Context receiver
   txtConnectionStatus.text = this.run(it)
  

这允许 ViewModel 保存所有用于计算显示信息的逻辑,并通过单元测试进行验证,而 Activity 是一个非常简单的表示,没有隐藏错误的内部逻辑。

【讨论】:

要启用数据绑定支持,您只需添加一个简单的 BindingAdapter,如下所示:@BindingAdapter("android:text")fun setText(view: TextView, value: Context.() -&gt; String) view.text = view.context.run(value)【参考方案16】:

最后我得到了使用 MVVM 在 vi​​ewModel 中获取上下文的最简单方法。假设我们需要 viewmodel 类中的上下文,因此我们可以进行依赖注入或使用 ANDROID_VIEW_MODEL 而不是使用 ViewModel。示例如下。

    class SampleViewModel(app: Application) : AndroidViewModel(app)

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

    val prefManager = PrefManager(context)

   //Now we can call any method which is in PrefManager class like

  prefManager.getToken()


【讨论】:

【参考方案17】:

我是这样创建的:

@Module
public class ContextModule 

    @Singleton
    @Provides
    @Named("AppContext")
    public Context provideContext(Application application) 
        return application.getApplicationContext();
    

然后我刚刚在 AppComponent 中添加了 ContextModule.class:

@Component(
       modules = 
                ...
               ContextModule.class
       
)
public interface AppComponent extends AndroidInjector<BaseApplication> 
.....

然后我在我的 ViewModel 中注入了上下文:

@Inject
@Named("AppContext")
Context context;

【讨论】:

以上是关于如何在 Android MVVM ViewModel 中获取上下文的主要内容,如果未能解决你的问题,请参考以下文章

MVVM简介

mvvm

vue

MVVM-light 中的清理与处置(布尔)

为啥要避免 WPF MVVM 模式中的代码隐藏?

如何构建Android MVVM 应用框架