Analyzing the Internals of Kotlin's Android Synthetic Import (Part 2)
This is the second part of the “Analyzing the Internals of Kotlin’s Android Synthetic Import” mini-series. If you haven’t already, check out Part 1
On the previous article, we saw how by importing the XML files, we had some caching and findViewById
logic automatically implemented, because of the instructions that the plugin provides to the compiler.
On this article, we will learn how our layout files are processed, so that as soon as we create a widget with an ID, it is automatically available as a field for us on our Kotlin code.
Analyzing XML layouts
1. Selecting XML files to evaluate
Android projects have many XML files of different types, however for synthetic imports we only care about layouts. The class AndroidLayoutXmlFileManager has the following logic:
- Takes as input the Android project itself, and evaluates each variant . Each productFlavor is considered a variant. If you have experimental options enabled, then each buildType generates a variant as well.
- It looks at the
res
(resources) folder, and gets all the files. - It filters those files, considering only those
.xml
files that are on thelayout
folder.
2. Parsing XML files / components
Once the plugin knows which files to evaluate, it evaluates the XML content using AndroidXmlVisitor.
- It iterates over each XML tag
- It stops evaluating it if it is a widget that is not supported.
val IGNORED_XML_WIDGET_TYPES = setOf(
"requestFocus", "merge", "tag", "check", "blink")
- It stops evaluating the tag if it does not have an
id
(without an id, synthetic import property cannot be generated) - It determines the widget type by:
- checking
class
attribute, if available.
<view class="android.widget.Button" ... />
- if not available, it uses the XML tag name
<Button .../>
- checking
3. Generating the properties
Some functions declared on syntheticDescriptorGeneration.kt have the logic to create properties for different components:
genPropertyForWidget
genPropertyForFragment
Behind the scenes, they are both declaring properties this way:
val property = object : AndroidSyntheticProperty, PropertyDescriptorImpl( ..,
containingDeclaration, ...,
CallableMemberDescriptor.Kind.SYNTHESIZED,
Modality.FINAL,
Visibilities.PUBLIC
) {
property.initialize(getter, null /* setter */)
As you can see, those properties are read-only.
4. Generating packages to import
There’s a class called AndroidPackageFragmentProviderExtension that does:
createPackageFragment(packageFqName, false)
createPackageFragment(packageFqName + ".view", true)
So when you create a layout, there are two possible ways to import them:
import kotlinx.android.synthetic.main.my_layout.*
import kotlinx.android.synthetic.main.my_layout.view.*
(note: in case it was not clear,
my_layout
is just a random layout name)
Let’s evaluate each import one by one.
…my_layout.*
It allows you to call the properties right away. This can only be done from classes that extend:
- View
- LayoutContainer
import kotlinx.android.synthetic.main.my_layout.*
class MyActivity : Activity() {
fun onCreate(...) {
my_text_view.text = "Hello world!"
}
}
Makes sense, considering that on the previous article we learned that internally those properties will call findViewById
. It wouldn’t be possible to do that from a non-View or non-container class.
…my_layout.view.*
If you are not within a view, but you have a reference to one, you can use this import
import kotlinx.android.synthetic.main.my_layout.view.*
class NotAView {
fun greet(layout: View) {
layout.my_text_view.text = "Hello world!"
}
}
It basically allows us to call synthetic properties over any View
(but as an “extension property” over the view, instead of calling it directly like on the previous case).
Be careful, because that means that you will be able to call properties even on view in which you would normally not call findViewById
.
So technically this is possible… (but don’t do it, it makes no sense)
fun greet(layout: View) {
layout.my_text_view
.my_text_view
.my_text_view
.text = "Hello world!"
}
Or even this, that will crash on runtime
fun greet(layout: View) {
layout.my_text_view
.layout
.my_text_view
.text = "Hello world!"
}
(because a
TextView
is not aViewGroup
, it can’t find / contain any other View except itself).
Unfortunely, there is not much that can’t be done to prevent that. findViewById
is a method defined at View
level, however that’s a functionality that works best only with ViewGroups and other view containers.
What have we learned?
While writing this post I kept digging more into the plugin code and played even more with synthetic imports. I was able to come up with answers to some doubts I used to have, and now I’ll share those answers with you.
How is the plugin able to generate fields on-the-fly, without compilation?
The plugin registers a listener (AndroidPsiTreeChangePreprocessor) that reacts to any modification on a file inside res/layout
folder. This includes:
- When the file itself is modified somehow: layout file added / removed / moved / replaced / renamed (so the imports can be updated)
- When a widget
id
orclass
is modified. It doesn’t react to any other widget-styling property, that would be unnecessary work.
What else is the plugin doing behind the scenes?
I think we have covered most of the important parts. However building a plugin is no easy task, there is a lot of code that is making sure that autocompletion, inference, indexing and other stuff works properly.
If you are interested in how to build a plugin, I suggest you to watch Writing Your First Kotlin Compiler Plugin by Kevin Most. It blew my mind away.
How much does Synthetic imports impact performance?
Now that we know how the plugin works, we can evaluate performance from 3 different angles:
- Runtime performance: when your application is executed, then performance will be just as fast as
findViewById
. Given that it implements a caching layer, runtime performance impact will be insignificant. - Compilation performance: on the previous part of this article, we saw how the plugin is adding compiler instructions to generate the code that makes synthetic imports work. This means that compilation time will indeed be impacted a bit.
- IDE performance: on this article, we saw how XML layouts are analyzed. All that work to evaluate files and its content is performed while we’re coding. It will NOT impact compilation time. However, your IDE will be constantly performing tasks to keep everything updated, so IDE performance may be impacted a bit too.
I hope that is enough for you, because (unfortunately) benchmarking or executing exhaustive performance tests are not in the scope of this article!
What about include tags inside our XML files?
The <include>
tags do not generate properties because they don’t have an id
nor a class
, so they are not processed.
If you want to reference the inner widgets of the <include>
block, you would have to use the synthetic imports from both the parent layout, and the included layout, in order to access all the components.
What will happen if I have different layouts per flavor?
The plugin will evaluate the layouts files that are available in your project for your current configurations.
So for instance, if you have a “premium” productFlavor, and create a activity_main.xml
file at both main
and premium
folders, then you have access to two imports:
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.premium.activity_main.*
The last one will only be available when that particular flavor is selected - if you are compiling another flavor, then the import will not be found!
Is Synthetic import compatible with Java?
No. This is a Kotlin-compiler plugin and part of Kotlin android extensions. Its benefits are only available while you code in Kotlin.
Overview
I hope that by the end of this mini-series, you now understand a little bit more about how this plugin works.
- The plugin analyses our layout files, and creates the appropriate fields so that we can import them right away.
- Synthetic Imports just call the good old
findViewById
behind the scenes. To do so, it dynamically adds some extra code during compilation. - In order to avoid performance issues, the plugin has a built-in caching mechanism.
This is the second and last part of this Synthetic Import mini-series. Thanks for reading and share it if you liked it!