Trainto.log()

IT, music, movie, travel, life.. random log of my life

Android - Implementing Zip function using LiveData

The last post, Replace EventBus with LiveData was introduced.

This time, Implementing Zip function(Like Rx) using LiveData will be introduced. To acheive this, MediatorLiveData, which extends LiveData, will be applied.

MediatorLiveData is a LiveData subclass, can observe other LiveData objects and react on LiveData objects’ changes. Basic usage can be found here.

If you’re reading this post, I’m sure that you already know well the concept of Zip function of Rx. Just in case, refer this page for basic concept of Zip function.

As always, start with dependencies configuration.

// This includes LiveDdata and ViewModel
// If you want to add just LiveData, then use 
// "android.arch.lifecycle:livedata:x.x.x"
implementation "android.arch.lifecycle:extensions:1.1.1"


Let’s assume that there is MainViewModel which calls two async retrofit calls.

class MainViewModel : ViewModel() {
    private val name: MutableLiveData<String> = MutableLiveData()
    private val age: MutableLiveData<Int> = MutableLiveData()
  
    fun getZippedLiveData(): LiveData {
        return zip2(name, age) { name: String, age: Int -> "name: $name, age: $age" }
    }

    fun start() {
        // Async calls
        // Asume there is RemoteApi class which implement Retrofit async call
        RemoteApi.getName() {
            it?.let { this.name.value = it }
        }

        RemoteApi.getAge() {
            it?.let { this.age.value = it }
        }
    }
}

This ViewModel calls 2 async remote calls from start(), and result(name, age) will be saved to each MutableLiveData. And View(activity) will observe zipped LiveData, and zipped LiveData will be passed to the View by calling getZippedLiveData().

The View(activity) code will look like below.

class MainActivity : AppCompatActivity() {
    private val vm = ViewModelProviders.of(this).get(MainViewModel::class.java)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // If both name and age are received from the remote calls,
        // then show them in the TextView
        vm.getZippedLiveData().observe(this, Observer {
            it?.let {
                textView.text = it
            }
        })

        vm.start()
    }
}

So far this is typical android MVVM architecture using LiveData and ViewModel. The main point is how the zipped LiveData object can be created at getZippedLiveData() function of MainViewModel.

Now let’s figure out how zipped LiveData can be created.

fun <T1, T2, R> zip2(src1: LiveData<T1>, src2: LiveData<T2>,
                     zipper: (T1, T2) -> R): LiveData<R> {

    return MediatorLiveData<R>().apply {
        var src1Version = 0
        var src2Version = 0

        var lastSrc1: T1? = null
        var lastSrc2: T2? = null

        fun updateValueIfNeeded() {
            if (src1Version > 0 && src2Version > 0 &&
                lastSrc1 != null && lastSrc2 != null) {
                value = zipper(lastSrc1!!, lastSrc2!!)
                src1Version = 0
                src2Version = 0
            }
        }

        addSource(src1) {
            lastSrc1 = it
            src1Version++
            updateValueIfNeeded()
        }

        addSource(src2) {
            lastSrc2 = it
            src2Version++
            updateValueIfNeeded()
        }
    }
}

Above zip2 function takes two LiveData, src1 and src2. These are used as input source for MediatorLiveData to create. And also it takes zipper function(Imagine if this was Java… would be much more complicated!) as its last parameter, and it should describe how the data would be zipped into one object R(See getZippedLiveData() function of MainViewModel). In the updateValueIfNeeded() function, it checks version and null, and then set MediatorLiveData’s value.

Simply The MediatorLiveData created issues(changing value) events only if all input source(src1, src2) are ready.

This example is very simple case, but if you edit zip2 function you like, it can be applied various case. For example src1, src2 parameters can be replaced with vararg.

Maybe you noticed this implementation does not behave exactly same with Rx’s zip. In this implementation, MediatorLiveData only takes the latest pair of input source. To make exactly same with Rx’s zip, input source’s history should be managed in the MediatorLiveData.

Android - Replace EventBus with LiveData

Google announced a new set of architecture libraries. One of the new compoenents is LiveData, which can be used to manage propagating data to the views, while respecting the view’s lifecycles. It means you don’t have to care about view’s lifecycles anymore.

Using this LiveData, an event bus can be implemented without any 3rd party libraries like Otto. It also can be achieved using Rx, but with Rx, it can easily lead to memory leaks unless subscriptions managed carefully. With LiveData, we don’t have to worry about memory leaks or view’s lifecycles.

Let’s start with implementation. (Kotlin will be used)

To use LiveData in your application, dependencies should be applied to your gradle configuration.

// This includes LiveDdata and ViewModel
// If you want to add just LiveData, then use 
// "android.arch.lifecycle:livedata:x.x.x"
implementation "android.arch.lifecycle:extensions:1.1.0"

Ok, everything’s set to start.

We need a class holds livedata(event) first.

object EventProvider {

    private val liveDataEvent = MutableLiveData<Event>()

