【问题标题】:Best practice for Android MVVM startActivityAndroid 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 是否是最佳实践?

【问题讨论】:

    标签: android mvvm android-databinding decoupling


    【解决方案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 你可以做类似的事情:stackoverflow.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 MVVM
    Android MVVM
    https://github.com/ivacf/archi
    People-MVVM
    MVVM 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 没有崩溃,但这被认为是一个错误)。见stackoverflow.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
            }
    
    1. 在您的活动或片段中调用方法:

    来自 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。
    猜你喜欢
    • 2019-04-26
    • 2018-11-19
    • 2012-08-19
    • 1970-01-01
    • 2017-09-01
    • 2015-10-20
    • 2018-11-03
    • 1970-01-01
    • 2012-02-09
    相关资源
    最近更新 更多