如何在 Android MVVM 视图模型中获得上下文

我试图在我的 Android 应用程序中实现 MVVM 模式。我读到过 ViewModel 不应该包含特定于 android 的代码(以使测试更容易) ,但是我需要为各种事情使用上下文(从 xml 获取资源、初始化首选项等)。最好的方法是什么?我看到 AndroidViewModel有一个对应用程序上下文的引用,但是它包含了 android 特定的代码,所以我不确定那是否应该在 ViewModel 中。这些也与活动生命周期事件相关,但是我使用匕首来管理组件的范围,所以我不确定这会对它产生什么影响。我是新的 MVVM 模式和匕首,所以任何帮助是感谢!

164662 次浏览

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

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

ViewModel 之所以不应该包含 Context 的实例,或者任何类似于 View 或者其他持有 Context 的对象,是因为它有一个单独的生命周期,而不是活动和片段。

我的意思是,假设你在你的应用程序上做了一个旋转变化。这会导致你的活动和碎片自我毁灭,所以它会重新创造自己。ViewModel 意味着在这种状态下持续存在,所以如果它仍然持有被破坏的活动的 View 或 Context,就有可能发生崩溃和其他异常。

至于您应该如何做您想做的事情,MVVM 和 ViewModel 与 JetPack 的 Databinding 组件一起工作得非常好。 对于通常存储 String、 int 或 etc 的大多数事情,您可以使用 Databinding 使视图直接显示它,因此不需要在 ViewModel 中存储值。

但是如果您不需要 Databinding,您仍然可以在构造函数或方法中传递 Context 来访问参考资料。只是不要在 ViewModel 中保存该 Context 的实例。

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

MVVM 是一个很好的架构,它肯定是 Android 开发的未来,但是有一些东西仍然是绿色的。以 MVVM 架构中的层通信为例,我见过不同的开发人员(非常有名的开发人员)使用 LiveData 以不同的方式通信不同的层。其中一些使用 LiveData 与用户界面通信 ViewModel,但是接下来他们使用回调接口与仓库通信,或者他们有交互器/用例,他们使用 LiveData 与它们通信。重点是,并非所有事情都是100% 定义 还没有的。

也就是说,我解决您的特定问题的方法是让应用程序的上下文可以通过 DI 在 ViewModel 中使用,从 strings.xml 中获取类似 String 的内容

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

希望能有帮助!

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

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

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

因此,只需像平常一样使用 ViewModel,并通过 ViewModelProvider 为其提供 ApplicationContext。像往常一样工厂。

我最终没有在 ViewModel 中直接使用 Context,而是创建了诸如 ResourceProvider 之类的提供程序类,它们将为我提供所需的资源,并将这些提供程序类注入到我的 ViewModel 中

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

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

将活动上下文作为内存泄漏传递给活动的 ViewModel 并不是一个好的做法。

因此,为了获得 ViewModel 中的上下文,ViewModel 类应该扩展 Android 视图模型类。通过这种方式,您可以获得下面的示例代码所示的上下文。

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


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


//... ViewModel methods


}

正如其他人提到的,你可以从 AndroidViewModel获得应用程序 Context,但从我在评论中收集到的信息来看,你试图在 ViewModel中操纵 @drawable,这破坏了 MVVM 的目的。

一般来说,需要有一个 Context在您的 ViewModel几乎普遍建议您应该重新考虑如何划分您的 ViewViewModels之间的逻辑。

与其让 ViewModel解析可绘制的内容并将它们提供给活动/片段,不如考虑让片段/活动根据 ViewModel拥有的数据来处理可绘制的内容。假设,您需要在开/关状态的视图中显示不同的绘图——应该是 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之外创建组件并将其传递进来。在 Fragment/Activity中初始化 ViewModel之前,您可以使用 DI,或者单件,或者创建依赖于 Context的组件。

何必呢

Context是一个 Android 特有的东西,在 ViewModel中依赖它进行单元测试是不方便的(当然你可以使用 AndroidJunitRunner进行 Android 特有的东西,但是如果没有额外的依赖性,拥有更清晰的代码是有意义的)。如果您不依赖于 Context,那么为 ViewModel测试模拟所有东西就更容易了。所以,经验法则是: 除非有充分的理由,否则不要在 ViewModel 中使用 Context

简而言之,别这么做

为什么?

它破坏了视图模型的整个目的

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

我是这样创造它的:

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

使用以下模式:

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

我在使用 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;
}
}

将 Context 注入 ViewModel 的问题在于,Context 可以随时更改,这取决于屏幕旋转、夜间模式或系统语言,并且任何返回的资源都可以相应地更改。 返回一个简单的资源 ID 会导致额外参数的问题,比如 getString 替换。 返回一个高级结果并将呈现逻辑移动到 Activity 会使测试变得更加困难。

我的解决方案是让 ViewModel 生成并返回一个稍后通过活动上下文运行的函数。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 是一个非常简单的表示,没有隐藏 bug 的内部逻辑。

使用 Hilt

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


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

然后通过构造函数传递它

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

在希尔特:

@Inject constructor(@ApplicationContext context : Context)

最后,我得到了使用 MVVM 在 viewModel 中获得上下文的最简单的方法。假设我们在 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()


}

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

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