Hacking Architecture Components by using Kotlin

综合技术 2018-03-14

I’ve been using Architecture Components for a while, and I must admit I love them. The Android team has managed to find a way to let us forget about lifecycles and focus on what really matters.

But not only that. Thanks to Architecture Components (which you can learn more about them here ), not committing mistakes becomes much easier. Just follow the rule of not using activities, views, contexts… from the ViewModels , and you are good to go: unexpected crashes during configuration changes will disappear.

And this also has another implicit benefit: your code becomes much easier to test . You get rid of the components that are more complicated to test and, if you use LiveData to communicate back to the activity, you can subscribe to them during tests and simulate all possible combinations. There’s a nice article about testing Architecture Components by Joe Birch.

A quick intro to Architecture Components

I don’t want to dive too deep into it, but to see how the rest of the code works, you need to have a little understanding of some concepts. In this article, I’ll only talk about two components:

  • ViewModel : Is the one that allows abstracting from the activity lifecycle. It’s usually created when the activity is created, and dies when the activity finishes . If the activity needs to be recreated (a rotation for instance), the ViewModel will be kept alive.
  • LiveData : it’s an observable component. You subscribe to the changes and receive updates when its value is modified. As the LiveData is lifecycle aware too, you don’t need to unsuscribe.

So, if the activity is using a ViewModel, it needs to recover it like this:

val vm = ViewModelProviders.of(this)[NotificationsListViewModel::class.java]

That class of architecture components will check if the ViewModel exists, and otherwise it will create a new instance and return it.

The ViewModel would be something like this:

class NotificationsListViewModel : ViewModel() {
    val notificationsList = MutableLiveData<List>()
    ...
}

And then, you can observe the changes on that LiveData by doing:

vm.notificationsList.observe(this, Observer(::updateUI))

When the LiveData changes (by setting a new value to the value property, or calling postValue ), the updateUI function will be called. Here I’m using afunction reference.

A couple of extra random ideas:

  • ViewModelProviders.of function accepts both an activity or a fragment.
  • LiveData needs to be observed by a LifecycleOwner . You can implement that interface, or just use activities and fragments from the support library, which already implement it for you.

Cleaning up the use of Architecture Components

By making use of Kotlin features, we can convert the code above into something simpler and nicer.

The ViewModel

First thing that is a little convoluted is the way we recover the ViewModel . We need to call a static method and then pass the class of the ViewModel we want to use. This second point can give us a clue of what we may need. Rememberreified types? With them, we can use the class of the generic type inside a function. So that will fit perfectly here:

We can do something like this:

inline fun  getViewModel(activity: FragmentActivity): T {
    return ViewModelProviders.of(activity)[T::class.java]
}

And now, to use it:

val vm = getViewModel(this)
vm.notificationsList.observe(this, Observer(::updateUI))

We’ve improved this a little bit, but there’s still some room for improvement. As you see here, we are passing this to the function to refer to the activity. Why not using anextension function instead? That way, activities would have a new function to recover the ViewModel directly:

inline fun  FragmentActivity.getViewModel(): T {
    return ViewModelProviders.of(this)[T::class.java]
}
val vm = getViewModel()

Much nicer! The thing is, that most times, when we get a ViewModel will be to do something with it. For instance, subscribing to some LiveData properties. We can make use of theninja functions, and do:

with(getViewModel()){
    notificationsList.observe(this@NotificationsListActivity, Observer(::updateUI))    
}

But why don’t we avoid that step? If you use Architecture Components in your App, you will probably do it for all your activities, so it makes sense to create your own function with receiver:

withViewModel {
    notificationsList.observe(this@NotificationsListActivity, Observer(::updateUI))
}

Much easier to read! How is this implemented?

inline fun  FragmentActivity.withViewModel(body: T.() -> Unit): T {
    val vm = getViewModel()
    vm.body()
    return vm
}

It just uses the getViewModel and calls the body function, which behaves as an extension function of the generic type, so it can be called by the ViewModel . This is pretty awesome, right?

The LiveData

Everything starts looking great, but there’s still one line that looks pretty busy:

notificationsList.observe(this@NotificationsListActivity, Observer(::updateUI))

But why don’t we think about it the other way round? In fact, it’s the activity who is observing the notificationsList , so the original implementation is a little misleading in naming. We would want something like:

activity.observe(notificationsList) { /* Do something when value changes */ }

Again, thanks to extension functions we can do this quite easily. As you may remember, LiveData must be observed by a LifecycleOwner . So let’s make an extension function for it:

