LiveData to the next level with MediatorLiveData and Transformations

An introduction to MediatorLiveData and Transformations to build reactive patterns with LiveData.

MediatorLiveData

From its explanation, it is a subclass of liveData which can ‘observe’ other LiveData and trigger onChanged() method when observed LiveData’s value is changed.

LiveData<String> source = ....; //
final MediatorLiveData<String> result = new MediatorLiveData<>();
        result.addSource(source, new Observer<String>() {
            @Override
            public void onChanged(@Nullable String x) {
                // do something here 
            }
        });

From the sample code above, when source’s value is changed, the onChanged method of Observer will be triggered.

Transformations

This class is way more like an utilisation of MediatorLiveData. It contains only 2 methods which are map() and switchMap()

@MainThread
    public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
            @NonNull final Function<X, Y> func) {
        final MediatorLiveData<Y> result = new MediatorLiveData<>();
        result.addSource(source, new Observer<X>() {
            @Override
            public void onChanged(@Nullable X x) {
                result.setValue(func.apply(x));
            }
        });
        return result;
    }

   @MainThread
    public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
            @NonNull final Function<X, LiveData<Y>> func) {
        final MediatorLiveData<Y> result = new MediatorLiveData<>();
        result.addSource(trigger, new Observer<X>() {
            LiveData<Y> mSource;

            @Override
            public void onChanged(@Nullable X x) {
                LiveData<Y> newLiveData = func.apply(x);
                if (mSource == newLiveData) {
                    return;
                }
                if (mSource != null) {
                    result.removeSource(mSource);
                }
                mSource = newLiveData;
                if (mSource != null) {
                    result.addSource(mSource, new Observer<Y>() {
                        @Override
                        public void onChanged(@Nullable Y y) {
                            result.setValue(y);
                        }
                    });
                }
            }
        });
        return result;
    }

These 2 methods doing the same thing which apply a function on the value stored in the LiveData object, and propagates the result downstream but the switchMap(), the function that passed in must return a LiveData object.

I’ve created a sample project here. You can take a look and stay with me to let me explain how the code works.

I use https://jsonplaceholder.typicode.com/ as a test api with /todos/1 endpoint. There is a created model name Todo to match with the response

import com.google.gson.annotations.SerializedName

// https://jsonplaceholder.typicode.com/todo/1
class Todo(
    @SerializedName("userId") val userId: Int,
    @SerializedName("id") val id: Int,
    @SerializedName("title") val title: String,
    @SerializedName("completed") val completed: Boolean
)

There are 2 more classes for handling responses from api.

import android.arch.lifecycle.LiveData

data class ResponseError(val code: Int? = null, val messages: String? = null)

data class ResponseResult<T>(val data: LiveData<T>, val error: LiveData<ResponseError>)

The ResponseResult will have 2 properties, both are a LiveData, one is for the response, the other one is for the error (ResponseError).

According to the Google suggestion, I made a repository module with will take a responsibility to fetch a data from network. In this module, I did some trick with ResponseResult:



class TodoRepository(private val api: TodoApi) {

    fun getTodos(): ResponseResult<Todo> {
        val data = MutableLiveData<Todo>()
        val error = MutableLiveData<ResponseError>()
        api.getTodos().enqueue(object : Callback<Todo> {
            override fun onResponse(call: Call<Todo>, response: Response<Todo>) {
                if (response.isSuccessful) {
                    data.value = response.body()
                } else {
                    error.value = ResponseError(response.code(), response.errorBody()?.string())
                }
            }

            override fun onFailure(call: Call<Todo>, t: Throwable) {
                error.value = ResponseError()
            }
        })
        return ResponseResult(data, error)
    }
}

The function getTodos() will return ResponseResult<Todo>, its 2 properties, data’s value will be updated when response is finished, and error’s value will be updated when error happened.

By following the SOLID guideline, I keep my Usecase class so simple.

class GetTodoUsecases(private val repository: TodoRepository) {

    operator fun invoke() = repository.getTodos()
}

Chaining LiveData

Let’s have a look at my ViewModel and Fragment class:

class MainFragment: Fragment(){
    ...
    ...
    ...
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel._onActivityCreated.call()
    }
    ...
    ...
    ...
}

class MainViewModel(private val getTodoUsecase: GetTodoUsecases) : ViewModel() {

    val _onActivityCreated = SingleLiveEvent<Any>()

    private val responseResult: LiveData<ResponseResult<Todo>> = Transformations.map(_onActivityCreated) {
        getTodoUsecase()
    }

    val todoResult: LiveData<Todo> = Transformations.switchMap(responseResult) {
        it.data
    }

    val errorResult: LiveData<ResponseError> = Transformations.switchMap(responseResult) {
        it.error
    }
}

I triggered a LiveData (SingleLiveEvent) at onActivityCreated(), and if you take a look at line 18, Transformation.map() is called which mean getTodoUsecase() will be called. After fetching is done, responseResult will be assigned. And if you take a look again at line 22 and 26, Transformation.switchMap() are called which means when the responseResult’s value is updated, todoResult and errorResult instances will be pointed to the same instances as responseResult’s properties.

Decision Tree

Tips:
OnChanged() method will not be called if MediatorLiveData is not observed at anywhere.

Reference:
Like 111 likes
Jutikorn Varojananulux
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.