· 8 min read
Converting Plaid to Kotlin: Lessons learned (Part 1)
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
People often ask me about the advantages of writing Android Apps using Kotlin. The thing is that I never directly converted an Android App from Java to Kotlin, so it’s always a difficult question to answer. Explaining a lot of abstract features without putting them into context is not always the best way to tell about the goodness of a language.
So, after testing Plaid, the app developed by Nick Butcher, and being marvelled about the awesome look and transitions of the App, I wanted to know more about it. And what a better way than rewriting the App into Kotlin?
I just converted HomeActivity and wanted to compare the resulting code, what things are drastically improved and, of course, give you access to the code so that you can extract your own conclusions. My first disclaimer is that, though it may happen or not, my main goal is not converting the whole App to Kotlin. It’s quite big, so not sure if I’ll find the time (or the need) to do it.
View Binding
Nick decided to use Butterknife to recover views, which is an excellent solution for Java code, but Kotlin provides Kotlin Android Extensions, which automatically binds them to the activity. So that way, we are saving all the @Bind
lines.
We have a couple of extra things, however, that Butterknife provides, such as onClick
and resources binding. For the first one, it’s so simple in Kotlin it doesn’t really add much boilerplate. In onCreate
we do:
fab.onClick { fabClick() }
I’m using an Anko function here, but using setOnClickListener
will be as easy.
Regarding the columns
value that is recovered, it’s only necessary in onCreate
, so I moved the declaration there, but a similar thing can be achieved with property delegation:
private val columns by lazy { resources.getInteger(R.integer.num_columns) }
The lazy
delegate delays the value assignment until the property is called, when the activity is already instantiated and we have access to resources
.
Properties declaration
In Java, we have to delay the fields assignment until our activity is ready. But properties in Kotlin need a value since the very beginning if we don’t want to deal with unnecessary nulls and mutable variables. So it’s very common to do the assignment directly in the declaration.
Once more, we have the problem of needing a context for many of these properties. So lazy
will be really useful here:
private val dribbblePrefs by lazy { DribbblePrefs.get(ctx) }
private val designerNewsPrefs by lazy { DesignerNewsPrefs.get(ctx) }
Of course, these declarations can be as complex as we need. DataManager
for instance, needs to extend a class and override a method:
private val dataManager by lazy { object : DataManager(this, filtersAdapter) {
override fun onDataLoaded(data: MutableList<out PlaidItem>?) {
feedAdapter.addAndResort(data)
checkEmptyState()
}
} }
This way, we can see how items are declared just in the declaration section, instead of having to do it in the middle of onCreate
. And as plus, we make sure that by the time we use them, they won’t be null, preventing unnecessary NullPointerException
s.
Use of standard functions
Kotlin standard library provides a good set of functions that are really useful. You can check part 1 and part 2 of Cedric’s articles about Standard Library.
We, for instance, have the apply()
function, which works as an extension function for the object that calls it, and returns the same object. A complete example of that is the way I inflate the no_filters
ViewStub
. First, it is done lazy
so that the stub is not inflated until it is called, and second, it applies an initialisation over the result of this inflation:
private val noFilterEmptyText by lazy {
// create the no filters empty text
(stub_no_filters.inflate() as TextView).apply {
onClick { drawer.openDrawer(GravityCompat.END) }
}
}
As you can see, the function is applied over the result of the inflation, and apply()
will return that same object, which is assigned to noFilterEmptyText
. Another good example is just inside that code. A SpannableStringBuilder
is a perfect candidate for this:
text = SpannableStringBuilder(emptyText).apply {
// show an image of the filter icon
setSpan(ImageSpan(ctx, R.drawable.ic_filter_small, ImageSpan.ALIGN_BASELINE), filterPlaceholderStart, filterPlaceholderStart + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// make the alt method (swipe from right) less prominent and italic
setSpan(ForegroundColorSpan(ContextCompat.getColor(ctx, R.color.text_secondary_light)), altMethodStart, emptyText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(StyleSpan(Typeface.ITALIC), altMethodStart, emptyText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
The apply()
function is also useful to do initialisations for our views. First, it visibly divides the code in blocks, which makes it easier to read. And second, the code is executed as if it was inside the class, so we can use all the public methods of the object without prepending the name of the object.
stories_grid.apply {
adapter = feedAdapter
val columns = resources.getInteger(R.integer.num_columns)
val gridManager = GridLayoutManager(ctx, columns).apply {
setSpanSizeLookup { pos -> if (pos == feedAdapter.dataItemCount) columns else 1 }
}
layoutManager = gridManager
addOnScrollListener { recycler, dx, dy ->
gridScrollY += dy
if (gridScrollY > 0 && toolbar.translationZ != -1f) {
toolbar.translationZ = -1f
} else if (gridScrollY == 0 && toolbar.translationZ != 0f) {
toolbar.translationZ = 0f
}
}
addOnScrollListener(object : InfiniteScrollListener(gridManager, dataManager) {
override fun onLoadMore() = dataManager.loadAllDataSources()
})
setHasFixedSize(true)
}
Here, the adapter, layout manager and listeners are added to the RecyclerView
. You can also see the use of the synthetic properties that are generated for Java getters and setters. Instead of doing setLayoutManager(gridManager)
, we can just do layoutManager = gridManager
.
Lambdas
Though the use of functions is everywhere, there are some evident simplifications here and there. We can use some calls by passing lambdas instead of creating the objects Java would need. A very nice example is the closeDrawerRunnable
. This is the code you need in Java:
final Runnable closeDrawerRunnable = new Runnable() {
@Override
public void run() {
drawer.closeDrawer(GravityCompat.END);
}
};
...
drawer.postDelayed(closeDrawerRunnable, 2000);
And the same in Kotlin:
val closeDrawerRunnable = { drawer.closeDrawer(GravityCompat.END) }
...
drawer.postDelayed(closeDrawerRunnable, 2000)
We also saw the example of onClick
before, and it also helps with setOnApplyWindowInsetsListener
:
drawer.setOnApplyWindowInsetsListener { v, insets -> ... }
4 Dealing with nullity
That’s another great feature of Kotlin, and improves the way we deal with nullity, and the amount of code we need for that. For instance in animateToolbar
method, this is what we have in Java, to ensure we are dealing with a non-null TextView
:
View t = toolbar.getChildAt(0);
if (t != null && t instanceof TextView) {
TextView title = (TextView) t;
...
}
We can do this in Kotlin:
val title = toolbar.getChildAt(0) as? TextView
title?.apply { ... }
With the advantage that now the code inside apply
behaves as an extension function, so you don’t need to write title
anymore. The first line tries to convert whatever is returned into a TextView
. If the child is null or is not a TextView
, title
will be null. The second line would behave the same as if (title != null) title.apply { }
. The apply()
function is only executed if title is not null.
You can find many more improvements throughout the code of the Activity. It’s not very beautiful because this activity is dealing with too much (there is even an instantiation of a Retrofit
client around), but it’s a good start to understand how different developing with Kotlin is.
Kotlin vs Java: Numbers
Finally, I want to share some numbers, of course as inaccurate as all external factors can generate, but it could help us get an idea. Not everything is good in Kotlin, compilation times for instance are a thing that needs to be improved.
KotlinJavaComparison
Line count | 576 | 702 | -22% | Character count | 24001 | 30589 | -27% | Clean compilation | 1m 40s | 1m 5s | +67% | Compilation after 1 line change | 29s | 10s | +190% | APK size | 4.7MB | 4.1MB | +14% | Method count | 41615 | 30129 | +38% |
The main problem Kotlin compiler has right now is that it can’t do partial compilations, so if we change one line, it needs to recompile all the classes. These things will probably change in the future, but that’s what we have now.
As you can see, Kotlin + Anko are adding around 11000 methods. The Anko library is quite huge (3000+ methods) and we could think of just creating our own functions if we are not using its core. Here it is the comparison:
Conclusion
Programming with Kotlin is delightful. You can achieve a lot more with less amount of code. This example could be improved a lot more if the whole App was done using Kotlin, because we could get rid of more boilerplate. But it’s a good way to understand some parts where Kotlin help us to improve readability and save some code.
While I continue converting the App to Kotlin code, I’ll probably find some more interesting things to tell. So stay tuned for new articles!. In the meanwhile, you can continue learning Kotlin with my book and the rest of articles about Kotlin. And of course, you can review the complete HomeActivity.