1. 程式人生 > >Architecture Components: Hack the API by using Kotlin extensions

Architecture Components: Hack the API by using Kotlin extensions

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:

1 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:

Want to learn Kotlin?

Check my free guide to create your first project in 15 minutes!

1234 classNotificationsListViewModel:ViewModel(){val notificationsList=MutableLiveData<List<Notification>>()...}

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

1 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 a function 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. Remember reified 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:

123 inline fun<reifiedT:ViewModel>getViewModel(activity:FragmentActivity):T{returnViewModelProviders.of(activity)[T::class.java]}

And now, to use it:

12 val vm=getViewModel<NotificationsListViewModel>(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 an extension function instead? That way, activities would have a new function to recover the ViewModel directly:

123 inline fun<reifiedT:ViewModel>FragmentActivity.getViewModel():T{returnViewModelProviders.of(this)[T::class.java]}
1 val vm=getViewModel<NotificationsListViewModel>()

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 the ninja functions, and do:

123 with(getViewModel<NotificationsListViewModel>()){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:

123 withViewModel<NotificationsListViewModel>{notificationsList.observe(this@NotificationsListActivity,Observer(::updateUI))}

Much easier to read! How is this implemented?

12345 inline fun<reifiedT:ViewModel>FragmentActivity.withViewModel(body:T.()->Unit):T{val vm=getViewModel<T>()vm.body()returnvm}

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:

1 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:

1 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:

123 fun<T:Any,L:LiveData<T>>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:

1 observe(notificationsList,::updateUI)

The result

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

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

To this:

123 withViewModel<NotificationsListViewModel>{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:

123456789 val appPackage=intent.getStringExtra(APP_PACKAGE)val factory=object:ViewModelProvider.Factory{override fun<T:ViewModel?>create(modelClass:Class<T>):T{returnNotificationsListViewModel(appPackage)asT}}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