MVP 与 MVVM:如何在 MVVM 中管理警报对话框并提高可测试性
MVP vs MVVM: how to manage alert dialogs in MVVM and improve testability
我是 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 个可观察字段,并添加了一个绑定适配器。
Activity:
private void setupViews() {
buttonGreeting = findViewById(R.id.buttonGreeting);
buttonGreeting.setOnClickListener(v -> mViewModel.onGreetingClicked());
}
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
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 管理警报对话框的最佳方式是什么?
我用的MVVM的方案是混合的,如下
来自 Medium 中提到的 Jose Alcérreca 的文章 post LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) referred in the SO answer to , I choose the forth option "Recommended: Use an Event wrapper". The reason being that I'm able to peek the message if needed. Also, I added the observeEvent()
extension method from this comment in Jose's Gist。
我的最终代码是:
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 爱好者,但同时我思想开放,我正在努力提高我对 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 个可观察字段,并添加了一个绑定适配器。
Activity:
private void setupViews() {
buttonGreeting = findViewById(R.id.buttonGreeting);
buttonGreeting.setOnClickListener(v -> mViewModel.onGreetingClicked());
}
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
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 管理警报对话框的最佳方式是什么?
我用的MVVM的方案是混合的,如下
来自 Medium 中提到的 Jose Alcérreca 的文章 post LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) referred in the SO answer to observeEvent()
extension method from this comment in Jose's Gist。
我的最终代码是:
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
}