fun <T : Any, L : LiveData> LifecycleOwner.observe(liveData: L, body: (T?) -> Unit) {
    liveData.observe(this, Observer(body))
}

We are hiding the ugly code inside the function, and creating a nice API for it:

observe(notificationsList, ::updateUI)

The result

So now, mixing all the pieces together, we’ve gone from this:

val vm = ViewModelProviders.of(this)[NotificationsListViewModel::class.java]
vm.notificationsList.observe(this, Observer(::updateUI))

To this:

withViewModel {
    observe(notificationsList, ::updateUI)
}

It’s not a huge deal, but if we’re repeating this on all activities it’s nice having this functions that make the code cleaner and easier to use.

Extra: ViewModels with constructor arguments

I must admit I’ve been following the easy path here. Regular ViewModels are easy to use when they don’t receive any arguments. But things become more complicated if those ViewModels need arguments .

Imagine that the activity is receiving some info through its intent, and this needs to be used by the ViewModel. You could write this code:

val appPackage = intent.getStringExtra(APP_PACKAGE)
 
val factory = object : ViewModelProvider.Factory {
    override fun  create(modelClass: Class): T {
        return NotificationsListViewModel(appPackage) as T
    }
}
 
val viewModel = ViewModelProviders.of(this, factory)[NotificationsListViewModel::class.java]

Pretty complex, right? I’m sure you think we can do it better. In fact, the factory is just a class with one method, so this sounds quite a lot like a lambda 樂. So why don’t we create a new set of functions that just support a lambda that represents the factory?

The withViewModel one is simple. It gets an extra lambda that returns the generated object:

inline fun  FragmentActivity.withViewModel(
    crossinline factory: () -> T,
    body: T.() -> Unit
): T {
    val vm = getViewModel(factory)
    vm.body()
    return vm
}

The getViewModel now will wrap the instance of the factory, but based on the previous code it’s not very difficult either:

inline fun  FragmentActivity.getViewModel(crossinline factory: () -> T): T {
 
    val vmFactory = object : ViewModelProvider.Factory {
        override fun  create(modelClass: Class): U = factory() as U
    }
 
    return ViewModelProviders.of(this, vmFactory)[T::class.java]
}

The generics part is a bit more convoluted, and I’m not totally sure that I got the best simplest solution, but it works and it just needs to be written once, so it can be enough.

With that, we are creating a factory object that will use the lambda to return the proper value. After all this nasty work, we can do things much cleaner:

val appPackage = intent.getStringExtra(APP_PACKAGE)
 
withViewModel({ NotificationsListViewModel(appPackage) }) {
    observe(notificationsList, ::updateUI)
}

Quite cool, right?

Conclusion

As I’ve mentioned several times, thanks to extension functions we can make the interaction with the framework much simpler and easier to read , by encapsulating all the boilerplate inside them.

In this case, I covered some situations I found when working with basic Architecture Components, but you can imagine that this can be applied to virtually any pain points of framework APIs.

Have you already found places where this kind of code made your life easier? Have you come up with other extension functions to deal with Architecture Components code? Feel free to let me know in the comments!

Author: Antonio Leiva

I’m in love with Kotlin. I’ve been learning about it for a couple of years, applying it to Android and digesting all this knowledge so that you can learn it with no effort.

Twitter Google+ Linkedin Github

您可能感兴趣的

Android and access to the database I have a question about access to database. I have a database, but I can't see this database in data on my davice. On emulator in data I can see this ...
Android 动画之kotlin 属性动画 简述 在手机上去实现一些动画效果算是件比较炫酷的事情,因此Android系统在一开始的时候就给我们提供了两种实现动画效果的方式,逐帧动画(frame-by-frame animation)和补间动画(tweened animation)。逐帧动画的工作原理很简单,其实就是将一个完整...
android窗口绘制的总结 image 好久没有更新技术文了,年后一直忙于其他事情,空闲时间,就在反思,规划,谈一些人生感悟。今天终于提起笔来,继续在代码的世界里,埋头耕耘。 今日我来进行一个分享大会,主要分享的是view 窗口相关的流程讲解,相信下面的链接,会帮你建立起来...
SQLite的升级 sqliteDataBase的升级其实只涉及到创建数据库的那个类, 下面给出一个类的代码做范例然后做简单讲解 package com.example.pei.textdemo.sqlite_object;import android.content.Context; import...
Kotlin 资源大全 – 学 Kotlin 看这一篇教程就够了... 目录 介绍 官网及文档 中文社区 教程 & 文章 开源库和框架 Demo 其他 介绍 为什么要做这个? 今天凌晨的 Google I/O 上,Google 正式宣布官方支持 Kotlin. 为了...