How our Android engineers automated the removal of Kotlin synthetics

Read the article

Our Mobile Platform team recently faced the challenge of having to remove all synthetic view properties—a feature from the deprecated Kotlin Android Extensions Gradle plugin. These synthetic properties were extremely widespread in our codebase, used in almost 600 screens! This post explains how we built a tool to automate the task, and the trade-offs we had to consider along the way.

Synthetics are set to be removed along with the release of Kotlin 1.8 (which will ship before the end of the year). We needed to remove them from our app, or face being stuck with an old Kotlin version. And getting stuck on an old version of Kotlin isn’t an option. For a start, it would prevent us upgrading other dependencies like Compose and KSP.

Google recommends replacing synthetic properties by either migrating to Compose, or by refactoring View-based screens to use View Binding. At Monzo, we’re already migrating our app to Compose, but this migration will realistically take years to fully complete. And since we've already chosen Compose, the idea of introducing View Binding and manually refactoring hundreds of older screens seemed like pointless churn.

A pragmatic solution 🔧

The most appealing option to use was to automate the migration. Writing a tool to migrate to Compose wasn’t even really considered (it seems impossible to do well). Instead we thought it might be possible to automate a migration to View Binding, so we started analysing how that refactor might look in various parts of our app: like fragments, activities, custom views, and adapters. Handling the common cases looked doable, but we kept running into edge cases that the tool would need to account for, increasing the scope of the work significantly. And while our Mobile Platform team could have invested a lot of time into building this out, we started to consider simpler options.

A more pragmatic approach was to write a tool that would replace each synthetic property by adding hardcoded copy of that property in the file that uses it. The new properties would match the signature of their synthetic counterpart exactly meaning we wouldn’t need to make any source code modifications aside from:

  • adding these new hardcoded properties at the end of the containing file

  • deleting all synthetic view import statements

To paint a clearer picture, here’s a full git-diff for one of our Fragments after our tool ran:

import android.view.View
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.monzo.commonui.findById
+import com.monzo.design.nosymbol.MonzoToolbar
import com.monzo.virtualcards.R
-import kotlinx.android.synthetic.main.vc_inactive_fragment.*

class InactiveVirtualCardsFragment : Fragment(R.layout.vc_inactive_fragment) {

}
+
+private val Fragment.vcInactiveRecyclerView inline get() = findById<RecyclerView>(R.id.vcInactiveRecyclerView)
+private val Fragment.vcInactiveToolbar inline get() = findById<MonzoToolbar>(R.id.vcInactiveToolbar)

The elephant in the room 🐘

To address the elephant in the room; these migrated properties are worse than their synthetic counterparts. There are 3 important issues with this design that need consideration.

1. The most glaring issue is that we no longer get the compile-time type safety that synthetic properties offer. If we change the underlying XML then our hardcoded property isn’t also automatically updated in our code. For us, we ultimately decided that this was an acceptable risk. Not only are all new screens at Monzo written in Compose, but whenever we add content to non-Compose screens, we tend to convert them to Compose while we’re in the neighbourhood anyway.

2. Another less obvious issue is that synthetic properties automatically cache views after they are found and our new properties don’t. This one is fairly easy to fix by adding some caching logic inside of our findById function.

3. The final issue is the most subtle: the non-explicit return type of this property. It turns out that synthetic view properties are platform types, meaning that the return type is either T or T?. There’s no way for our tool to know if the view can be null at a given call-site or not, so unfortunately we need our custom findById function to also return a platform type 🤢.

IntelliJ to the rescue 🦸

In order to automatically add these properties in our Kotlin files, we needed a tool that could help us:

  • detect all synthetic view properties in the codebase

  • extract their corresponding view types from the XML

  • make the necessary code modifications to our files

As it turns out, an IntelliJ Plugin is the perfect tool for solving all 3 of these requirements thanks to Program Structure Interface (PSI). You can think of PSI like an Abstract Syntax Tree (AST), but with additional superpowers.

A regular AST won’t be able to distinguish a synthetic view property from any other property because they’re syntactically equivalent. On the other hand, PSI allows us to infer whether or not a property is specifically an AndroidSyntheticProperty.

PSI elements can be “resolved” (think cmd+clicking on an element in Android Studio). Resolving an element just returns its declaration. In the case of a synthetic view property, it is going to return an XML PSI element: the corresponding view. We can then look at this element to extract its view type. That’s all the information we need!

PSI is mutable: we can add and remove elements from the tree. This is how IntelliJ is able to write code for you (for example, automatically generating equals/hashCode).

Our plugin source code 🔌

Our migration plugin was built as a mass migration tool. It runs once and applies the changes everywhere. We ran it a few months ago and have had zero issues from it. Considering we turned months worth of churn into a couple days of hacking a plugin together, it feels like we made the right choice for the position we were in! Now our engineers can continue to focus on building exciting new features, or migrating older screens into Compose 🎉

If your team is in also stuck with synthetic imports and are looking for inspiration, we're open sourcing our plugin code to help you build something similar (or just use ours outright)! You can find the our plugin code and instructions on how to run it on GitHub.

Join our mobile engineering team 🙋‍♀️

We are currently looking for more mobile engineers to join our growing team. We have several non-graduates, only some of us studied Computer Science, some of us have worked in huge companies, some have only ever worked in startups, and others are former consultants. As long as you enjoy learning new things, we’d love to talk to you. Take a look at the links below for full role descriptions and to apply: