简体   繁体   English

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

[英]How to get Context in Android MVVM ViewModel

I am trying to implement MVVM pattern in my android app.我正在尝试在我的 android 应用程序中实现 MVVM 模式。 I have read that ViewModels should contain no android specific code (to make testing easier), however I need to use context for various things (getting resources from xml, initializing preferences, etc).我已经读过 ViewModels 不应该包含 android 特定代码(以使测试更容易),但是我需要为各种事情使用上下文(从 xml 获取资源,初始化首选项等)。 What is the best way to do this?做这个的最好方式是什么? I saw that AndroidViewModel has a reference to the application context, however that contains android specific code so I'm not sure if that should be in the ViewModel.我看到AndroidViewModel有一个对应用程序上下文的引用,但是它包含 android 特定代码,所以我不确定它是否应该在 ViewModel 中。 Also those tie into the Activity lifecycle events, but I am using dagger to manage the scope of components so I'm not sure how that would affect it.这些也与活动生命周期事件相关,但我使用匕首来管理组件的 scope,所以我不确定这会如何影响它。 I am new to the MVVM pattern and Dagger so any help is appreciated!我是 MVVM 模式和 Dagger 的新手,因此不胜感激!

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

For Android Architecture Components View Model,对于 Android 架构组件视图模型,

It's not a good practice to pass your Activity Context to the Activity's ViewModel as its a memory leak.将您的 Activity Context 传递给 Activity 的 ViewModel 作为内存泄漏并不是一个好习惯。

Hence to get the context in your ViewModel, the ViewModel class should extend the Android View Model Class.因此,要在您的 ViewModel 中获取上下文,ViewModel 类应该扩展Android 视图模型类。 That way you can get the context as shown in the example code below.这样您就可以获得上下文,如下面的示例代码所示。

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

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

    //... ViewModel methods 

}

It's not that ViewModels shouldn't contain Android specific code to make testing easier, since it's the abstraction that makes testing easier.并不是说 ViewModel 不应该包含 Android 特定的代码来使测试更容易,因为它是使测试更容易的抽象。

The reason why ViewModels shouldn't contain an instance of Context or anything like Views or other objects that hold onto a Context is because it has a separate lifecycle than Activities and Fragments. ViewModels 不应该包含 Context 的实例或任何像 Views 或其他持有 Context 的对象的原因是因为它具有与活动和片段不同的生命周期。

What I mean by this is, let's say you do a rotation change on your app.我的意思是,假设您对应用程序进行了轮换更改。 This causes your Activity and Fragment to destroy itself so it recreates itself.这会导致您的 Activity 和 Fragment 自行销毁,以便重新创建自己。 ViewModel is meant to persist during this state, so there's chances of crashes and other exceptions happening if it's still holding a View or Context to the destroyed Activity. ViewModel 旨在在此状态期间持续存在,因此如果它仍然持有已销毁活动的视图或上下文,则可能会发生崩溃和其他异常。

As for how you should do what you want to do, MVVM and ViewModel works really well with the Databinding component of JetPack.至于您应该如何做您想做的事,MVVM 和 ViewModel 与 JetPack 的数据绑定组件配合得非常好。 For most things you would typically store a String, int, or etc for, you can use Databinding to make the Views display it directly, thus not needing to store the value inside ViewModel.对于大多数通常会存储 String、int 等的内容,您可以使用 Databinding 使 Views 直接显示它,因此不需要将值存储在 ViewModel 中。

But if you don't want Databinding, you can still pass the Context inside the constructor or methods to access the Resources.但是如果你不想要数据绑定,你仍然可以在构造函数或方法中传递 Context 来访问资源。 Just don't hold an instance of that Context inside your ViewModel.只是不要在您的 ViewModel 中保存该 Context 的实例。

Short answer - Don't do this简短回答 - 不要这样做

Why ?为什么 ?

It defeats the entire purpose of view models它违背了视图模型的全部目的

Almost everything you can do in view model can be done in activity/fragment by using LiveData instances and various other recommended approaches.通过使用 LiveData 实例和其他各种推荐的方法,您可以在视图模型中执行的几乎所有操作都可以在活动/片段中完成。

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

