Android MVVM startActivity 的最佳实践

Posted

技术标签:

【中文标题】Android MVVM startActivity 的最佳实践【英文标题】:Best practice for Android MVVM startActivity 【发布时间】:2017-03-21 09:30:11 【问题描述】:

我正在使用 MVVM 和 DataBinding 构建一个 android 应用程序。我的 ViewModel 中有一个启动 Activity 的函数。 可以在 ViewModel 中调用 onClick 吗?

像这样。

public class MyViewModel 
    public void onClick(View view, long productId) 
        Context context = view.getContext();
        Intent intent = new Intent(context, ProductDetailActivity.class);
        intent.putExtra("productId", productId);
        context.startActivity(intent);
    

在我的 XML 中:

...
android:onClick="@(v) -> viewModel.onClick(v, viewModel.product.id)">

或者将它移动到 View 并从 EventBus 或 Rx 调用它并且我的 ViewModel 中只有 POJO 是否是最佳实践?

【问题讨论】:

【参考方案1】:

您的问题的答案是您的目标是什么?

如果您想使用 MVVM 来分离关注点,以便您可以对 Viewmodel 进行单元测试,那么您应该尝试将需要 Context 的所有内容与您的 Viewmodel 分开。 Viewmodel 包含您应用的核心业务逻辑,应该没有外部依赖项。

但是我喜欢你要去哪里:) 如果决定打开哪个 Activity 在于视图,那么为它编写一个 JUnit 测试是非常非常困难的。但是,您可以将对象传递给执行startActivity() 调用的Viewmodel。现在在您的单元测试中,您可以简单地模拟这个对象并验证是否打开了正确的Activity

【讨论】:

"Viewmodel 包含你应用的核心业务逻辑,应该没有外部依赖。" - 我认为它总是会有外部依赖。我看不出在没有数据绑定的情况下使用 ViewModel 有什么意义,它是特定于 android 的,只需检查 example import: "import android.databinding.ObservableBoolean;" @LLL,你在某种程度上是对的。问题是想要达到什么目标。对我来说,最重要的是可测试性。如果我依赖于ViewContext,我只能通过Robolectric 进行测试。这些测试很慢并且需要大量开销。所以我正在寻找的是简单的单元测试,它可以在很短的时间内运行并且易于实现。对数据绑定库的依赖并没有减少这一点。 @Kaskasi,所以有任何方法可以实现这样的功能。我不想将 Context 保留在我的 ViewModel 类中,因为它会造成内存泄漏。我也不想通过任何参考。因此,如果我想将一些字符串资源从视图传递给 viewModel,或者我想在视图中调用一个方法来启动一些活动,那么我该如何实现它。 @SainiArun 你可以做类似的事情:***.com/questions/31883415/… 所以你要做的就是将一个抽象传递给你的 ViewModel 或 Presenter。该抽象(接口)可以有一个方法String getString(@StringRes int id)。在该接口的具体实现中,您将实现getString 并可以将其委托给上下文。获取字符串()。这样,ViewModel 只能看到一个抽象,您可以模拟它以进行测试 @Kaskasi,感谢您的回复。实际上我已经知道这种方法并且我也在使用它。我并不真正关心测试。我唯一担心的是不要使用任何对视图的引用,因为这会导致内存泄漏,我不希望这样。那么告诉我,如果我们使用视图中实现并被 ViewModel 引用的接口来获取或设置视图中的数据,会不会导致内存泄漏?【参考方案2】:

我的做法是,在您的 ViewModel 中:

val activityToStart = MutableLiveData<Pair<KClass<*>, Bundle?>>()

这可以让你检查Activity启动的类,以及Bundle中传递的数据。然后,在您的 Activity 中,您可以添加以下代码:

viewModel.activityToStart.observe(this, Observer  value ->
    val intent = Intent(this, value.first.java)
    if(value.second != null)
        intent.putExtras(value.second)
    startActivity(intent)
)

【讨论】:

谢谢,但对我不起作用。你能解释一下吗? XML中view标签的onClick方法如何使用?【参考方案3】:

将它放在ViewModel 中绝对完美,但是您需要将ViewModel 设置为Activity/Fragment

您可以通过以下链接了解 MVVM 架构。

Approaching Android with MVVMAndroid MVVMhttps://github.com/ivacf/archiPeople-MVVMMVVM on Android: What You Need to Know

【讨论】:

这些示例中的大多数在 ViewModel 中都有上下文引用。对我来说,这不是真正的 MVVM。 @MwBakker 我认为 MVVM 最重要的是层与层之间的逻辑分离,因此,视图应该负责处理 UI,仅此而已!因此,在这种情况下,我相信 View Model 引用应用程序上下文比强制视图不执行任何 UI 任务的危害要小。顺便说一句,在许多其他情况下,我们可能需要 ViewModel 中的上下文,这就是为什么 Android 有一个具有上下文的 ViewModel 版本! 认为您应该在 viewmodel 中没有 android 包?【参考方案4】:

正如 MVVM 的原理所指出的,只有 View(活动/片段)持有对 ViewModel 的引用,而 ViewModel 不应该持有对任何 View 的引用。

