MVP vs MVVM:如何在 MVVM 中管理警报对话框并提高可测试性

Posted

技术标签:

【中文标题】MVP vs MVVM:如何在 MVVM 中管理警报对话框并提高可测试性【英文标题】:MVP vs MVVM: how to manage alert dialogs in MVVM and improve testability 【发布时间】:2019-11-15 10:57:54 【问题描述】:

我是 MVP 爱好者,但同时我思想开放,我正在努力提高我对 MVVM 和数据绑定的了解:

我在这里叉了https://github.com/jpgpuyo/MVPvsMVVM

原始仓库https://github.com/florina-muntenescu/MVPvsMVVM 来自@FMuntenescu

我创建了几个分支。在其中一个中,我想显示 2 个具有不同样式的不同警报对话框,具体取决于在按钮上执行的单击次数:

偶数点击 -> 显示标准对话框 奇数点击次数 -> 显示 droidcon 对话框

你可以在这里找到分支: https://github.com/jpgpuyo/MVPvsMVVM/tree/multiple_dialogs_databinding_different_style

我在视图模型中创建了 2 个可观察字段,并添加了一个绑定适配器。

活动:

private void setupViews() 
    buttonGreeting = findViewById(R.id.buttonGreeting);
    buttonGreeting.setOnClickListener(v -> mViewModel.onGreetingClicked());


<LinearLayout
    android:layout_
    android:layout_
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    app:greetingType="@viewModel.greetingType"
    app:greetingMessage="@viewModel.greetingMessage">

视图模型:

public ObservableField<String> greetingMessage = new ObservableField<>();
public ObservableField<GreetingType> greetingType = new ObservableField<>();

public void onGreetingClicked() 
    numberOfClicks++;
    if (numberOfClicks % 2 == 0) 
        mSubscription.add(mDataModel.getStandardGreeting()
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(greeting -> 
                    greetingMessage.set(greeting);
                    greetingType.set(GreetingType.STANDARD);
                ));
     else 
        mSubscription.add(mDataModel.getDroidconGreeting()
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(greeting -> 
                    greetingMessage.set(greeting);
                    greetingType.set(GreetingType.DROIDCON);
                ));
    

MVVMBindingAdapter:

@BindingAdapter("greetingType", "greetingMessage")
public static void showAlertDialog(View view, GreetingType greetingType, 
String greetingMessage) 
    if (GreetingType.STANDARD.equals(greetingType))
        new DialogHelper().showStandardGreetingDialog(view.getContext(), 
        greetingMessage, greetingMessage);
     else if(GreetingType.DROIDCON.equals(greetingType)) 
        new DialogHelper().showDroidconGreetingDialog(view.getContext(), 
        greetingMessage, greetingMessage);
    


对于 MVVM,不确定如何实现它以完全可通过 java 单元测试进行测试。我已经创建了一个绑定适配器,但是:

我需要一个 if/else 绑定适配器来显示一个或另一个对话框。

我不知道如何将对话框助手注入绑定适配器,因此我无法通过单元测试进行验证,除非使用 powermock。

我为每个对话框添加了不同的样式,因为如果我不放置样式,我们可以认为对话框中的标题和消息是从数据层中检索的,但是如果认为android样式是从数据中获取的,那就很奇怪了层。

是否可以在 MVVM 中注入一个对话框助手来解决这个问题并使代码可测试?

使用 MVVM 管理警报对话框的最佳方式是什么?

【问题讨论】:

经过一番研究,我找到了SingleLiveEvent 的方法。我还发现了一个interesting discussion,Hannes Dorfmann 提出了一种模型视图意图方法。使用这种方法,我可以创建 2 种不同的状态:每个对话框一个。无论如何,这将是另一种方法,在我看来,MVP 仍然比 MVVM 或 MVI 提供更多的可测试性,因为演示者知道观点。 【参考方案1】:

我用于MVVM的方案是混合的,如下。

从Jose Alcérreca 在Show Dialog from ViewModel in Android MVVM Architecture 的SO 答案中提到的Medium 帖子LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) 中提到的文章中,我选择了第四个选项“推荐:使用事件包装器”。原因是我可以在需要时查看消息。另外,我从this comment in Jose's Gist 中添加了observeEvent() 扩展方法。

我的最终代码是:

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 * See:
 *  https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
 *  https://gist.github.com/JoseAlcerreca/e0bba240d9b3cffa258777f12e5c0ae9
 */
open class LiveDataEvent<out T>(private val content: T) 

    @Suppress("MemberVisibilityCanBePrivate")
    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? 
        return if (hasBeenHandled) 
            null
         else 
            hasBeenHandled = true
            content
        
    

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content


inline fun <T> LiveData<LiveDataEvent<T>>.observeEvent(owner: LifecycleOwner, crossinline onEventUnhandledContent: (T) -> Unit) 
    observe(owner, Observer  it?.getContentIfNotHandled()?.let(onEventUnhandledContent) )

用法是这样的(我的例子是在数据同步完成时触发事件):

class ExampleViewModel() : ViewModel() 
    private val _synchronizationResult = MutableLiveData<LiveDataEvent<SyncUseCase.Result>>()
    val synchronizationResult: LiveData<LiveDataEvent<SyncUseCase.Result>> = _synchronizationResult

    fun synchronize() 
        // do stuff...
        // ... when done we get "result"
        _synchronizationResult.value = LiveDataEvent(result)
    

并通过使用observeEvent() 来获得漂亮、简洁的代码:

exampleViewModel.synchronizationResult.observeEvent(this)  result ->
    // We will be delivered "result" only once per change

【讨论】:

以上是关于MVP vs MVVM:如何在 MVVM 中管理警报对话框并提高可测试性的主要内容,如果未能解决你的问题,请参考以下文章

MVC vs MVP vs MVVM

Android 中 MVC、MVP 和 MVVM 对比

Jetpack Compose 架构如何选?MVP MVVM 还是 MVI?

MVP MVC 和 MVVM 的区别

浅谈开发中的MVVM模式及与MVP和MVC的区别

mvc mvp mvvm的区别