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

Annotation processing - where we are and where we want to be?

Annotation processing - where we are and where we want to be?

Slides from Android Tech Talks #31 (GDG Kraków) online meeting presentation

Miłosz Lewandowski

July 09, 2020
Tweet

More Decks by Miłosz Lewandowski

Other Decks in Programming

Transcript

  1. Annotation processing ⚙ where we are and where we want

    to be Miłosz Lewandowski Senior Android Developer @ Miquido GDG Kraków co-organizer @plnice
  2. • “It’s just a tool” • “There’s nothing you can

    do about it” • “Better focus on the app architecture, modularization, product, ...” • “Don’t use Dagger, switch to Koin” • “No need to care about it if my app builds in less than X minutes” • “I have worse problems than this one” Why care?
  3. Why care? • You should equally care about all the

    tools you use in your project • You should be aware of what happens under the hood in your project ◦ You should understand your project in the technical terms ◦ You should not ignore any of the project technical areas and aspects ◦ You should not assume that something “just works” • If you notice that something is wrong, it’s too late ◦ You need to dedicate some extra time to investigate the issue ◦ Investigation can be harder, you may run into multiple problems ◦ During the investigation, the issue persists
  4. Why care? • Build time matters ◦ Even small improvements

    multiplied by # of builds and devs can make a difference! • Modularization won’t solve all of your build time problems ◦ You keep tasks small, run them in parallel, but the build time is still there
  5. Why care? Developer’s main responsibility is to keep the project

    in a best possible condition in technical areas
  6. How to care? • Have a dedicated time to investigate

    your project (in technical terms) ◦ No one will give you time to do this • Jump into different areas (build/tools, static code analysis, architecture) • Try changing things, look for improvements, adopt new practices • Keep a technical backlog, pick things from it • Do it on a regular basis ◦ It’s ok to leave it for a while in hard times, but this should be an exception • Do it also when “nothing wrong happens”
  7. Small kapt improvements • Build cache support: since Kotlin 1.2.20

    ◦ kapt { useBuildCache = true } • Running kapt tasks in parallel: since Kotlin 1.2.60 ◦ kapt.use.worker.api=true • Compile avoidance for kapt: since Kotlin 1.3.20 ◦ Annotation processing is skipped when source files are unchanged or changes in dependencies are ABI compatible ◦ kapt.include.compile.classpath=false
  8. Incremental annotation processing • Can speed up local build times

    • Supported since: ◦ Gradle 4.7 (April 2018) ◦ Android Gradle Plugin 3.3 (January 2019) ◦ Kotlin (kapt) 1.3.30 (April 2019), enabled by default since Kotlin 1.3.50 • Annotation processors authors need to make them incremental • To make incremental annotation processing work, all annotation processors in the module must be incremental
  9. Incremental annotation processing • https://docs.gradle.org/current/userguide/java_plugin.html# state_of_support_in_popular_annotation_processors • Examples: ◦ Dagger:

    2.18 (hidden behind feature toggle), 2.24 default ◦ Room: 2.2.0 (hidden behind feature toggle) ◦ Moshi: 1.9.0 ◦ Epoxy: 4.0.0-beta1
  10. Incremental annotation processing - Room android { ... defaultConfig {

    ... javaCompileOptions { annotationProcessorOptions { arguments += ["room.incremental":"true"] } } } }
  11. Incremental annotation processing - the approach • Check build log

    for information about non-incremental processors ◦ [WARN] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: com.airbnb.epoxy.EpoxyProcessor (NOT_INCREMENTAL). • Keep a list of non-incremental annotation processors • Check from time to time if there are any updates to the libraries ◦ Always check release notes for e.g. new feature toggles! • Subscribe to related GitHub issues/pull requests notifications • Move code related to particular annotation processor to other module
  12. Incremental annotation processing - under the hood • Incremental processors:

    ◦ must generate their files using Filer API ◦ must not depend on compiler-specific APIs like com.sun.source.util.Trees • “Isolating” and “aggregating” annotation processors • “Isolating” processors look at each annotated element in isolation • “Aggregating” processors can aggregate several source files into one or more output files
  13. Annotation processing time • Common belief: “annotation processing is slow”

    • Doesn’t apply to project I’m working on (could be better, though)
  14. How does kapt work • kapt doesn’t do any annotation

    processing by itself ◦ it generates Java stubs and feeds them to Java annotation processing • kapt steps: ◦ Parse Kotlin sources; generate Java stubs ◦ Run Java annotation processing ◦ Compile Java sources; compile Kotlin sources • Stubs are generated even for classes without any annotations source: Vladimir Tagakov (Lyft), Droidcon San Francisco https://www.droidcon.com/media-detail?video=380953207
  15. Working around kapt • Vladimir Tagakov (Lyft), Droidcon San Francisco

    - “Working around kapt” lightning talk (https://www.droidcon.com/media-detail?video=380953207) • NAPT plugin ◦ annotation processing for Kotlin without kapt ◦ not open sourced yet ◦ can’t reference generated code from Kotlin (can be workarounded), can’t reference generated types from Kotlin
  16. Getting rid of annotation processing • What if we could

    disable annotation processing in following scenarios: ◦ dev builds ◦ PR checks (static code analysis, unit tests) • Generated code could be replaced with reflection • Risk: can lead to runtime errors in debug builds, or compilation errors when preparing release builds • Gain: can greatly speed up most of the builds
  17. Getting rid of annotation processing dependencies { if (<pr check

    or IDE build, e.g. properties.containsKey(“android.injected.invoked.from.ide”)>) { implementation "com.somedep:somedep-reflect:x.x.x" } else { kapt "com.somedep:somedep-processor:x.x.x" } debugImplementation "com.somedep:somedep-reflect:x.x.x" kaptRelease "com.somedep:somedep-processor:x.x.x" } • First way seems to be less readable • Second way cannot be applied for non-Android modules!
  18. dagger-reflect • https://github.com/JakeWharton/dagger-reflect • Two approaches: ◦ partial reflection ◦

    full reflection • Partial reflection still uses (in a minimal way) an annotation processor • Full reflection avoids using annotation processors • Some limitations, but didn’t run into these • Some code adjustments may be needed ◦ Lint checks for wrong annotation retention are available
  19. Issues with dagger-reflect (full reflection) • Scopes ◦ https://github.com/JakeWharton/dagger-reflect/issues/186 •

    R8 issues: ◦ rules not included in the artifact ◦ suggested ones do not work when included manually ◦ https://github.com/JakeWharton/dagger-reflect/pull/143 • Some limitations, but didn’t run into these ◦ https://github.com/JakeWharton/dagger-reflect#unsupported-features-and-limitations
  20. Moshi • For each annotated class (@JsonClass(generateAdapter = true)), Moshi’s

    annotation processor generates adapter • A need for annotation processing can be skipped by using reflection-based adapter: val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build()
  21. Moshi // src/debug/java/com/mypackage/DefaultMoshi.kt object DefaultMoshi { fun newBuilder(): Moshi.Builder {

    return Moshi.Builder().add(KotlinJsonAdapterFactory()) } } // src/release/java/com/mypackage/DefaultMoshi.kt object DefaultMoshi { fun newBuilder(): Moshi.Builder { return Moshi.Builder() } }
  22. Moshi-Sealed { "type": "success", "value": "value" } { "type": "error"

    "error_logs": { "log1": "value1", "log2": "value2" } } data class Message( val type: Type, // enum val value: String?, val error_logs: Map<String, Any>? )
  23. Moshi-Sealed @JsonClass(generateAdapter = true, generator = "sealed:type") sealed class Message

    { @TypeLabel("success") @JsonClass(generateAdapter = true) data class Success(val value: String) : Message() @TypeLabel("error") @JsonClass(generateAdapter = true) data class Error(val error_logs: Map<String, Any>) : Message() @DefaultObject object Unknown : Message() }
  24. Moshi-Sealed // src/debug/java/com/mypackage/DefaultMoshi.kt object DefaultMoshi { fun newBuilder(): Moshi.Builder {

    return Moshi.Builder() .add(MoshiSealedJsonAdapterFactory()) .add(KotlinJsonAdapterFactory()) } }
  25. Annotation processors that cannot be replaced with reflection • Example:

    Epoxy @ModelView class MyView : ViewGroup() { @ModelProp fun setTitle(text: CharSequence?) { title_view.text = text } } myView { title("Title") }
  26. Kotlin Symbol Processing (KSP) API • A way to create

    lightweight plugins for Kotlin compiler • Developed by Google, available in Kotlin 1.4-M1 • “For some processors (...) KSP reduces full compilation times by up to 25% when compared to KAPT” • Incremental processing planned, but not implemented yet
  27. Kotlin Symbol Processing (KSP) API • Resources: ◦ https://github.com/android/kotlin/tree/ksp/libraries/tools/kotlin-symbol-processing-api ◦

    https://github.com/android/kotlin/blob/ksp/libraries/tools/kotlin-symbol-processing-api/ reference.md ◦ https://www.zacsweers.dev/kotlin-symbol-processor-early-thoughts/
  28. Annotation processing improvements - step by step 1. Make sure

    you’re running latest versions of Gradle, Android Gradle Plugin and Kotlin and all the optimization flags are correctly switched 2. Make sure you’re using incremental annotation processors. Don’t mix in one module incremental processors with non-incremental ones. 3. Use reflection-based artifacts for dev/debug/PR check builds. 4. If possible, drop kapt for that builds. 5. Look for Kotlin with stable KSP version and libraries that will use it.