· 6 min read
Converting Plaid to Kotlin: Lessons learned (Part 2)
Warning: The Plaid project has changed a lot since I wrote this, but here there are still some useful ideas that can help you in your own projects to start thinking the “Kotlin” way
We saw pretty amazing improvements in the first article thanks to the use of Kotlin in an activity. But the thing is that these kind of classes are not the best candidates to notice the benefits because they are mostly overriding methods and doing a couple of things, which it still inevitably needs some boilerplate.
I’ve continued porting the App to Kotlin (you can see all the changes in the repo), and some interesting things are arising. I want to focus today in the transformation of a specific class: DataManager
. One spoiler, the size of the class has been reduced from 422 lines to 177. And, in my opinion, is much easier to understand.
When
The first big thing we see when taking a look at the class is a huge if/else
inside loadSource
class. This could have been a switch
in first instance, which could have improved readability, but it would’ve kept being a little hard to understand anyway. In Kotlin, we can use when expression, which is the most similar thing to switch
in Java, but much more powerful. Conditions can be as complex as we need, and are always a good substitute for if/else
. In this case we are using one of its simplest versions, but it helps this method look better:
when (source.key) {
SourceManager.SOURCE_DESIGNER_NEWS_POPULAR -> loadDesignerNewsTopStories(page)
SourceManager.SOURCE_DESIGNER_NEWS_RECENT -> loadDesignerNewsRecent(page)
SourceManager.SOURCE_DRIBBBLE_POPULAR -> loadDribbblePopular(page)
SourceManager.SOURCE_DRIBBBLE_FOLLOWING -> loadDribbbleFollowing(page)
SourceManager.SOURCE_DRIBBBLE_USER_LIKES -> loadDribbbleUserLikes(page)
SourceManager.SOURCE_DRIBBBLE_USER_SHOTS -> loadDribbbleUserShots(page)
SourceManager.SOURCE_DRIBBBLE_RECENT -> loadDribbbleRecent(page)
SourceManager.SOURCE_DRIBBBLE_DEBUTS -> loadDribbbleDebuts(page)
SourceManager.SOURCE_DRIBBBLE_ANIMATED -> loadDribbbleAnimated(page)
SourceManager.SOURCE_PRODUCT_HUNT -> loadProductHunt(page)
else -> when (source) {
is Source.DribbbleSearchSource -> loadDribbbleSearch(source, page)
is Source.DesignerNewsSearchSource -> loadDesignerNewsSearch(source, page)
}
}
Though this is not the case, when
is an expression, so it can return a value, which can be really useful.
Dealing with maps
Though this is not an example of a huge line reduction, it is interesting to see how we can deal with maps in Kotlin. This is how getNextPageIndex
looks in Java:
private int getNextPageIndex(String dataSource) {
int nextPage = 1; // default to one – i.e. for newly added sources
if (pageIndexes.containsKey(dataSource)) {
nextPage = pageIndexes.get(dataSource) + 1;
}
pageIndexes.put(dataSource, nextPage);
return nextPage;
}
How this looks in Kotlin?
private fun getNextPageIndex(dataSource: String): Int {
val nextPage = 1 + pageIndexes.getOrElse(dataSource) { 0 }
pageIndexes += dataSource to nextPage
return nextPage
}
We have a couple of interesting things here. The first one, is the function getOrElse
, that allows to return a default value if an element is not found in the map:
val nextPage = 1 + pageIndexes.getOrElse(dataSource) { 0 }
Thanks to this, we save the condition that checks it. But another even more interesting thing is how we add a new item to the map. Maps in Kotlin have implemented + operator, so we can add a new item just doing map = map + Pair
, which is the same as map += Pair
. This is how it would be:
pageIndexes += Pair(dataSource, nextPage)
But as I probably told before, we can make use of to
, an infix function that returns a Pair
. So the previous line is the same as:
pageIndexes += dataSource to nextPage
Converting callbacks into lambdas
Here it is where the magic really happens. There is a lot of repetitive code in loader classes. Most of them have the same structure that is duplicated once and another in all the calls to the API. First, a Retrofit callback is created. If the request is successful, it extracts the necessary information from the result and does whatever it needs with it. It finally calls the “data loaded” listener. In any case (success or failure), the loadingCount
is updated.
Just an example:
private void loadDesignerNewsTopStories(final int page) {
getDesignerNewsApi().getTopStories(page, new Callback<StoriesResponse>() {
@Override
public void success(StoriesResponse storiesResponse, Response response) {
if (storiesResponse != null && sourceIsEnabled(SourceManager.SOURCE_DESIGNER_NEWS_POPULAR)) {
setPage(storiesResponse.stories, page);
setDataSource(storiesResponse.stories, SourceManager.SOURCE_DESIGNER_NEWS_POPULAR);
onDataLoaded(storiesResponse.stories);
}
loadingCount.decrementAndGet();
}
@Override
public void failure(RetrofitError error) {
loadingCount.decrementAndGet();
}
});
}
This code is rather difficult to understand if it’s not analyzed. We can create a callback function that creates the Retrofit callback and implements the structure of the previous callback. The only different thing it does is to extract the necessary info from the result:
private inline fun <T> callback(page: Int, sourceKey: String, crossinline extract: (T) -> List<PlaidItem>) = retrofitCallback<T> { result, response ->
if (sourceIsEnabled(sourceKey)) {
val items = extract(result)
setPage(items, page)
setDataSource(items, sourceKey)
onDataLoaded(items)
}
}
It does exactly the same: checks if the source is enabled, and if it is, it will extract the items from the result, call setPage
, setDataSource
and onDataLoaded
. The implementation of retrofitCallback
is just to make the previous function simpler, but it could be omitted:
private inline fun <T> retrofitCallback(crossinline code: (T, Response) -> Unit): Callback<T> = object : Callback<T> {
override fun success(t: T?, response: Response) {
t?.let { code(it, response) }
loadingCount.decrementAndGet()
}
override fun failure(error: RetrofitError) {
loadingCount.decrementAndGet()
}
}
The inline
modifier is a way to optimize functions that have other functions as parameters. When a function includes a lambda, the translation is equivalent to an object which has a function that implements that code. If we use inline
, however, the lambda will be substituted by its code whenever it’s called. If the lambda returns a value, we also have to use crossinline
. Check Kotlin reference for more info.
Both functions are generic so that they can accept any required type. With this, the previous request looks like this:
private fun loadDesignerNewsTopStories(page: Int) {
designerNewsApi.getTopStories(page, callback(page, SourceManager.SOURCE_DESIGNER_NEWS_POPULAR) { it.stories })
}
The function calls to getTopStories
, and creates a callback that receives the page, the key for the source, and a function that gets the stories from the result. The similar structure works for the rest of calls, but they can do whatever they need with the result. For instance, there’s another response that needs to be modified to include the user:
private fun loadDribbbleUserShots(page: Int) = dribbbleLogged {
val user = dribbblePrefs.user
dribbbleApi.getUserShots(page, DribbbleService.PER_PAGE_DEFAULT, callback(page, SourceManager.SOURCE_DRIBBBLE_USER_SHOTS) {
// this api call doesn't populate the shot user field but we need it
it.apply { forEach { it.user = user } }
})
}
As you can see, the previous one also requires to be logged to Dribbble. dribbbleLogged
will be the function in charge of checking that and doing something else if it isn’t.
private inline fun dribbbleLogged(code: () -> Unit) {
if (dribbblePrefs.isLoggedIn) {
code()
} else {
loadingCount.decrementAndGet()
}
}
Conclusion
This part has shown the power of lambdas and the use of functions as first-class citizens. The reduction of code is huge (238% smaller), but that’s not the most important improvement. The code is now much easier to read and much less prone to errors. Don’t forget to take a look at the latest commits in my Plaid fork at Github.