Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Code Generation in Kotlin with Annotation and KotlinPoet

Code Generation in Kotlin with Annotation and KotlinPoet

In this session, I talked about how we can implement an annotation processor into our Kotlin project together with KotlinPoet to generate Kotlin source files. I then presented some strategies to test and validate the generated code. Discussed the benefits and limitations of this approach and when the appropriate time to use it is. Finally, I provided some sample implementation of annotation processing which we can use to increase our productivity.

Malvin Sutanto

August 24, 2019
Tweet

More Decks by Malvin Sutanto

Other Decks in Programming

Transcript

  1. ©2019 Wantedly, Inc. Code Generation in Kotlin with Annotation and

    KotlinPoet Kotlin Fest 2019 Aug 24, 2019 - Malvin Sutanto Photo by Dave Michuda on Unsplash
  2. ©2019 Wantedly, Inc. Treat other programs as their data To

    add or change behavior and functionalities • Runtime: Reflection and parser • Compile time: Generator Metaprogramming https://jakewharton.com/mechanisms-of-metaprogramming/
  3. ©2019 Wantedly, Inc. Read files and properties during runtime Read

    from Java or Kotlin classes • Read variables • Invoke functions Interpret files, like XML or JSON Metaprogramming Reflection and parser
  4. ©2019 Wantedly, Inc. Generate files based on input Annotation Can

    output source code, byte code, or any type of files Can be compile safe Better performance during runtime Dagger 2 for dependency injection Metaprogramming Generator
  5. ©2019 Wantedly, Inc. Dagger 2 Example Metaprogramming @Inject lateinit var

    repository: Repository !!... DaggerComponent.inject(this)
  6. ©2019 Wantedly, Inc. API added in Java 6 Reads representation

    of your code • Not the code itself • Code has not been compiled yet • Special APIs to read code Annotation Processor Annotation
  7. ©2019 Wantedly, Inc. Annotation Processor Convert data class to a

    map @AutoMap data class Person( val id: Long, val name: String?, val address: Address ) /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } @AutoMap data class Address( val streetName: String, val city: String ) /** * Converts [Address] to [Map]. !*/ fun Address.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "streetName" to streetName, "city" to city ) return map }
  8. ©2019 Wantedly, Inc. Annotation Processor Annotation module !// :annotation/build.gradle apply

    plugin: 'kotlin' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” } Java/Kotlin module
  9. ©2019 Wantedly, Inc. Annotation Processor Processor module !// :processor/build.gradle apply

    plugin: 'kotlin' apply plugin: ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” }
  10. ©2019 Wantedly, Inc. Annotation Processor Processor module !// :processor/build.gradle apply

    plugin: 'kotlin' apply plugin: ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” } Java/Kotlin module
  11. ©2019 Wantedly, Inc. !// :processor/build.gradle apply plugin: 'kotlin' apply plugin:

    ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” } AutoMapProcessor Kotlin metadata
  12. ©2019 Wantedly, Inc. AutoMapProcessor Kotlin metadata !// :processor/build.gradle apply plugin:

    'kotlin' apply plugin: ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” }
  13. ©2019 Wantedly, Inc. AutoMapProcessor Kotlin metadata @Metadata( mv = {1,

    1, 15}, bv = {1, 0, 3}, k = 1, d1 = {"\u00002\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\t\n\u0000\n\u0002\u0010\u00 0e\n\u0000\n\u0002\u0010 \n\u0002\u0018\u0002\n\u0002\b\f\n\u0002\u0010\u000b\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u000 2\b\u0086\b\u0018\u00002\u00020\u0001B% \u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\b\u0010\u0004\u001a\u0004\u0018\u00010\u0005\u001 2\f\u0010\u0006\u001a\b\u0012\u0004\u0012\u00020\b0\u0007¢ \u0006\u0002\u0010\tJ\t\u0010\u0010\u001a\u0002!!...”}, d2 = {"Lcom/malvinstn/myannotationprocessing/Person;", "", "id", "", "name", "", "address", "", "Lcom/malvinstn/myannotationprocessing/Address;", "(JLjava/lang/String;Ljava/util/List;)V", "getAddress", "()Ljava/util/List;", "getId", "()J", "getName", "()Ljava/lang/String;", "component1", "component2", "component3", "copy", "equals", "", "other", "hashCode", "", "toString", "app_debug"} ) public final class Person {
  14. ©2019 Wantedly, Inc. AutoMapProcessor Processor class class AutoMapProcessor : KotlinAbstractProcessor(),

    KotlinMetadataUtils { override fun getSupportedAnnotationTypes(): Set<String> = setOf(AutoMap!::class.java.canonicalName) override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { TODO("Process kotlin code here") } }
  15. ©2019 Wantedly, Inc. AutoMapProcessor Processor class class AutoMapProcessor : KotlinAbstractProcessor(),

    KotlinMetadataUtils { override fun getSupportedAnnotationTypes(): Set<String> = setOf(AutoMap!::class.java.canonicalName) override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { TODO("Process kotlin code here") } }
  16. ©2019 Wantedly, Inc. AutoMapProcessor Processor class class AutoMapProcessor : KotlinAbstractProcessor(),

    KotlinMetadataUtils { override fun getSupportedAnnotationTypes(): Set<String> = setOf(AutoMap!::class.java.canonicalName) override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { TODO("Process kotlin code here") } }
  17. ©2019 Wantedly, Inc. !// :processor/build.gradle apply plugin: 'kotlin' apply plugin:

    ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” } AutoMapProcessor Auto service
  18. ©2019 Wantedly, Inc. AutoMapProcessor Auto service !// :processor/build.gradle apply plugin:

    'kotlin' apply plugin: ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” }
  19. ©2019 Wantedly, Inc. AutoMapProcessor Processor class class AutoMapProcessor : KotlinAbstractProcessor(),

    KotlinMetadataUtils { override fun getSupportedAnnotationTypes(): Set<String> = setOf(AutoMap!::class.java.canonicalName) override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { TODO("Process kotlin code here") } }
  20. ©2019 Wantedly, Inc. AutoMapProcessor Processor class annotated with AutoService @AutoService(Processor!::class)

    class AutoMapProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils { override fun getSupportedAnnotationTypes(): Set<String> = setOf(AutoMap!::class.java.canonicalName) override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { TODO("Process kotlin code here") } }
  21. ©2019 Wantedly, Inc. AutoMapProcessor Kotlin class syntax class KotlinClass {

    private var count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  22. ©2019 Wantedly, Inc. AutoMapProcessor Type class KotlinClass { private var

    count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  23. ©2019 Wantedly, Inc. AutoMapProcessor Members class KotlinClass { private var

    count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  24. ©2019 Wantedly, Inc. AutoMapProcessor Parameter class KotlinClass { private var

    count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  25. ©2019 Wantedly, Inc. AutoMapProcessor Top level member class KotlinClass {

    private var count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  26. ©2019 Wantedly, Inc. AutoMapProcessor Return type class KotlinClass { private

    var count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  27. ©2019 Wantedly, Inc. AutoMapProcessor Visibility modifier class KotlinClass { private

    var count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  28. ©2019 Wantedly, Inc. AutoMapProcessor Kotlin class syntax class KotlinClass {

    private var count: Int = 0 fun incrementCount() { count += 1 } fun incrementCountBy(value: Int) { count += value } } fun KotlinClass.printCount(): String = "Count is: $count"
  29. ©2019 Wantedly, Inc. Processing Kotlin code Get annotated element override

    fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { val elements = roundEnv.getElementsAnnotatedWith(AutoMap!::class.java) !// Loop through all annotated element for (element in elements) { !// Check if it is a public data class. !// Read the declared variables of each element. !// Check if a declared variable’s Type is also annotated with @AutoMap. !// Generate code with KotlinPoet. } return true }
  30. ©2019 Wantedly, Inc. Processing Kotlin code Get annotated element override

    fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { val elements = roundEnv.getElementsAnnotatedWith(AutoMap!::class.java) !// Loop through all annotated element for (element in elements) { !// Check if it is a public data class. !// Read the declared variables of each element. !// Check if a declared variable’s Type is also annotated with @AutoMap. !// Generate code with KotlinPoet. } return true }
  31. ©2019 Wantedly, Inc. Processing Kotlin code Get annotated element override

    fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { val elements = roundEnv.getElementsAnnotatedWith(AutoMap!::class.java) !// Loop through all annotated element for (element in elements) { !// Check if it is a public data class. !// Read the declared variables of each element. !// Check if a declared variable’s Type is also annotated with @AutoMap. !// Generate code with KotlinPoet. } return true } for (element in elements)
  32. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. val metadata: KotlinClassMetadata? = element.kotlinMetadata as? KotlinClassMetadata if (element !is TypeElement !|| metadata !== null) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a class", element) continue } val classProto: ProtoBuf.Class = metadata.data.classProto !!... } for (element in elements)
  33. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. val metadata: KotlinClassMetadata? = element.kotlinMetadata as? KotlinClassMetadata if (element !is TypeElement !|| metadata !== null) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a class", element) continue } val classProto: ProtoBuf.Class = metadata.data.classProto !!... }
  34. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. val metadata: KotlinClassMetadata? = element.kotlinMetadata as? KotlinClassMetadata if (element !is TypeElement !|| metadata !== null) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a class", element) continue } val classProto: ProtoBuf.Class = metadata.data.classProto !!... }
  35. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. val metadata: KotlinClassMetadata? = element.kotlinMetadata as? KotlinClassMetadata if (element !is TypeElement !|| metadata !== null) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a class", element) continue } val classProto: ProtoBuf.Class = metadata.data.classProto !!... }
  36. ©2019 Wantedly, Inc. Processing Kotlin code ProtoBuf.Class val classProto: ProtoBuf.Class

    = metadata.data.classProto classProto.constructorList classProto.supertypeList classProto.typeParameterList classProto.functionList classProto.modality !!...
  37. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. val metadata: KotlinClassMetadata? = element.kotlinMetadata as? KotlinClassMetadata if (element !is TypeElement !|| metadata !== null) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a class", element) continue } val classProto: ProtoBuf.Class = metadata.data.classProto !!... } for (element in elements) { val classProto: ProtoBuf.Class = metadata.data.classProto
  38. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. !!... val classProto: ProtoBuf.Class = metadata.data.classProto if (!classProto.isDataClass !|| !// Is not a data class. classProto.visibility !!= ProtoBuf.Visibility.PUBLIC !|| !// Is not public. classProto.isInnerClass !// Is an inner class. ) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a public data class", element) continue } !// Read the declared variables of each element. } val classProto: ProtoBuf.Class = metadata.data.classProto for (element in elements) {
  39. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. !!... val classProto: ProtoBuf.Class = metadata.data.classProto if (!classProto.isDataClass !|| !// Is not a data class. classProto.visibility !!= ProtoBuf.Visibility.PUBLIC !|| !// Is not public. classProto.isInnerClass !// Is an inner class. ) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a public data class", element) continue } !// Read the declared variables of each element. }
  40. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. !!... val classProto: ProtoBuf.Class = metadata.data.classProto if (!classProto.isDataClass !|| !// Is not a data class. classProto.visibility !!= ProtoBuf.Visibility.PUBLIC !|| !// Is not public. classProto.isInnerClass !// Is an inner class. ) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a public data class", element) continue } !// Read the declared variables of each element. }
  41. ©2019 Wantedly, Inc. Processing Kotlin code Validating element for (element

    in elements) { !// Check if it is a valid element. !!... val classProto: ProtoBuf.Class = metadata.data.classProto if (!classProto.isDataClass !|| !// Is not a data class. classProto.visibility !!= ProtoBuf.Visibility.PUBLIC !|| !// Is not public. classProto.isInnerClass !// Is an inner class. ) { messager.printMessage(Diagnostic.Kind.ERROR, "Not a public data class", element) continue } !// Read the declared variables of each element. } !// Read the declared variables of each element. for (element in elements) {
  42. ©2019 Wantedly, Inc. Processing Kotlin code Reading class’ variables for

    (element in elements) { !!... !// Read the declared variables of each element. val variables: List<Pair<String, TypeElement?!>> = element.enclosedElements .filterIsInstance<VariableElement>() .map { variable !-> !// Check if a declared variable’s Type is also annotated with @AutoMap. val varType: TypeElement? = typeUtils.asElement(variable.asType()) as? TypeElement val hasAnnotation: Boolean = varType!?.getAnnotation(AutoMap!::class.java) !!= null variable.simpleName.toString() to varType!?.takeIf { hasAnnotation } } generateCode(element, variables) } !// Read the declared variables of each element. for (element in elements) {
  43. ©2019 Wantedly, Inc. Processing Kotlin code Reading class’ variables for

    (element in elements) { !!... !// Read the declared variables of each element. val variables: List<Pair<String, TypeElement?!>> = element.enclosedElements .filterIsInstance<VariableElement>() .map { variable !-> !// Check if a declared variable’s Type is also annotated with @AutoMap. val varType: TypeElement? = typeUtils.asElement(variable.asType()) as? TypeElement val hasAnnotation: Boolean = varType!?.getAnnotation(AutoMap!::class.java) !!= null variable.simpleName.toString() to varType!?.takeIf { hasAnnotation } } generateCode(element, variables) }
  44. ©2019 Wantedly, Inc. Processing Kotlin code Reading class’ variables for

    (element in elements) { !!... !// Read the declared variables of each element. val variables: List<Pair<String, TypeElement?!>> = element.enclosedElements .filterIsInstance<VariableElement>() .map { variable !-> !// Check if a declared variable’s Type is also annotated with @AutoMap. val varType: TypeElement? = typeUtils.asElement(variable.asType()) as? TypeElement val hasAnnotation: Boolean = varType!?.getAnnotation(AutoMap!::class.java) !!= null variable.simpleName.toString() to varType!?.takeIf { hasAnnotation } } generateCode(element, variables) }
  45. ©2019 Wantedly, Inc. Processing Kotlin code Reading class’ variables for

    (element in elements) { !!... !// Read the declared variables of each element. val variables: List<Pair<String, TypeElement?!>> = element.enclosedElements .filterIsInstance<VariableElement>() .map { variable !-> !// Check if a declared variable’s Type is also annotated with @AutoMap. val varType: TypeElement? = typeUtils.asElement(variable.asType()) as? TypeElement val hasAnnotation: Boolean = varType!?.getAnnotation(AutoMap!::class.java) !!= null variable.simpleName.toString() to varType!?.takeIf { hasAnnotation } } generateCode(element, variables) }
  46. ©2019 Wantedly, Inc. Processing Kotlin code Reading class’ variables for

    (element in elements) { !!... !// Read the declared variables of each element. val variables: List<Pair<String, TypeElement?!>> = element.enclosedElements .filterIsInstance<VariableElement>() .map { variable !-> !// Check if a declared variable’s Type is also annotated with @AutoMap. val varType: TypeElement? = typeUtils.asElement(variable.asType()) as? TypeElement val hasAnnotation: Boolean = varType!?.getAnnotation(AutoMap!::class.java) !!= null variable.simpleName.toString() to varType!?.takeIf { hasAnnotation } } generateCode(element, variables) }
  47. ©2019 Wantedly, Inc. Processing Kotlin code Reading class’ variables for

    (element in elements) { !!... !// Read the declared variables of each element. val variables: List<Pair<String, TypeElement?!>> = element.enclosedElements .filterIsInstance<VariableElement>() .map { variable !-> !// Check if a declared variable’s Type is also annotated with @AutoMap. val varType: TypeElement? = typeUtils.asElement(variable.asType()) as? TypeElement val hasAnnotation: Boolean = varType!?.getAnnotation(AutoMap!::class.java) !!= null variable.simpleName.toString() to varType!?.takeIf { hasAnnotation } } generateCode(element, variables) }
  48. ©2019 Wantedly, Inc. !// :processor/build.gradle apply plugin: 'kotlin' apply plugin:

    ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” } Generate Kotlin Code KotlinPoet
  49. ©2019 Wantedly, Inc. Generate Kotlin Code KotlinPoet !// :processor/build.gradle apply

    plugin: 'kotlin' apply plugin: ‘kotlin-kapt' dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib:1.3.41” implementation project(':annotation') !// Kotlin metadata implementation “me.eugeniomarletti.kotlin.metadata:kotlin-metadata:1.4.0” !// Configuration generator for service providers implementation “com.google.auto.service:auto-service:1.0-rc6“ kapt "com.google.auto.service:auto-service:1.0-rc6" !// Code generation library for kotlin implementation “com.squareup:kotlinpoet:1.2.0” }
  50. ©2019 Wantedly, Inc. Generate Kotlin Code @AutoMap data class Person(

    val id: Long, val name: String?, val address: Address ) /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  51. ©2019 Wantedly, Inc. Generate Kotlin Code @AutoMap data class Person(

    val id: Long, val name: String?, val address: Address ) /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  52. ©2019 Wantedly, Inc. Generate Kotlin Code @AutoMap data class Person(

    val id: Long, val name: String?, val address: Address ) /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  53. ©2019 Wantedly, Inc. Generate Kotlin Code @AutoMap data class Person(

    val id: Long, val name: String?, val address: Address ) /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  54. ©2019 Wantedly, Inc. Generate Kotlin Code @AutoMap data class Person(

    val id: Long, val name: String?, val address: Address ) /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  55. ©2019 Wantedly, Inc. Generate Kotlin Code @AutoMap data class Person(

    val id: Long, val name: String?, val address: Address ) /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  56. ©2019 Wantedly, Inc. Generate Kotlin Code /** * Converts [Person]

    to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  57. ©2019 Wantedly, Inc. Generate Kotlin Code /** * Converts [Person]

    to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  58. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder()
  59. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction)
  60. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = !!... val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction)
  61. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = ClassName("kotlin.collections", “Map") !// Map<String, Any?> .plusParameter(String!::class.asTypeName()) .plusParameter(Any!::class.asTypeName().copy(nullable = true)) val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction)
  62. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = ClassName("kotlin.collections", "Map") !// Map<String, Any?> .plusParameter(String!::class.asTypeName()) .plusParameter(Any!::class.asTypeName().copy(nullable = true)) val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction)
  63. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = ClassName("kotlin.collections", "Map") !// Map<String, Any?> .plusParameter(String!::class.asTypeName()) .plusParameter(Any!::class.asTypeName().copy(nullable = true)) val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction)
  64. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = ClassName("kotlin.collections", "Map") !// Map<String, Any?> .plusParameter(String!::class.asTypeName()) .plusParameter(Any!::class.asTypeName().copy(nullable = true)) val mapOfFunction: MemberName = MemberName("kotlin.collections", “mapOf") val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction)
  65. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = ClassName("kotlin.collections", "Map") !// Map<String, Any?> .plusParameter(String!::class.asTypeName()) .plusParameter(Any!::class.asTypeName().copy(nullable = true)) val mapOfFunction: MemberName = MemberName("kotlin.collections", “mapOf") val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction) .indent() codeBlockBuilder.unindent() .addStatement(")") .addStatement("return map")
  66. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = ClassName("kotlin.collections", "Map") !// Map<String, Any?> .plusParameter(String!::class.asTypeName()) .plusParameter(Any!::class.asTypeName().copy(nullable = true)) val mapOfFunction: MemberName = MemberName("kotlin.collections", “mapOf") val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction) .indent() codeBlockBuilder.unindent() .addStatement(")") .addStatement("return map")
  67. ©2019 Wantedly, Inc. Generate Kotlin Code Code block val variables:

    List<Pair<String, TypeElement?!>> = !!... val returnType: ParameterizedTypeName = ClassName("kotlin.collections", "Map") !// Map<String, Any?> .plusParameter(String!::class.asTypeName()) .plusParameter(Any!::class.asTypeName().copy(nullable = true)) val mapOfFunction: MemberName = MemberName("kotlin.collections", “mapOf") val codeBlockBuilder: CodeBlock.Builder = CodeBlock.builder() .addStatement("val map: %T = %M(", returnType, mapOfFunction) .indent() !// Loop through all variables variables.forEachIndexed { !!... } codeBlockBuilder.unindent() .addStatement(")") .addStatement("return map") variables.forEachIndexed
  68. ©2019 Wantedly, Inc. Generate Kotlin Code Code block, cont’d val

    codeBlockBuilder: CodeBlock.Builder = !!... !// Loop through all variables variables.forEachIndexed { index: Int, (name: String, varType: TypeElement?) !-> val comma = if (index < variables.size - 1) "," else "" if (varType !!= null) { !// Another class that is annotated with @AutoMap, use toMap instead. val toMap = MemberName(varType.asClassName().packageName, "toMap") codeBlockBuilder.addStatement("%S to %L.%M()%L", name, name, toMap, comma) } else { codeBlockBuilder.addStatement("%S to %L%L", name, name, comma) } } variables.forEachIndexed
  69. ©2019 Wantedly, Inc. Generate Kotlin Code Code block, cont’d val

    codeBlockBuilder: CodeBlock.Builder = !!... !// Loop through all variables variables.forEachIndexed { index: Int, (name: String, varType: TypeElement?) !-> val comma = if (index < variables.size - 1) "," else "" if (varType !!= null) { !// Another class that is annotated with @AutoMap, use toMap instead. val toMap = MemberName(varType.asClassName().packageName, "toMap") codeBlockBuilder.addStatement("%S to %L.%M()%L", name, name, toMap, comma) } else { codeBlockBuilder.addStatement("%S to %L%L", name, name, comma) } }
  70. ©2019 Wantedly, Inc. Generate Kotlin Code Code block, cont’d val

    codeBlockBuilder: CodeBlock.Builder = !!... !// Loop through all variables variables.forEachIndexed { index: Int, (name: String, varType: TypeElement?) !-> val comma = if (index < variables.size - 1) "," else "" if (varType !!= null) { !// Another class that is annotated with @AutoMap, use toMap instead. val toMap = MemberName(varType.asClassName().packageName, "toMap") codeBlockBuilder.addStatement("%S to %L.%M()%L", name, name, toMap, comma) } else { codeBlockBuilder.addStatement("%S to %L%L", name, name, comma) } }
  71. ©2019 Wantedly, Inc. Generate Kotlin Code Code block, cont’d val

    codeBlockBuilder: CodeBlock.Builder = !!... !// Loop through all variables variables.forEachIndexed { index: Int, (name: String, varType: TypeElement?) !-> val comma = if (index < variables.size - 1) "," else "" if (varType !!= null) { !// Another class that is annotated with @AutoMap, use toMap instead. val toMap = MemberName(varType.asClassName().packageName, "toMap") codeBlockBuilder.addStatement("%S to %L.%M()%L", name, name, toMap, comma) } else { codeBlockBuilder.addStatement("%S to %L%L", name, name, comma) !// !-> “id” to id, } }
  72. ©2019 Wantedly, Inc. Generate Kotlin Code Code block, cont’d val

    codeBlockBuilder: CodeBlock.Builder = !!... !// Loop through all variables variables.forEachIndexed { index: Int, (name: String, varType: TypeElement?) !-> val comma = if (index < variables.size - 1) "," else "" if (varType !!= null) { !// Another class that is annotated with @AutoMap, use toMap instead. val toMap = MemberName(varType.asClassName().packageName, "toMap") codeBlockBuilder.addStatement("%S to %L.%M()%L", name, name, toMap, comma) } else { codeBlockBuilder.addStatement("%S to %L%L", name, name, comma) !// !-> “id” to id, } }
  73. ©2019 Wantedly, Inc. Generate Kotlin Code Code block, cont’d val

    codeBlockBuilder: CodeBlock.Builder = !!... val funSpec = FunSpec.builder("toMap") .addCode(codeBlockBuilder.build()) .build()
  74. ©2019 Wantedly, Inc. Generate Kotlin Code /** * Converts [Person]

    to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  75. ©2019 Wantedly, Inc. Generate Kotlin Code /** * Converts [Person]

    to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map } Sample
  76. ©2019 Wantedly, Inc. Generate Kotlin Code Receiver type val returnType:

    ParameterizedTypeName = !!... val codeBlockBuilder: CodeBlock.Builder = !!... val className: ClassName = element.asClassName() val funSpec = FunSpec.builder("toMap") .receiver(className) .addCode(codeBlockBuilder.build()) .build()
  77. ©2019 Wantedly, Inc. Generate Kotlin Code Receiver type val returnType:

    ParameterizedTypeName = !!... val codeBlockBuilder: CodeBlock.Builder = !!... val className: ClassName = element.asClassName() val funSpec = FunSpec.builder("toMap") .receiver(className) .addCode(codeBlockBuilder.build()) .build() fun Person.toMap(): Map<String, Any?> { !!... }
  78. ©2019 Wantedly, Inc. Generate Kotlin Code Parameterized return type val

    returnType: ParameterizedTypeName = !!... val codeBlockBuilder: CodeBlock.Builder = !!... val className: ClassName = !!... val funSpec = FunSpec.builder("toMap") .receiver(className) .returns(returnType) .addCode(codeBlockBuilder.build()) .build()
  79. ©2019 Wantedly, Inc. Generate Kotlin Code Parameterized return type val

    returnType: ParameterizedTypeName = !!... val codeBlockBuilder: CodeBlock.Builder = !!... val className: ClassName = !!... val funSpec = FunSpec.builder("toMap") .receiver(className) .returns(returnType) .addCode(codeBlockBuilder.build()) .build() fun Person.toMap(): Map<String, Any?> { !!... }
  80. ©2019 Wantedly, Inc. Generate Kotlin Code FunSpec, other options FunSpec.builder(!!...)

    .addKDoc(!!...) .addAnnotation(MyAnnotation!::class) .addParameter(ParameterSpec) .addModifiers(!!...) .beginControlFlow() .endControlFlow() .nextControlFlow(!!...) !!...
  81. ©2019 Wantedly, Inc. Generate Kotlin Code Parameterized return type val

    returnType: ParameterizedTypeName = !!... val codeBlockBuilder: CodeBlock.Builder = !!... val className: ClassName = !!... val funSpec = FunSpec.builder("toMap") .receiver(className) .returns(returnType) .addCode(codeBlockBuilder.build()) .build() val funSpec = FunSpec.builder("toMap")
  82. ©2019 Wantedly, Inc. Generate Kotlin Code FileSpec val className: ClassName

    = !!... val funSpec = FunSpec.builder("toMap") !!... FileSpec.builder(className.packageName, className.simpleName) .addFunction(funSpec) !!... val funSpec = FunSpec.builder("toMap")
  83. ©2019 Wantedly, Inc. Generate Kotlin Code FileSpec val className: ClassName

    = !!... val funSpec = FunSpec.builder("toMap") !!... FileSpec.builder(className.packageName, className.simpleName) .addFunction(funSpec) !!...
  84. ©2019 Wantedly, Inc. Generate Kotlin Code FileSpec val className: ClassName

    = !!... val funSpec = FunSpec.builder("toMap") !!... FileSpec.builder(className.packageName, className.simpleName) .addFunction(funSpec) !!...
  85. ©2019 Wantedly, Inc. Generate Kotlin Code FileSpec, other options FileSpec.builder(!!...)

    .addType(TypeSpec) .addAnnotation(MyAnnotation!::class) .addProperty(PropertySpec) .addComment("This is a generated file. Do not edit”) !!...
  86. ©2019 Wantedly, Inc. Generate Kotlin Code Documentation val mapClass =

    ClassName("kotlin.collections", "Map") val funSpec = FunSpec.builder("toMap") .receiver(className) .returns(returnType) .addKdoc("Converts [%T] to [%T].", className, mapClass) .addCode(codeBlockBuilder.build()) .build() FileSpec.builder(className.packageName, className.simpleName) .addFunction(funSpec) .addComment("This is a generated file. Do not edit") .build() .writeTo(outputDir)
  87. ©2019 Wantedly, Inc. Generate Kotlin Code Documentation val mapClass =

    ClassName("kotlin.collections", "Map") val funSpec = FunSpec.builder("toMap") .receiver(className) .returns(returnType) .addKdoc("Converts [%T] to [%T].", className, mapClass) .addCode(codeBlockBuilder.build()) .build() FileSpec.builder(className.packageName, className.simpleName) .addFunction(funSpec) .addComment("This is a generated file. Do not edit") .build() .writeTo(outputDir)
  88. ©2019 Wantedly, Inc. Generate Kotlin Code Documentation val mapClass =

    ClassName("kotlin.collections", "Map") val funSpec = FunSpec.builder("toMap") .receiver(className) .returns(returnType) .addKdoc("Converts [%T] to [%T].", className, mapClass) .addCode(codeBlockBuilder.build()) .build() FileSpec.builder(className.packageName, className.simpleName) .addFunction(funSpec) .addComment("This is a generated file. Do not edit") .build() .writeTo(outputDir)
  89. ©2019 Wantedly, Inc. Generate Kotlin Code End result package com.malvinstn.myannotationprocessing

    import com.malvinstn.annotation.AutoMap @AutoMap data class Person( val id: Long, val name: String?, val address: Address ) !// This is a generated file. Do not edit package com.malvinstn.myannotationprocessing import kotlin.Any import kotlin.String import kotlin.collections.Map import kotlin.collections.mapOf /** * Converts [Person] to [Map]. !*/ fun Person.toMap(): Map<String, Any?> { val map: Map<String, Any?> = mapOf( "id" to id, "name" to name, "address" to address.toMap() ) return map }
  90. ©2019 Wantedly, Inc. Testing Generated Code Test module class PersonTest

    { @Test fun `test when name is null map"name" is null`() { val person = Person( id = 1, name = null, address = Address( streetName = "streetName", city = "city" ) ) val map = person.toMap() assertNull(map["name"]) } }
  91. ©2019 Wantedly, Inc. Testing Generated Code Test module class PersonTest

    { @Test fun `test when name is null map"name" is null`() { val person = Person( id = 1, name = null, address = Address( streetName = "streetName", city = "city" ) ) val map = person.toMap() assertNull(map["name"]) } }
  92. ©2019 Wantedly, Inc. Good for testing happy paths But unable

    to test and validate error cases Page Title Page Subtitle
  93. ©2019 Wantedly, Inc. Kotlin Compile Testing • https://github.com/tschuchortdev/kotlin-compile-testing Kompile Testing

    • https://github.com/permissions-dispatcher/kompile-testing Testing Generated Code Testing libraries
  94. ©2019 Wantedly, Inc. 1. Add dependency as testImplementation • To

    :processor module 2. Write source code in String format • Or load from a File 3. Use compile API from the library 4. Validate compiler’s result • And validate generated classes Testing Generated Code Testing libraries
  95. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing @Test

    fun `test AutoMap, when applied to data class, then generate toMap function`() { val result = KotlinCompilation().apply { sources = listOf( SourceFile.new( “Person.kt", """ import com.malvinstn.annotation.AutoMap @AutoMap data class Person(val id: Long, val name: String?) """ ) ) annotationProcessors = listOf(AutoMapProcessor()) !!... }.compile() !!... }
  96. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing @Test

    fun `test AutoMap, when applied to data class, then generate toMap function`() { val result = KotlinCompilation().apply { sources = listOf( SourceFile.new( “Person.kt", """ import com.malvinstn.annotation.AutoMap @AutoMap data class Person(val id: Long, val name: String?) """ ) ) annotationProcessors = listOf(AutoMapProcessor()) !!... }.compile() !!... }
  97. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing @Test

    fun `test AutoMap, when applied to data class, then generate toMap function`() { val result = !!...{ assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) !// Assert generated classes val kClazz = result.classLoader.loadClass("PersonKt") val paramClazz = result.classLoader.loadClass("Person") kClazz.getDeclaredMethod("toMap", paramClazz) }
  98. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing @Test

    fun `test AutoMap, when applied to data class, then generate toMap function`() { val result = !!...{ assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) !// Assert generated classes val kClazz = result.classLoader.loadClass("PersonKt") val paramClazz = result.classLoader.loadClass("Person") kClazz.getDeclaredMethod("toMap", paramClazz) }
  99. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing @Test

    fun `test AutoMap, when applied to data class, then generate toMap function`() { val result = !!...{ assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) !// Assert generated classes val kClazz = result.classLoader.loadClass("PersonKt") val paramClazz = result.classLoader.loadClass("Person") kClazz.getDeclaredMethod("toMap", paramClazz) }
  100. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing public

    final class PersonKt { @NotNull public static final Map toMap(@NotNull Person $this$toMap) { Intrinsics.checkParameterIsNotNull($this$toMap, "$this$toMap"); Map map = MapsKt.mapOf(new Pair[]{TuplesKt.to("id", $this$toMap.getId()), TuplesKt.to("name", $this$toMap.getName()), TuplesKt.to("address", AddressKt.toMap($this$toMap.getAddress()))}); return map; } }
  101. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing -

    testing error case @Test fun `test AutoMap, when applied to non data class, then exit with compilation error`() { val result = KotlinCompilation().apply { sources = listOf( SourceFile.new( "Person.kt", """ import com.malvinstn.annotation.AutoMap @AutoMap class Person(val id: Long, val name: String?) """ ) ) !!... }.compile() assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) }
  102. ©2019 Wantedly, Inc. Testing Generated Code Kotlin compile testing -

    testing error case @Test fun `test AutoMap, when applied to non data class, then exit with compilation error`() { val result = KotlinCompilation().apply { sources = listOf( SourceFile.new( "Person.kt", """ import com.malvinstn.annotation.AutoMap @AutoMap class Person(val id: Long, val name: String?) """ ) ) !!... }.compile() assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) }
  103. ©2019 Wantedly, Inc. Increase the overall build time • All

    steps need to be performed during compile time • Implement incremental kapt for better performance • Supported since Kotlin 1.3.30 • https://docs.gradle.org/5.0/userguide/ java_plugin.html#sec:incremental_annotation_processing • https://blog.jetbrains.com/kotlin/2019/04/kotlin-1-3-30-released/ Things to Consider Build time overhead
  104. ©2019 Wantedly, Inc. Increased size of your app • Unable

    to modify existing files or classes • Only create additional classes and functions Things to Consider Code footprint
  105. ©2019 Wantedly, Inc. Generated code are not accessible until kapt

    is run • “Unresolved references” error. • Might shadow the root cause of a compilation error Things to Consider Error handling and code visibility
  106. ©2019 Wantedly, Inc. 1. Generate code based on members of

    an element 2. Focus on API over readability 3. Deal with repeated codes • Can be automated • Reduce boilerplate codes 4. Time investment Should We Use Annotation Processor?
  107. ©2019 Wantedly, Inc. Sample Annotation Processor Variable List for Java

    and Kotlin Objects For Api Consumption @ApiEntity data class User( val id: Long, val name: String, val wanted_score: Int, val profile: Profile ) @ApiEntity data class Profile( val introduction: String ) val fields: List<String> = listOf( "id", "name", “wanted_score”, “profile.introduction”, )
  108. ©2019 Wantedly, Inc. Sample Annotation Processor Sealed Class to Bundle

    For Analytics Log @AnalyticsEvent sealed class MyEvent { data class ShareImage(val imageName: String, val fullString: String) : MyEvent() object ButtonTapped : MyEvent() } /** * Converts [MyEvent] to event name and params and logs it using [EventTracker.logEvent]. * * This is a generated function. Do not edit. !*/ fun EventTracker.logEvent(event: MyEvent) { val name: String val params: Bundle when (event) { is MyEvent.ShareImage !-> { name = "share_image" params = bundleOf( "image_name" to event.imageName, "full_string" to event.fullString ) } is MyEvent.ButtonTapped !-> { name = "button_tapped" params = Bundle() } } logEvent(name, params) } medium.com/@malvinsutanto
  109. ©2019 Wantedly, Inc. 1. Kotlin Metadata: https://github.com/Takhion/kotlin-metadata 2. KotlinPoet: https://square.github.io/kotlinpoet/

    3. Auto Service: https://github.com/google/auto/tree/master/service 4. Kotlin compile testing: https://github.com/tschuchortdev/kotlin-compile-testing 5. Kompile Testing: https://github.com/permissions-dispatcher/kompile-testing 6. Kotlin Metadata documentation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-metadata/index.html 7. Kotlinx Metadata: https://github.com/JetBrains/kotlin/tree/master/libraries/kotlinx-metadata/jvm 8. Better Analytics in Android with Annotation Processing and KotlinPoet: https://link.medium.com/Bh8wRWOY6Y 9. Mechanisms of Metaprogramming: https://jakewharton.com/mechanisms-of-metaprogramming/ 10.KotlinFest 2018 - Annotation Processing in a Kotlin World by Zac Sweers: https://youtu.be/a2RoLFzrFG0 11.Moshi: https://github.com/square/moshi Thank You! References and links Twitter/Medium: @malvinsutanto