在你的情况下,要开始一项活动,我会这样做:

MyViewModel.class

public class MyViewModel 
public static final int START_SOME_ACTIVITY = 123;

 @Bindable
 private int messageId;

 public void onClick() 
  messageId = START_SOME_ACTIVITY;
  notifyPropertyChanged(BR.messageId); //BR class is automatically generated when you rebuild the project
 

 public int getMessageId() 
        return messageId;
 

 public void setMessageId(int message) 
        this.messageId = messageId;
 


在你的 MainActivity.class

@BindingAdapter("showMessage")
public static void runMe(View view, int messageId) 
    if (messageId == Consts.START_SOME_ACTIVITY)       
        view.getContext().startActivity(new Intent(view.getContext(), SomeActivity.class));      
    


@Override
protected void onPause() 
    super.onPause();
    finish(); //only call if you want to clear this activity after go to other activity

最后,在你的 activity_main.xml

<Button    
  android:onClick="@()-> myViewModel.onClick()"    
  bind:showMessage="@myViewModel.messageId" />

【讨论】:

在视图模型中值保持不变,因此它可能会一次又一次地开始活动【参考方案5】:

根据数据绑定文档。 有两种方法可以做到这一点:

1- MethodReferences :您必须将视图作为参数传递给函数,否则会出现编译时错误。 如果您将使用这种方式,请在此处创建一个单独的类作为示例来处理此类事件。

我的处理程序

public class MyHandler 
   public void onClick(View view, long productId) 
        Context context = view.getContext();
        Intent intent = new Intent(context, ProductDetailActivity.class);
        intent.putExtra("productId", productId);
        context.startActivity(intent);
    

XML

<data>
        <variable
            name="viewModel"
            type="com.example.ViewModel"

        <variable
            name="myHandler"
            type="com.example.MyHandler" />

    </data>android:onClick="@myHandler.onClick(viewModel.product.id)">

2- Listener bindings :您不需要将视图作为此处的示例传递。 但是如果你想 startActivity 让你的 viewModel 扩展 AndroidViewModel 并且你将使用 applicaion 对象。

视图模型

public class MyViewModel extends AndroidViewModel 
    public void onClick(long productId) 
        Intent intent = new Intent(getApplication(), ProductDetailActivity.class);
        intent.putExtra("productId", productId);
        context.startActivity(intent);
    

XML

android:onClick="@() -> viewModel.onClick(viewModel.product.id)">

【讨论】:

在第 2 部分中,context 并未实际定义。假设你想使用getApplication() 作为上下文,不幸的是你必须设置FLAG_INTENT_NEW_TASK,否则你会崩溃。 (有一些最新版本的 Android 没有崩溃,但这被认为是一个错误)。见***.com/q/3918517/3175580 您可以通过 xml 轻松传递它。 android:onClick="@(v) -> viewModel.onClick(v,viewModel.product.id)"> .. 代码:public void onClick(View v,long productId) ... v.getContext() 【参考方案6】:

在 MVVM 中,我们可以将 LiveData 用于此 Event 。因为当activity/Fragment 被销毁时,ViewModel 是活着的!所以最好的方法是LiveData

1.创建类调用EventExtends它来自ViewModel

class Event : ViewModel() 

2.从 LiveData 创建字段:

private val _showSignIn = MutableLiveData<Boolean?>()

3.为这个私有字段创建方法:

val showSignIn: LiveData<Boolean?>
    get() = _showSignIn

4.create 方法,您可以在 liveData 上设置值:

 fun callSignIn() 
        _showSignIn.value = true
    

最终事件类:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class Event : ViewModel() 

     private val _showSignIn = MutableLiveData<Boolean?>()

        val showSignIn: LiveData<Boolean?>
            get() = _showSignIn

        fun callSignIn() 
            _showSignIn.value = true
        
    在您的活动或片段中调用方法:

来自 eventViewModel 的实例:

 private val eventViewModel = Event()

调用观察:

 eventViewModel.showSignIn.observe(this, Observer 
            startActivity(Intent(this, MainActivity::class.java))
        )

如果您使用data binding,您可以在onClick XML 中调用callSignIn()

在变量标签中:

<variable
            name="eventViewModel"
            type=packageName.Event" />

 android:onClick="@() -> eventViewModel.callSignIn()" 

注意:不要忘记在 activity/fragment 中设置绑定:

  binding.eventViewModel = eventViewModel

我寻找最好的方法并找到它。我希望能帮助别人

【讨论】:

小心这种方法。如果你按下按钮,Activity 会启动,然后你返回旋转屏幕,LiveData 值会再次发送,所以 Activity 会意外地再次启动。因此,对于这种事件,您应该使用其他方法,搜索 SingleLiveEvent 或 EventWrapper。

以上是关于Android MVVM startActivity 的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

Android学习总结——实现Home键功能

android开发之路03

Android中具有干净架构的mvvm和没有干净架构的mvvm有啥区别?

android mvvm 角色分别都有哪些担任

Android 中 MVC、MVP 和 MVVM 对比

Android、MVVM:在 ViewModel 中调用 ContentResolver