How to use Android Frameworks Using Annotation Processing?
Annotation Processing: Android Frameworks
This tutorial describes how to use in Kotlin popular Android frameworks and libraries that rely on annotation processing.
The Android world has many popular frameworks simplifying development. You can use the same frameworks if you develop in Kotlin, often as easily as you'd do that in Java. This tutorial provides examples and highlights the differences in settings.
We'll look at Dagger, Butterknife, Data Binding, Auto-parcel and DBFlow (other frameworks can be set up similarly). All these frameworks work through annotation processing: you annotate the code to have the boiler-plate code generated for you. Annotations allow to hide all the verbosity and keep your code simple, and if you need to understand what actually happens at runtime, you can look at the generated code. Note that all these frameworks generate source code in Java, not Kotlin.
In Kotlin you specify the dependencies in a similar to Java way using Kotlin Annotation processing tool(
kapt
) instead of annotationProcessor
.Dagger
Dagger is a dependency injection framework. If you're not familiar with it yet, you can read its user's guide. We've converted the coffee example described in this guide into Kotlin, and you can find the result here. The Kotlin code looks pretty much the same; you can browse the whole example in one file.
As in Java, you use
@Inject
to annotate the constructor used by Dagger to create instances of a class. Kotlin has a short syntax for declaring a property and a constructor parameter at the same time. To annotate the constructor, use the constructor
keyword explicitly and put the @Inject
annotation before it:class Thermosiphon
@Inject constructor(
private val heater: Heater
) : Pump {
// ...
}
Annotating methods looks absolutely the same. In the example below
@Binds
determines that a Thermosiphon
object is used whenever a Pump
is required, @Provides
specifies the way to build a Heater
, and @Singleton
says that the same Heater
should be used all over the place:@Module
abstract class PumpModule {
@Binds
abstract fun providePump(pump: Thermosiphon): Pump
}
@Module(includes = arrayOf(PumpModule::class))
class DripCoffeeModule {
@Provides @Singleton
fun provideHeater(): Heater = ElectricHeater()
}
@Module
-annotated classes define how to provide different objects. Note that when you pass an annotation argument as a vararg argument, you have to explicitly wrap it into arrayOf
, like in @Module(includes = arrayOf(PumpModule::class))
above.To have a dependency-injected implementation generated for the type, annotate it with
@Component
. The generated class will have the name of this type prepended with Dagger, like DaggerCoffeeShop
below:@Singleton
@Component(modules = arrayOf(DripCoffeeModule::class))
interface CoffeeShop {
fun maker(): CoffeeMaker
}
fun main(args: Array<String>) {
val coffee = DaggerCoffeeShop.builder().build()
coffee.maker().brew()
}
Dagger generates an implementation of
CoffeeShop
that allows you to get a fully-injected CoffeeMaker
. You can navigate and see the implementation of DaggerCoffeeShop
if you open the project in IDE.We observed that annotating your code almost hasn't changed when you switched to Kotlin. Now let's see what changes should be made to the build script.
In Java you specify
Dagger
as annotationProcessor
(or apt
) dependency:dependencies {
...
annotationProcessor "com.google.dagger:dagger-compiler:$dagger-version"
}
In Kotlin you have to add the
kotlin-kapt
plugin to enable kapt
, and then replace annotationProcessor
with kapt
:apply plugin: 'kotlin-kapt'
dependencies {
...
kapt "com.google.dagger:dagger-compiler:$dagger-version"
}
That's all! Note that
kapt
takes care of your Java files as well, so you don't need to keep the annotationProcessor
dependency.The full build script for the sample project can be found here. You can also look at the converted code for the Android sample.
ButterKnife
ButterKnife allows to bind views to fields directly instead of calling
findViewById
.Note that Kotlin Android Extensions plugin (automatically bundled into the Kotlin plugin in Android Studio) solves the same issue: replacing
findViewById
with a concise and straightforward code. Consider using it unless you're already using ButterKnife and don't want to migrate.You use
ButterKnife
with Kotlin in the same way as you use it with Java. Let's see first the changes in the Gradle build script, and then highlight some of the differences in the code.In the Gradle dependencies you use add the
kotlin-kapt
plugin and replace annotationProcessor
with kapt
:apply plugin: 'kotlin-kapt'
dependencies {
...
compile "com.jakewharton:butterknife:$butterknife-version"
kapt "com.jakewharton:butterknife-compiler:$butterknife-version"
}
Let's look over it to spot what has changed. In Java you annotated the field, binding it with the corresponding view:
R2.id.title) TextView title;
(
In Kotlin you can't work with fields directly, you work with properties. You annotate the property:
@BindView(R2.id.title)
lateinit var title: TextView
The
@BindView
annotation is defined to be applied to the fields only, but the Kotlin compiler understands that and annotates the corresponding field under the hood when you apply the annotation to the whole property.Note how the lateinit modifier allows to declare a non-null type initialized after the object is created (after the constructor call). Without
lateinit
you'd have to declare a nullable type and add additional nullability checks.You can also configure methods as listeners, using ButterKnife annotations:
R2.id.hello)
internal fun sayHello() {
Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show()
}
(
This code specifies an action to be performed on the "hello" button click. Note that with lambdas this code looks rather concise written directly in Kotlin:
hello.setOnClickListener {
toast("Hello, views!")
}
The
toast
function is defined in the Anko library.Data Binding
The Data Binding Library allows you to bind your application data to the layouts in a concise way.
You enable the library using the same configuration as in Java:
android {
...
dataBinding {
enabled = true
}
}
To make it work with Kotlin classes add the
kapt
dependency:apply plugin: 'kotlin-kapt'
dependencies {
kapt "com.android.databinding:compiler:$android_plugin_version"
}
When you switch to Kotlin, your xml layout files don't change at all. For instance, you use
variable
within data
to describe a variable that may be used within the layout. You can declare a variable of a Kotlin type:<data>
<variable name="data" type="org.example.kotlin.databinding.WeatherData"/>
</data>
You use the
@{}
syntax for writing expressions, which can now refer Kotlin properties:<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@{data.imageUrl}"
android:contentDescription="@string/image" />
Note that the databinding expression language uses the same syntax for referring to properties as Kotlin:
data.imageUrl
. In Kotlin you can write v.prop
instead of v.getProp()
even if getProp()
is a Java method. Similarly, instead of calling a setter directly, you may use an assignment:class MainActivity : AppCompatActivity() {
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.data = weather
// the same as
// binding.setData(weather)
}
}
You can bind a listener to run an action when a specific event happens:
<Button
android:text="@string/next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="startOtherActivity" />
Here
startOtherActivity
is a method defined in our MainActivity
:class MainActivity : AppCompatActivity() {
// ...
fun startOtherActivity(view: View) = startActivity<OtherActivity>()
}
This example uses the utility function
startActivity
creating an intent with no data and starting a new activity, which comes from the Anko library. To pass some data, you can say startActivity<OtherActivity>("KEY" to "VALUE")
.Note that instead of declaring lambdas in xml like in the following example, you can can bind actions directly in the code:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
// the same logic written in Kotlin code
button.setOnClickListener { presenter.onSaveClick(task) }
In the last line
button
is referenced by id
using the Kotlin Android Extensions plugin. Consider using this plugin as an alternative which allows you to keep binding logic in code and have the concise syntax at the same time.You can find an example project here.
DBFlow
DBFlow is a SQLite library that simplifies interaction with databases. It heavily relies on annotation processing.
To use it with Kotlin configure annotation processing dependency using
kapt
:apply plugin: 'kotlin-kapt'
dependencies {
kapt "com.github.raizlabs.dbflow:dbflow-processor:$dbflow_version"
compile "com.github.raizlabs.dbflow:dbflow-core:$dbflow_version"
compile "com.github.raizlabs.dbflow:dbflow:$dbflow_version"
}
Here is a detailed guide how to configure DBFlow.
If your application already uses DBFlow, you can safely introduce Kotlin into your project. You can gradually convert existing code to Kotlin (ensuring that everything compiles along the way). The converted code doesn't differ much from Java. For instance, declaring a table looks similar to Java with the small difference that default values for properties must be specified explicitly:
@Table(name="users", database = AppDatabase::class)
class User : BaseModel() {
@PrimaryKey(autoincrement = true)
@Column(name = "id")
var id: Long = 0
@Column
var name: String? = null
}
Besides converting existing functionality to Kotlin, you can also enjoy the Kotlin specific support. For instance, you can declare tables as data classes:
@Table(database = KotlinDatabase::class)
data class User(@PrimaryKey var id: Long = 0, @Column var name: String? = null)
DBFlow defines a bunch of extensions to make its usage in Kotlin more idiomatic, which you can include in your dependencies:
dependencies {
compile "com.github.raizlabs.dbflow:dbflow-kotlinextensions:$dbflow_version"
}
That gives you a way to express queries via C#-like LINQ syntax, use lambdas to write much simpler code for asynchronous computations, and more. Read all the details here.
You can browse the converted sample application.
Auto-Parcel
When you specify the dependency you again use
kapt
as annotation processor to take care of Kotlin files:apply plugin: 'kotlin-kapt'
dependencies {
...
kapt "frankiesardo:auto-parcel:$latest-version"
}
You can annotate Kotlin classes with
@AutoValue
. Let's look at the converted Address
class for which the Parcelable
implementation will be generated:@AutoValue
abstract class Address : Parcelable {
abstract fun coordinates(): DoubleArray
abstract fun cityName(): String
companion object {
fun create(coordinates: DoubleArray, cityName: String): Address {
return builder().coordinates(coordinates).cityName(cityName).build()
}
fun builder(): Builder = `$AutoValue_Address`.Builder()
}
@AutoValue.Builder
interface Builder {
fun coordinates(x: DoubleArray): Builder
fun cityName(x: String): Builder
fun build(): Address
}
}
Kotlin doesn't have
static
methods, so they should be place inside a companion object
. If you still want to use them from Java code, annotate them with @JvmStatic
.If you need to access a Java class or method with a name that is not a valid identifier in Kotlin, you can escape the name with the backtick (`) character, like in accessing the generated class `
$AutoValue_Address
`.Overall the converted code looks very similar to the original Java code.
Comments
Post a Comment