As others have mentioned, there's AndroidViewModel which you can derive from to get the app Context but from what I gather in the comments, you're trying to manipulate @drawable s from within your ViewModel which defeats the purpose MVVM.正如其他人所提到的,您可以从中派生出AndroidViewModel来获取应用程序Context但是根据我在评论中收集的内容,您试图从ViewModel操作@drawable ,这违背了 MVVM 的目的。

In general, the need to have a Context in your ViewModel almost universally suggests you should consider rethinking how you divide the logic between your View s and ViewModels .一般来说,几乎普遍需要在您的ViewModel有一个Context建议您应该考虑重新考虑如何划分ViewViewModels之间的逻辑。

Instead of having ViewModel resolve drawables and feed them to the Activity/Fragment, consider having the Fragment/Activity juggle the drawables based on data possessed by the ViewModel .与其让ViewModel解析可绘制对象并将它们提供给 Activity/Fragment,不如考虑让 Fragment/Activity 根据ViewModel拥有的数据处理可绘制对象。 Say, you need different drawables to be displayed in a view for on/off state -- it's the ViewModel that should hold the (probably boolean) state but it's the View 's business to select the drawable accordingly.比如说,您需要在视图中显示不同的可绘制对象以进行开/关状态——应该是ViewModel保持(可能是布尔值)状态,但View的业务是相应地选择可绘制对象。

DataBinding makes it quite easy: 数据绑定使它变得非常简单:

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

If you have more states and drawables, to avoid unwieldy logic in the layout file you can write a custom BindingAdapter that translates, say, an Enum value into an R.drawable.* ref, eg:如果您有更多的状态和可绘制对象,为了避免布局文件中的笨拙逻辑,您可以编写一个自定义的BindingAdapterEnum值转换为R.drawable.* ref,例如:

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}"
    ... />

If you need the Context for some component that you use within your ViewModel -- then, create the component outside the ViewModel and pass it in. You can use DI, or singletons, or create the Context -dependent component right before initialising the ViewModel in Fragment / Activity .如果您需要在ViewModel使用的某些组件Context - 那么,在ViewModel之外创建该组件并将其传入。您可以使用 DI 或单例,或者在初始化ViewModel之前创建依赖于Context组件Fragment / Activity

Why bother何苦

Context is an Android-specific thing, and depending on it in ViewModel s is unwieldy for unit tests (of course you can use AndroidJunitRunner for android-specific stuff, but it just makes sense to have cleaner code without the extra dependency). Context是 Android 特定的东西,并且在ViewModel s 中依赖它对于单元测试来说是笨拙的(当然,您可以将AndroidJunitRunner用于 android 特定的东西,但拥有更清晰的代码而没有额外的依赖是有意义的)。 If you don't depend on Context , mocking everything for the ViewModel test is easier.如果您不依赖Context ,则为ViewModel测试模拟所有内容会更容易。 So, rule of thumb is: don't use Context in ViewModels unless you have a very good reason to do so.因此,经验法则是:不要在 ViewModel 中使用Context ,除非您有充分的理由这样做。

TL;DR: Inject the Application's context through Dagger in your ViewModels and use it to load the resources. TL;DR:通过 Dagger 在您的 ViewModel 中注入应用程序的上下文并使用它来加载资源。 If you need to load images, pass the View instance through arguments from the Databinding methods and use that View context.如果您需要加载图像,请通过数据绑定方法的参数传递 View 实例并使用该 View 上下文。

The MVVM is a good architecture and It's definitely the future of Android development, but there's a couple of things that are still green. MVVM 是一个很好的架构,它绝对是 Android 开发的未来,但有一些东西仍然是绿色的。 Take for example the layer communication in a MVVM architecture, I've seen different developers (very well known developers) use LiveData to communicate the different layers in different ways.以 MVVM 架构中的层通信为例,我见过不同的开发人员(非常知名的开发人员)使用 LiveData 以不同的方式来通信不同的层。 Some of them use LiveData to communicate the ViewModel with the UI, but then they use callback interfaces to communicate with the Repositories, or they have Interactors/UseCases and they use LiveData to communicate with them.其中一些使用 LiveData 将 ViewModel 与 UI 进行通信,但随后它们使用回调接口与存储库进行通信,或者它们具有交互器/用例并使用 LiveData 与它们进行通信。 Point here, is that not everything is 100% define yet .点这里,是不是一切都100%的定义

