我正在尝试在android应用中实现MVVM模式。我已经读过ViewModels应该不包含任何android特定代码(使测试更容易),但是我需要对各种事物使用上下文(从xml获取资源,初始化首选项等)。做这个的最好方式是什么?我看到AndroidViewModel
引用了应用程序上下文,但是其中包含android特定的代码,因此我不确定是否应该在ViewModel中使用它。那些也与Activity生命周期事件相关联,但是我使用匕首来管理组件的范围,所以我不确定这将如何影响它。我是MVVM模式和Dagger的新手,所以感谢您的帮助!
答案 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
,这建议您应该重新考虑如何在View
和ViewModels
之间划分逻辑。 / 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是非常简单的表示形式,没有内部逻辑可以隐藏错误。