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


90

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


以防万一有人尝试使用AndroidViewModel但要获取,Cannot create instance exception您可以参考我的这个答案stackoverflow.com/a/62626408/1055241
gprathour

您不应该在ViewModel中使用Context,而是创建UseCase来通过这种方式获取Context
Ruben Caster,

Answers:


71

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


像魅力一样工作!
SPM

有人可以在代码中显示吗?我在Java中
比斯瓦斯Khayargoli

55

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

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

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

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

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

    //... ViewModel methods 

}

2
为什么不直接使用应用程序参数和普通的ViewModel?我在“ getApplication <Application>()”中看不到任何意义。它只是增加了样板。
令人难以置信的1

50

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

ViewModels不应包含Context实例或诸如Views或其他保存在Context上的对象之类的原因是因为它具有与Activity和Fragments不同的生命周期。

我的意思是,假设您对应用程序进行轮换更改。这会导致您的“活动”和“片段”自行销毁,因此会重新创建自己。ViewModel旨在在此状态下保持不变,因此如果它仍对被破坏的Activity持有View或Context,则有发生崩溃和其他异常的可能性。

至于您应该如何做,MVVM和ViewModel与JetPack的Databinding组件非常兼容。对于大多数情况,通常会存储String,int等,因此可以使用Databinding使Views直接显示它,从而不需要在ViewModel中存储值。

但是,如果您不希望进行数据绑定,则仍然可以在构造函数或方法内部传递Context来访问资源。只是不要在ViewModel中保存该Context的实例。


1
据我了解,包括特定于android的代码需要运行仪器测试,这比普通的JUnit测试要慢得多。我目前正在将Databinding用于click方法,但是我看不到它将如何帮助从xml或首选项获取资源。我只是意识到,对于首选项,我还需要在模型内部添加一个上下文。我目前正在做的是让Dagger注入应用程序上下文(上下文模块从应用程序类内部的静态方法获取它)
Vincent Williams

@VincentWilliams是的,使用ViewModel可以使您的代码从UI组件中抽象出来,这使您更容易进行测试。但是,我要说的是,不包括任何Context,Views或类似内容的主要原因不是出于测试原因,而是因为ViewModel的生命周期可以帮助您避免崩溃和其他错误。至于数据绑定,这可以为您提供资源,因为在大多数情况下,您需要访问代码中的资源是由于需要将String,color,dimen应用于布局中,而数据绑定可以直接进行此操作。
Jackey '18

哦,好了,我明白了你的意思,但是在这种情况下数据绑定将无济于事,因为我需要访问用于模型的字符串(可以将它们放在常量类中,而不是我想的xml中),并且还用于初始化SharedPreferences
Vincent Williams

3
如果我想基于值模型viewmodel切换文本视图中的文本,则需要对字符串进行本地化,因此我需要在viewmodel中获取资源,而没有上下文,我将如何访问资源?
Srishti Roy

3
@SrishtiRoy如果您使用数据绑定,则很容易根据视图模型中的值切换TextView的文本。不需要访问ViewModel内部的Context,因为所有这些操作都在布局文件中发生。但是,如果必须在ViewModel中使用Context,则应考虑使用AndroidViewModel而不是ViewModel。AndroidViewModel包含可以使用getApplication()调用的Application Context,因此,如果ViewModel需要上下文,则应该可以满足Context的需求。
Jackey

15

简短答案-不要这样做

为什么呢

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

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


21
为什么AndroidViewModel类甚至存在?
Alex Berdnikov

1
@AlexBerdnikov MVVM的目的是将视图(活动/片段)与ViewModel隔离,甚至比MVP更重要。这样就更容易测试了。
hushed_voice

3
@free_style感谢您的澄清,但问题仍然存在:如果我们不能在ViewModel中保留上下文,为什么AndroidViewModel类甚至存在?它的全部目的是提供应用程序上下文,不是吗?
Alex Berdnikov

6
@AlexBerdnikov在viewmodel中使用Activity上下文可能会导致内存泄漏。因此,通过使用AndroidViewModel类,您将由Application Context提供,这不会(希望)引起任何内存泄漏。因此,使用AndroidViewModel可能比将活动上下文传递给它更好。但是,这样做仍然会使测试变得困难。这是我的看法。
hushed_voice

1
我无法从资源库访问res / raw文件夹中的文件吗?
Fugogugo

14

最后我做了什么,而不是直接在ViewModel中拥有一个Context,我创建了提供者类(例如ResourceProvider),它将为我提供所需的资源,然后将这些提供者类注入到ViewModel中


1
我在AppModule中将ResourcesProvider与Dagger一起使用。通过ResourcesProvider或AndroidViewModel获取上下文的好方法是否更好地获取资源的上下文?
Usman Rana

@Vincent:如何使用resourceProvider在ViewModel中获取Drawable?
HoangVu

@Vegeta您将getDrawableRes(@DrawableRes int id)在ResourceProvider类中添加类似方法
Vincent Williams

