androidmvvmandroid-databindingandroid-mvp

MVP vs MVVM: how to manage alert dialogs in MVVM and improve testability


I'm MVP lover but at the same time I'm open minded and I'm trying to improve my knowledge about MVVM and databinding:

I have forked here https://github.com/jpgpuyo/MVPvsMVVM

the original repo https://github.com/florina-muntenescu/MVPvsMVVM from @FMuntenescu

I have created several branches. In one of them, I want to show 2 different alert dialogs with diferent styles depending of the number of clicks performed on a button:

You can find the branch here: https://github.com/jpgpuyo/MVPvsMVVM/tree/multiple_dialogs_databinding_different_style

I have created 2 observable fields in view model and I have added one binding adapter.

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}">

ViewModel:

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);
    }
}

With MVVM, not sure about how to implement it to be fully testable with java unit tests. I have created a binding adapter, but then:

I have added different styles for each dialog, because if I don't put styles, we can consider that title and message from dialog are retrieved from data layer, but it would be strange to consider that a android style is from data layer.

Is it ok inject a dialog helper into MVVM to solve this problem and make code testable?

Which would be the best way to manage alert dialogs with MVVM?


Solution

  • The solution I use for MVVM is mixed, as follows.

    From the article from Jose Alcérreca mentioned in the Medium post LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) referred in the SO answer to Show Dialog from ViewModel in Android MVVM Architecture, 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.

    My final code is:

    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) })
    }
    

    Usage is like so (my example triggers the event when data synchronization finishes):

    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)
        }
    }
    

    And consuming it by using observeEvent() to have nice, concise code:

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