    fun post(event: Any) {
        this.liveDataEvent.postValue(Event(event))
    }

    fun getEventLiveData(): MutableLiveData<Event> {
        return liveDataEvent
    }


    class Event(private val value:Any) {
        private var isConsumed = false

        fun setConsumed() {
            isConsumed = true
        }

        fun isConsumed(): Boolean {
            return isConsumed
        }

        fun getValue(): Any {
            return this.value
        }
    }
}

This class is singleton and holds MutalbeLiveData. The Event class's value field has Any type so that any type of data can be sent through this EvnetProvider. And Event can be submmited through post function. Also note that the Event class has isConsumed field. This is due to the behaviour of LiveData which it sends out when new observer is registered. This is totally fine if it is for propagating values, but here LiveData is used for Event Bus, so we don't want to receive events every time activities created and registered.

Ok, now we prepared event bus. Now let’s subscribe this event bus from the view(activity).

abstract class BaseActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        EventProvider.getEventLiveData().observe(this, Observer {
            it?.let { 
                if (!it.isConsumed()) handleEventBus(it.getValue())
                it.setConsumed()
            }
        })
    }
    
    protected fun handleEventBus(value: Any) {
        when (value) {
            is String -> Log.d("Event", value)
        }
    }
}

I mentioned that with LiveData, we don’t have to care about view’s lifecycles. To take this advantage, activity should extend android.support.vy.app.AppCompatActivity.(For fragment, it should extends android.support.v4.app.Fragment) Because these support libraries are already integrated with LifecycleOwners.

This BaseActivity subscribes the event bus through call observe(LifecyclerOwner, Observer) of the LiveData inside of EventProvider on its onCreate() lifecycle. When event occurred, Observer checks if this event consumed before, if it’s not, then call handleEventBus function.

Now you can create activities extends this BaseActivity, and override the handleEventBus to make the activity has its own behaviour to the specific event.

Kotlin - Kotlin's magic with let, apply, run and with

Kotlin’s Standard.kt provides some higher-order functions implementing idiomatic patterns like let, apply, run and with.

With the help of these functions, your code can be more simple and elegant.

The only challenge of using these functions(let, apply, run and with) is that it feels they are very much similar.

Let’s go through how to handle this Kotlin’s magic.

let

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

let() passes the object, which calls it, to the block, and returns the result of the block.

val student = Student("David", 20)
val result = student.let { it.age * 2 }

println(result) // 40

This is a very simple example, but consider using let with safe calls(?.). You don’t have to bother with if (obj != null) {….} anymore. Check below.

// without safe calls and let
var strRes: String? = null
if (context != null) {
    strRes = context.getString(R.string.app_name)
}

// with safe calls and let
var strRes:String? = context?.let { it.getString(R.string.app_name) }

This is quite powerful, isn’t it?


apply

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

apply() passes the object, which calls it, to the block’s receiver, and returns the object itself.

Let’s check out how apply() can be used.

class Student(val name: String, val age: Int) {
    var major: String? = null
    var mobileNumber: String? = null
    var address: String? = null
}

val newStudent = Student("Joon", 20).apply { 
    major = "Computer Science"
    mobileNumber = "+82-10-1111-1111"
    address = "Seoul"
}

With apply, new Student instance initialized in a convenient way.


run

/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

There are 2 types of run() function.

Using run() independently without object, it will be treated like anonymous function without parameters. Just like any other functions, it can return nothing, or return something.

When run() called from objects, the object will be passed to the block, and returns result of the block.

Maybe it is confused with apply(), but remember that they return different type.(apply returns an object which it is called, and run returns the result of the block.)

class Student(val name: String, val age: Int) {
    var major: String? = null
    var mobileNumber: String? = null
    var address: String? = null
}

val newStudent = Student("Joon", 20).apply { 
    major = "Computer Science"
    mobileNumber = "+82-10-1111-1111"
    address = "Seoul"
}

val homework = newStudent.run {
    if (major === "Computer Science") {
        "Implementing LinkedList"
    } else {
        "No homework!!"
    }
}

println(homework) // "Implementing LinkedList"


with

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

with() passes its parameter to the block as a receiver, and returns the result of the block.

Actually with() is almost same with run(), except where the object passed to the block is. And run() can be used with safe calls, but with() can’t.

class Student(val name: String, val age: Int) {
    var major: String? = null
    var mobileNumber: String? = null
    var address: String? = null
}

val newStudent = Student("Joon", 20).apply { 
    major = "Computer Science"
    mobileNumber = "+82-10-1111-1111"
    address = "Seoul"
}

val homework = with(newStudent) {
    if (major === "Computer Science") {
        "Implementing LinkedList"
    } else {
        "No homework!!"
    }
}


There are another useful funtions in Standard.kt like also, takeIf, takeUnless and repeat. Refer to Standard.kt, and examine them with simple code. I believe it will make your kotlin code more beautiful.