Kotlin Android Extensions
Author | Yan Zhulanow |
In this tutorial we'll walk through the steps required to use the Kotlin Android Extensions plugin, enhancing the development experience with Android.
View Binding
Background
Every Android developer knows well the findViewById()
function. It is, without a doubt, a source of potential bugs and nasty code which is hard to read and support. While there are several libraries available that provide solutions to this problem, those libraries require annotating fields for each exposed View
.
The Kotlin Android Extensions plugin allows us to obtain the same experience we have with some of these libraries, without having to add any extra code.
In essence, this allows for the following code:
// Using R.layout.activity_main from the 'main' source set
import kotlinx.android.synthetic.main.activity_main.*
class MyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Instead of findViewById<TextView>(R.id.textView)
textView.setText("Hello, world!")
}
}
textView
is an extension property for Activity
, and it has the same type as declared in activity_main.xml
(so it is a TextView
).
Using Kotlin Android Extensions
Configuring the Dependency
In this tutorial we're going to be using Gradle but the same can be accomplished using either IntelliJ IDEA project structure or Maven. For details on setting up Gradle to work with Kotlin, see Using Gradle.
Android Extensions is a part of the Kotlin plugin for IntelliJ IDEA and Android Studio. You do not need to install additional plugins.
All you need is to enable the Android Extensions Gradle plugin in your module's build.gradle
file:
apply plugin: 'kotlin-android-extensions'
Importing synthetic properties
It is convenient to import all widget properties for a specific layout in one go:
import kotlinx.android.synthetic.main.<layout>.*
Thus if the layout filename is activity_main.xml
, we'd import kotlinx.android.synthetic.main.activity_main.*
.
If we want to call the synthetic properties on View
, we should also import kotlinx.android.synthetic.main.activity_main.view.*
.
Once we do that, we can then invoke the corresponding extensions, which are properties named after the views in the XML file. For example, for this view:
<TextView
android:id="@+id/hello"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
There will be a property named hello
:
activity.hello.text = "Hello World!"
Experimental Mode
Android Extensions plugin includes several experimental features such as LayoutContainer
support and a Parcelable
implementation generator. These features are not considered production ready yet, so you need to turn on the experimental mode in build.gradle
in order to use them:
androidExtensions {
experimental = true
}
LayoutContainer
Support
Android Extensions plugin supports different kinds of containers. The most basic ones are Activity
, Fragment
and View
, but you can turn (virtually) any class to an Android Extensions container by implementing the LayoutContainer
interface, e.g.:
import kotlinx.android.extensions.LayoutContainer
class ViewHolder(override val containerView: View) : ViewHolder(containerView), LayoutContainer {
fun setup(title: String) {
itemTitle.text = "Hello World!"
}
}
Note that you need to turn on the experimental flag to use LayoutContainer
.
Flavor Support
Android Extensions plugin supports Android flavors. Suppose you have a flavor named free
in your build.gradle
file:
android {
productFlavors {
free {
versionName "1.0-free"
}
}
}
So you can import all synthetic properties for the free/res/layout/activity_free.xml
layout by adding this import:
import kotlinx.android.synthetic.free.activity_free.*
In the experimental mode, you can specify any variant name (not only flavor), e.g. freeDebug
or freeRelease
will work as well.
View Caching
Invoking findViewById()
can be slow, especially in case of huge view hierarchies, so Android Extensions tries to minimize findViewById()
calls by caching views in containers.
By default, Android Extensions adds a hidden cache function and a storage field to each container (Activity
, Fragment
, View
or a LayoutContainer
implementation) written in Kotlin. The method is pretty small so it does not increase the size of APK much.
In the following example, findViewById()
is only invoked once:
class MyActivity : Activity()
fun MyActivity.a() {
textView.text = "Hidden view"
textView.visibility = View.INVISIBLE
}
However in the following case:
fun Activity.b() {
textView.text = "Hidden view"
textView.visibility = View.INVISIBLE
}
We wouldn't know if this function would be invoked on only activities from our sources or on plain Java activities also. Because of this, we don’t use caching there, even if MyActivity
instance from the previous example is passed as a receiver.
Changing View Caching Strategy
You can change the caching strategy globally or per container. This also requires switching on the experimental mode.
Project-global caching strategy is set in the build.gradle
file:
androidExtensions {
defaultCacheImplementation = "HASH_MAP" // also SPARSE_ARRAY, NONE
}
By default, Android Extensions plugin uses HashMap
as a backing storage, but you can switch to the SparseArray
implementation, or just switch off caching. The latter is especially useful when you use only the Parcelable part of Android Extensions.
Also, you can annotate a container with @ContainerOptions
to change its caching strategy:
import kotlinx.android.extensions.ContainerOptions
@ContainerOptions(cache = CacheImplementation.NO_CACHE)
class MyActivity : Activity()
fun MyActivity.a() {
// findViewById() will be called twice
textView.text = "Hidden view"
textView.visibility = View.INVISIBLE
}
Parcelable
Starting from Kotlin 1.1.4, Android Extensions plugin provides Parcelable implementation generator as an experimental feature.
Enabling Parcelable support
Apply the kotlin-android-extensions
Gradle plugin as described above and turn on the experimental flag.
How to use
Annotate the class with @Parcelize
, and a Parcelable
implementation will be generated automatically.
import kotlinx.android.parcel.Parcelize
@Parcelize
class User(val firstName: String, val lastName: String, val age: Int): Parcelable
@Parcelize
requires all serialized properties to be declared in the primary constructor. Android Extensions will issue a warning on each property with a backing field declared in the class body. Also, @Parcelize
can't be applied if some of the primary constructor parameters are not properties.
If your class requires more advanced serialization logic, you can write it inside a companion class:
@Parcelize
data class Value(val firstName: String, val lastName: String, val age: Int) : Parcelable {
private companion object : Parceler<User> {
override fun User.write(parcel: Parcel, flags: Int) {
// Custom write implementation
}
override fun create(parcel: Parcel): User {
// Custom read implementation
}
}
}
Supported Types
@Parcelize
supports a wide range of types:
- Primitive types (and its boxed versions);
- Objects and enums;
String
,CharSequence
;Exception
;Size
,SizeF
,Bundle
,IBinder
,IInterface
,FileDescriptor
;SparseArray
,SparseIntArray
,SparseLongArray
,SparseBooleanArray
;- All
Serializable
(yes,Date
is supported too) andParcelable
implementations; - Collections of all supported types:
List
(mapped toArrayList
),Set
(mapped toLinkedHashSet
),Map
(mapped toLinkedHashMap
);- Also a number of concrete implementations:
ArrayList
,LinkedList
,SortedSet
,NavigableSet
,HashSet
,LinkedHashSet
,TreeSet
,SortedMap
,NavigableMap
,HashMap
,LinkedHashMap
,TreeMap
,ConcurrentHashMap
;
- Also a number of concrete implementations:
- Arrays of all supported types;
- Nullable versions of all supported types.
Custom Parceler
s
Even if your type is not supported directly, you can write a Parceler
mapping object for it.
class ExternalClass(val value: Int)
object ExternalClassParceler : Parceler<ExternalClass> {
override fun create(parcel: Parcel) = ExternalClass(parcel.readInt())
override fun ExternalClass.write(parcel: Parcel, flags: Int) {
parcel.writeInt(value)
}
}
External parcelers can be applied using @TypeParceler
or @WriteWith
annotations:
// Class-local parceler
@Parcelable
@TypeParceler<ExternalClass, ExternalClassParceler>()
class MyClass(val external: ExternalClass)
// Property-local parceler
@Parcelable
class MyClass(@TypeParceler<ExternalClass, ExternalClassParceler>() val external: ExternalClass)
// Type-local parceler
@Parcelable
class MyClass(val external: @WriteWith<ExternalClassParceler>() ExternalClass)