1
这违背了“干净架构”方法,后者指出框架依赖项不应跨越域逻辑(ViewModels)的边界。
IgorGanapolsky '19

1
@IgorGanapolsky VM并非完全是域逻辑。域逻辑是其他类,例如交互器和存储库等。虚拟机属于“胶水”类别,因为它们确实与您的域交互,但不是直接交互。如果您的VM是您域的一部分,那么您应该重新考虑使用模式的方式,因为您要赋予它们过多的责任。
mradzinski

8

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

MVVM是一个很好的体系结构,这绝对是Android开发的未来,但是仍有几件事仍然是绿色的。以MVVM架构中的层通信为例,我已经看到不同的开发人员(非常著名的开发人员)使用LiveData以不同的方式来通信不同的层。他们中的一些人使用LiveData与UI进行ViewModel的通信,但随后他们使用回调接口与存储库进行通信,或者它们具有Interactors / UseCases,并且使用LiveData与它们进行通信。这里要指出的是,并不是所有的东西都是100%定义的还没有

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

如果要处理图像加载,则尝试通过Databinding适配器方法传递View对象,并使用View的上下文加载图像。为什么?因为如果您使用应用程序的上下文加载图像,某些技术(例如Glide)可能会遇到问题。

希望能帮助到你!


5
TL; DR应该位于顶部
Jacques Koorts

1
谢谢您的回答。但是,如果可以让androidviewmodel扩展viewmodel并使用类本身提供的内置上下文,为什么还要使用dagger注入上下文?特别是考虑到使Dagger和MVVM协同工作的可重复代码的荒谬性,其他解决方案似乎更加清晰。您对此有何想法?
Josip Domazet

7

正如其他人提到的那样AndroidViewModel,您可以从中获取要获取的应用程序,Context但是根据我在评论中收集的信息,您正在尝试@drawable从内部操纵s,ViewModel从而破坏了MVVM的用途。

一般而言,几乎所有人都需要有一个Context,这ViewModel表明您应该考虑重新考虑如何在Views和ViewModels

与其让ViewModel解析的可绘制对象提供给活动/片段,不如考虑使片段/活动根据拥有的数据来处理可绘制对象ViewModel。假设您需要在可打开/关闭状态的视图中显示不同的可绘制对象-ViewModel应该保持(可能是布尔)状态的是可绘制对象,但是View是可绘制对象,但是相应地选择可绘制对象是我们的职责。

使用DataBinding可以很容易地完成它:

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

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

或者,也许您需要在Context内部使用的某个组件,ViewModel然后在之外创建该组件ViewModel并将其传递。您可以Context在初始化ViewModelin Fragment/之前使用DI或单例,或创建依赖于组件的组件Activity

为什么要打扰:Context是Android专用的东西,并且依赖于ViewModels是一个不好的做法:它们妨碍了单元测试。另一方面,您自己的组件/服务接口完全在您的控制之下,因此您可以轻松地模拟它们以进行测试。


5

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

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

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


3

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


我想缩小我的问题范围。在viewmodel中有上下文引用是否不好(这不会影响测试吗?)并且使用AndroidViewModel类会以任何方式影响Dagger吗?它不与活动生命周期相关吗?我正在使用Dagger来控制组件的生命周期
Vincent Williams

14
ViewModel类没有getApplication方法。
beroal

4
否,但是AndroidViewModel
4

1
但是您需要在其构造函数中传递Application实例,这与从其访问Application实例相同
John Sardinha

2
具有应用程序上下文并不是什么大问题。您不希望拥有活动/片段上下文,因为如果片段/活动被破坏并且视图模型仍然引用了现在不存在的上下文,您会感到无聊。但是您永远不会销毁APPLICATION上下文,但是VM仍然具有对它的引用。对?您能想象一个情况,您的应用程序退出但Viewmodel退出吗?:)
user1713450

3

您不应在ViewModel中使用与Android相关的对象,因为使用ViewModel的动机是将Java代码和Android代码分开,以便可以分别测试业务逻辑,并且将有单独的Android组件层和业务逻辑和数据,您不应在ViewModel中包含上下文,因为它可能导致崩溃


2
这是一个公平的观察,但是某些后端库仍然需要Application上下文,例如MediaStore。以下4gus71n的答案说明了如何妥协。
Bryan W. Wagner

1
是的,您可以使用应用程序上下文,但不能使用活动的上下文,因为应用程序上下文在整个应用程序生命周期中都存在,但不能使用活动上下文,因为将活动上下文传递给任何异步进程都可能导致内存泄漏。我在本文中提到的上下文是活动上下文。但是您仍应注意不要将上下文传递给任何异步进程,即使它是应用程序上下文也是如此。
罗希特·夏尔马

2

SharedPreferences在使用ViewModel该类时遇到麻烦,因此我从上面的答案中获取了建议,并使用进行了以下操作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;
    }
}

0

我是这样创建的:

@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;

0

使用以下模式:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.