That being said, my approach with your specific problem is having an Application's context available through DI to use in my ViewModels to get things like String from my strings.xml话虽如此,我解决您的具体问题的方法是通过 DI 提供应用程序的上下文,以便在我的 ViewModel 中使用,以从我的 strings.xml 中获取诸如 String 之类的东西

If I'm dealing with image loading, I try to pass through the View objects from the Databinding adapter methods and use the View's context to load the images.如果我正在处理图像加载,我会尝试从数据绑定适配器方法传递 View 对象并使用 View 的上下文来加载图像。 Why?为什么? because some technologies (for example Glide) can run into issues if you use the Application's context to load images.因为如果您使用应用程序的上下文加载图像,某些技术(例如 Glide)可能会遇到问题。

Hope it helps!希望能帮助到你!

has a reference to the application context, however that contains android specific code具有对应用程序上下文的引用,但是它包含特定于 android 的代码

Good news, you can use Mockito.mock(Context.class) and make the context return whatever you want in tests!好消息,您可以使用Mockito.mock(Context.class)并使上下文在测试中返回您想要的任何内容!

So just use a ViewModel as you normally would, and give it the ApplicationContext via the ViewModelProviders.Factory as you normally would.因此,只需像往常一样使用ViewModel ,并像往常一样通过 ViewModelProviders.Factory 为其提供 ApplicationContext。

you can access the application context from getApplication().getApplicationContext() from within the ViewModel.您可以从 ViewModel 中的getApplication().getApplicationContext()访问应用程序上下文。 This is what you need to access resources, preferences, etc..这是您访问资源、首选项等所需的内容。

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

I was having trouble getting SharedPreferences when using the ViewModel class so I took the advice from answers above and did the following using AndroidViewModel .在使用ViewModel类时,我在获取SharedPreferences时遇到了问题,所以我从上面的答案中获取了建议,并使用AndroidViewModel了以下操作。 Everything looks great now现在一切看起来都很棒

For the 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>"));
    }

}

And in the FragmentFragment

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

Using Hilt使用刀柄

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

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

Then pass it via constructor然后通过构造函数传递

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

在刀柄:

@Inject constructor(@ApplicationContext context : Context) 

Use the following pattern:使用以下模式:

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

The problem with injecting a Context into the ViewModel is that the Context can change at any time, depending on screen rotation, night mode, or system language, and any returned resources can change accordingly.将 Context 注入 ViewModel 的问题在于,Context 可以随时更改,具体取决于屏幕旋转、夜间模式或系统语言,并且任何返回的资源都可以相应更改。 Returning a simple resource ID causes problems for extra parameters, like getString substitutions.返回一个简单的资源 ID 会导致额外参数出现问题,例如 getString 替换。 Returning a high-level result and moving rendering logic to the Activity makes it harder to test.返回高级结果并将渲染逻辑移动到 Activity 会使测试变得更加困难。

My solution is to have the ViewModel generate and return a function that is later run through the Activity's Context.我的解决方案是让 ViewModel 生成并返回一个稍后通过 Activity 的上下文运行的函数。 Kotlin's syntactic sugar makes this incredibly easy! 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)
  }
}

This allows ViewModel to hold all of the logic for calculating the displayed information, verified by unit tests, with the Activity being a very simple representation with no internal logic to hide bugs.这允许 ViewModel 保存所有用于计算显示信息的逻辑,由单元测试验证,而 Activity 是一个非常简单的表示,没有内部逻辑来隐藏错误。

Finally i got the easiest way to get context in viewModel using MVVM.最后,我得到了使用 MVVM 在 vi​​ewModel 中获取上下文的最简单方法。 Suppose we need context in viewmodel class so we can go to dependency injection or using ANDROID_VIEW_MODEL instead of using ViewModel.假设我们需要 viewmodel 类中的上下文,因此我们可以进行依赖注入或使用 ANDROID_VIEW_MODEL 而不是使用 ViewModel。 sample is given below.示例如下。

    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()

}

This is a way to get Context in to ViewModel这是一种将 Context 放入 ViewModel 的方法

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

I created it this way:我是这样创建的:

@Module
public class ContextModule {

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

And then I just added in AppComponent the ContextModule.class:然后我只是在 AppComponent 中添加了 ContextModule.class:

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

And then I injected the context in my ViewModel:然后我在我的 ViewModel 中注入了上下文:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM