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 DaggerButterknifeData BindingAuto-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"
}
We've converted the ButterKnife sample to Kotlin. The resulting code can be found here.
Let's look over it to spot what has changed. In Java you annotated the field, binding it with the corresponding view:
@BindView(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:
@OnClick(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

Auto-Parcel allows to generate Parcelable values for classes annotated with @AutoValue.
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"
}
The converted sample can be found here.
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

Popular posts from this blog

Top 16 Mobile App Development Companies in India | App Developers 2017

CCEE CDAC Exam Questions

Important JavaScript Question Answers