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

时间:2018-07-21 00:29:47

标签: android mvvm dagger-2 android-context

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

15 个答案:

答案 0 :(得分:15)

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

答案 1 :(得分:13)

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

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

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

关于应该怎么做,MVVM和ViewModel与JetPack的Databinding组件配合得很好。 对于大多数情况,通常需要存储String,int等,您可以使用Databinding使Views直接显示它,因此不需要在ViewModel中存储值。

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

答案 2 :(得分:8)

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

答案 3 :(得分:5)

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

答案 4 :(得分:4)

  

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

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

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

答案 5 :(得分:3)

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

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

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

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

希望有帮助!

答案 6 :(得分:3)

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

答案 7 :(得分: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;
    }
}

答案 8 :(得分:1)

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

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

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

try:
    # RATIO FOR THE LEFT LEG
    ratio1 = distance(dict[x],dict[y]) 
    print(ratio1)
except KeyError:
    ratio1 = 0
    print('Left Ratio Not Available')
try:
    # RATIO FOR THE RIGHT LEG    
    ratio2 = distance(dict[p],dict[q])
    print(ratio2)
except KeyError:
    ratio2 = 0
    print('Right Ratio Not Available')

print('max ratio is : ', max(ratio1,ratio2))

答案 9 :(得分:1)

正如其他人提到的那样,您可以从中获得AndroidViewModel来获得应用Context,但是根据我在评论中收集到的信息,您正在尝试操纵@drawable在您的ViewModel中,这几乎肯定会破坏完成整个MVVM的目的。

总的来说,Context中需要有一个ViewModel,这建议您应该重新考虑如何在ViewViewModels之间划分逻辑。 / p>

例如与其让ViewModel解析可绘制对象并将它们提供给活动/片段,不如让片段/活动基于ViewModel拥有的数据来处理可绘制对象。例如,如果您有某种On / Off指示器,则ViewModel应该保持(可能是布尔值)状态,但是View的任务是相应地选择适当的可绘制对象。

如果您需要Context用于与ViewModel的构造函数(手动/通过注入)不直接相关的视图(例如后端请求)的某些组件/服务-这样,显式依赖Context,因此在测试中易于模拟(只需将模拟服务/组件传递给构造函数,或将它们提供给注入选择的工具,而无需实际的Context

答案 10 :(得分:1)

使用刀柄

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

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

然后通过构造函数传递

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

答案 11 :(得分:0)

简短答案-不要这样做

为什么?

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

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

答案 12 :(得分:0)

我是这样创建的:

_indicator() {
return Container(
  decoration: BoxDecoration(color: cWhite, boxShadow: [BoxShadow(color: cDivider, offset: Offset(2.0, 2.0))]),
  padding: EdgeInsets.only(top: 20, left: 10, right: 10),
  child: TabBar(
    controller: TabController(vsync: this, length: 3),
    indicatorColor: cPrimary,
    indicatorSize: TabBarIndicatorSize.label,
    labelColor: textPrimary,
    indicatorWeight: 4,
    labelStyle: TextStyles.TITLE_S,
    unselectedLabelColor: textThird,
    unselectedLabelStyle: TextStyles.TEXT_S_3,
    tabs: <Widget>[
      Align(alignment: Alignment.centerLeft,child: Tab(text: pageTitle[0]),),
      Tab(text: pageTitle[1]),
      Align(alignment: Alignment.centerRight,child: Tab(text: pageTitle[2]),),
    ],
  )
);

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

@Module
public class ContextModule {

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

然后将上下文注入到ViewModel中:

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

答案 13 :(得分:0)

使用以下模式:

lambda

答案 14 :(得分:0)

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

我的解决方案是让ViewModel生成并返回一个函数,该函数稍后将在Activity的Context中运行。 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是非常简单的表示形式,没有内部逻辑可以隐藏错误。