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

A deep dive into migrating 
from AutoValue to K...

A deep dive into migrating 
from AutoValue to Kotlin Data Classes

AutoValue is a powerful tool provided by Google to generate Java immutable value classes. If you are migrating your project to Kotlin, data classes are the most direct way to replace it while keeping the same features. There are however trade-offs to take into account.

In this talk we introduce how we migrated our project from AutoValue to Kotlin data classes and how we dealt with the issues we encountered along the way. We also discuss the advantages and disadvantages of each approach in terms of build time, apk size, method count and serialization time.

After this introduction, we deep dive into two topics: First, we look into different serialization strategies available and how they perform. We look into the Gson, Moshi and Kotlinx.serialization libraries and explore their trade-offs when choosing one.

Second, we explain how we built an IntelliJ/Android Studio plugin to help us automate the conversion from AutoValue to Kotlin Data Classes. We explain how plugins work and how you can also develop one to help automate repetitive IDE-related tasks. The sample plugin used in the demo is available at: https://github.com/riclage/ConvertToDataClassPlugin

Ricardo Lage

June 27, 2018
Tweet

More Decks by Ricardo Lage

Other Decks in Programming

Transcript

  1. public class User { } String uuid; String login; String

    name; String email; String extraInfo;
  2. public final class User { } String uuid; String login;

    String name; String email; String extraInfo; private final private final private final private final private final
  3. public final class User { } public User(String uuid, String

    login, String name, String email, String extraInfo) { this.uuid = uuid; this.login = login; this.name = name; this.email = email; this.extraInfo = extraInfo; } String uuid; String login; String name; String email; String extraInfo; private final private final private final private final private final
  4. public final class User { } public User(String uuid, String

    login, String name, String email, @Nullable String extraInfo) { this.uuid = uuid; this.login = login; this.name = name; this.email = email; this.extraInfo = extraInfo; } String uuid; String login; String name; String email; String extraInfo; private final private final private final private final private final
  5. @Override public boolean equals(Object o) { if (this == o)

    return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (!uuid.equals(user.uuid)) return false; if (!login.equals(user.login)) return false; if (!name.equals(user.name)) return false; if (!email.equals(user.email)) return false; if (extraInfo != null ? !extraInfo.equals(user.extraInfo) : user.extraInfo != null) return false; return true; } @Override public int hashCode() { int result = uuid.hashCode(); result = 31 * result + login.hashCode(); result = 31 * result + name.hashCode(); result = 31 * result + email.hashCode(); result = 31 * result + (extraInfo != null ? extraInfo.hashCode() : 0); return result; }
  6. public String getUuid() { return uuid; } public String getLogin()

    { return login; } public String getName() { return name; } public String getEmail() { return email; } public String getExtraInfo() { return extraInfo; }
  7. public final class User implements Parcelable { private final String

    uuid; private final String login; private final String name; private final String email; private final String extraInfo; public User(String uuid, String login, String name, String email, @Nullable String extraInfo) { this.uuid = uuid; this.login = login; this.name = name; this.email = email; this.extraInfo = extraInfo; } public User(Parcel in) { uuid = in.readString(); login = in.readString(); name = in.readString(); email = in.readString(); extraInfo = in.readString(); }
  8. And that’s not all • Validate non-null constructor parameters •

    Add toString() implementation • Ensure consistency when adding/removing fields • Rely on reflection or write your own adapter for serialization • Rinse & repeat for every new such classes
  9. • Easier to create & maintain value classes • Automatically

    generates equals(), hashCode() and toString() for you • Plugin system for additional code generation: Parcelable implementation, Gson type adapters, etc. AutoValue to the rescue
  10. Why use AutoValue? • No runtime dependencies • API-invisible -

    The class functions like any other • Negligible cost to performance • Very few limitations on what your class can do • Extralinguistic "magic" kept to an absolute minimum (uses only standard Java platform technologies, in the manner they were intended) Source: https://github.com/google/auto/blob/master/value/ userguide/why.md
  11. @AutoValue public abstract class User { public abstract String uuid();

    public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); }
  12. // Generated by com.google.auto.value.processor.AutoValueProcessor final class AutoValue_User extends User {

    private final String uuid; private final String login; private final String name; private final String email; private final String extraInfo; AutoValue_User( String uuid, String login, String name, String email, @Nullable String extraInfo) { if (uuid == null) { throw new NullPointerException("Null uuid"); } this.uuid = uuid; if (login == null) { throw new NullPointerException("Null login"); } this.login = login;
  13. @AutoValue public abstract class User { public abstract String uuid();

    public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); }
  14. @AutoValue public abstract class User { public abstract String uuid();

    public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); public static final User create(String login, String name, String email) { return new AutoValue_User(UUID.randomUUID().toString(), login, name, email, null); } }
  15. @AutoValue public abstract class User { public abstract String uuid();

    public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); public static final User create(String login, String name, String email) { return new AutoValue_User(UUID.randomUUID().toString(), login, name, email, null); } }
  16. @AutoValue public abstract class User { public abstract String uuid();

    public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); public static final User create(String login, String name, String email) { return new AutoValue_User(UUID.randomUUID().toString(), login, name, email, null); } }
  17. @AutoValue public abstract class User implements Parcelable { public abstract

    String uuid(); public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); public abstract User withExtraInfo(String extraInfo); public static final User create(String login, String name, String email) { return new AutoValue_User(UUID.randomUUID().toString(), login, name, email, null); } public static TypeAdapter<User> typeAdapter(Gson gson) { return new AutoValue_User.GsonTypeAdapter(gson); }
  18. @AutoValue public abstract class User implements Parcelable { public abstract

    String uuid(); public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); public abstract User withExtraInfo(String extraInfo); public static final User create(String login, String name, String email) { return new AutoValue_User(UUID.randomUUID().toString(), login, name, email, null); } public static TypeAdapter<User> typeAdapter(Gson gson) { return new AutoValue_User.GsonTypeAdapter(gson); }
  19. @AutoValue public abstract class User implements Parcelable { public abstract

    String uuid(); public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); public abstract User withExtraInfo(String extraInfo); public static final User create(String login, String name, String email) { return new AutoValue_User(UUID.randomUUID().toString(), login, name, email, null); } public static TypeAdapter<User> typeAdapter(Gson gson) { return new AutoValue_User.GsonTypeAdapter(gson); }
  20. // Generated by com.google.auto.value.processor.AutoValueProcessor abstract class $$$AutoValue_User extends User {

    private final String uuid; private final String login; private final String name; private final String email; private final String extraInfo; $$$AutoValue_User( String uuid, String login, String name, String email, @Nullable String extraInfo) { if (uuid == null) { throw new NullPointerException("Null uuid"); } this.uuid = uuid; if (login == null) { throw new NullPointerException("Null login"); } this.login = login;
  21. abstract class $$AutoValue_User extends $$$AutoValue_User { $$AutoValue_User(String uuid, String login,

    String name, String email, @Nullable String extraInfo) { super(uuid, login, name, email, extraInfo); } public static final class GsonTypeAdapter extends TypeAdapter<User> { private final TypeAdapter<String> string_adapter; public GsonTypeAdapter(Gson gson) { this.string_adapter = gson.getAdapter(String.class); } @Override public void write(JsonWriter jsonWriter, User object) throws IOException { if (object == null) { jsonWriter.nullValue(); return; } jsonWriter.beginObject(); jsonWriter.name("uuid"); string_adapter.write(jsonWriter, object.uuid()); jsonWriter.name("login"); string_adapter.write(jsonWriter, object.login()); jsonWriter.name("name");
  22. abstract class $AutoValue_User extends $$AutoValue_User { $AutoValue_User(String uuid, String login,

    String name, String email, String extraInfo) { super(uuid, login, name, email, extraInfo); } @Override public final User withExtraInfo(String extraInfo) { return new AutoValue_User(uuid(), login(), name(), email(), extraInfo); } }
  23. final class AutoValue_User extends $AutoValue_User { public static final Parcelable.Creator<AutoValue_User>

    CREATOR = new Parcelable.Creator<AutoValue_User>() { @Override public AutoValue_User createFromParcel(Parcel in) { return new AutoValue_User( in.readString(), in.readString(), in.readString(), in.readString(), in.readInt() == 0 ? in.readString() : null ); } @Override public AutoValue_User[] newArray(int size) { return new AutoValue_User[size]; } }; AutoValue_User(String uuid, String login, String name, String email, @Nullable String extraInfo) { super(uuid, login, name, email, extraInfo); }
  24. @AutoValue public abstract class User { public abstract String uuid();

    public abstract String login(); public abstract String name(); public abstract String email(); @Nullable public abstract String extraInfo(); public static final User create(String login, String name, String email) { return new AutoValue_User(UUID.randomUUID().toString(), login, name, email, null); } }
  25. @AutoValue abstract class User : Parcelable { abstract fun uuid():

    String abstract fun login(): String abstract fun name(): String abstract fun email(): String abstract fun extraInfo(): String? abstract fun withExtraInfo(extraInfo: String): User companion object { fun typeAdapter(gson: Gson): TypeAdapter<User> = `$ $AutoValue_User`.GsonTypeAdapter(gson) fun create(login: String, name: String, email: String): User = AutoValue_User(UUID.randomUUID().toString(), login, name, email, null) } }
  26. Data Classes • Kotlin’s answer to value classes: equals(), hashCode()

    and toString() derived from the constructor: data class User(val uuid: String, val login: String, 
 val name: String, val email: String, val extraInfo: String?) • Built-in copy() function: 
 val userWithInfo = user.copy(extraInfo = "Hello World") • Destructuring declarations: 
 val (_, login, name) = user

  27. Data classes vs AutoValue • Less external dependencies • Simpler/smaller

    code • No “open” abstract classes • Faster build / less overhead from annotation processors • Plugin issues
  28. Plugin issues • Yet another external dependencies • "with" requires

    custom methods for each configuration:
 
 
 
 • Gson plugin requires @SerializedName for all camel cased fields. E.g., 
 abstract fun withExtraInfo(extraInfo: String): User abstract fun withExtraInfoAndEmail(extraInfo: String, email: String): User @SerializedName("extra_info") abstract fun extraInfo(): String?
  29. @AutoValue abstract class User : Parcelable { abstract fun uuid():

    String abstract fun login(): String abstract fun name(): String abstract fun email(): String abstract fun extraInfo(): String? abstract fun withExtraInfo(extraInfo: String): User companion object { @JvmStatic fun typeAdapter(gson: Gson): TypeAdapter<User> = `$ $AutoValue_User`.GsonTypeAdapter(gson) @JvmStatic fun create(login: String, name: String, email: String): User = AutoValue_User(UUID.randomUUID().toString(), login, name, email, null) } } In other words. We are moving from this:
  30. @Parcelize data class User(val uuid: String, val login: String, 


    val name: String, val email: String, val extraInfo: String?): Parcelable To this:
  31. Parcelize plugin • Kotlin’s first class support for Parcelable
 https://github.com/Kotlin/KEEP/blob/master/proposals/extensions/android-

    parcelable.md • Marked as experimental but no issues present on the latest Kotlin version • A compiler plugin
 On AS: Tools → Kotlin → Show Kotlin Bytecode → Decompile button
  32. Parcelize plugin • Kotlin’s first class support for Parcelable
 https://github.com/Kotlin/KEEP/blob/master/proposals/extensions/android-

    parcelable.md • Marked as experimental but no issues present on the latest Kotlin version • A compiler plugin
 On AS: Tools → Kotlin → Show Kotlin Bytecode → Decompile button
  33. @Parcelize public final class User implements Parcelable { @NotNull private

    final String uuid; @NotNull private final String login; @NotNull private final String name; @NotNull private final String email; @Nullable private final String extraInfo; public static final User.Creator CREATOR = new User.Creator(); @NotNull public final String getUuid() { return this.uuid; } @NotNull public final String getLogin() { User class decompiled to Java from our data class
  34. Non-optional fields and Gson @Parcelize data class User(val uuid: String,

    val login: String, 
 val name: String, val email: String, val extraInfo: String?): Parcelable • When de-serializing a User object, what if “login" is not present in the response?
  35. Non-optional fields and Gson @Parcelize data class User(val uuid: String,

    val login: String, 
 val name: String, val email: String, val extraInfo: String?): Parcelable • When de-serializing a User object, what if “login" is not present in the response? • Program is going to fail in unexpected ways when we try to use the “login" field.
  36. Serialization • Gson - The classic
 https://github.com/google/gson • Moshi -

    The modern
 https://github.com/square/moshi
 - Faster than Gson & without its quirks
 - Kotlin support (Kotlin reflection or CodeGen)
  37. GsonValue @Parcelize @GsonConstructor data class User (val uuid: String, val

    login: String, val name: String, val email: String, @SerializedName("extra_info") val extraInfo: String?): Parcelable
  38. GsonValue @Parcelize @GsonConstructor data class User (val uuid: String, val

    login: String, val name: String, val email: String, @SerializedName("extra_info") val extraInfo: String?): Parcelable
  39. Kotshi @Parcelize @JsonSerializable data class User (val uuid: String, val

    login: String, val name: String, val email: String, @Json(name = “extra_info") val extraInfo: String?): Parcelable
  40. Kotshi @Parcelize @JsonSerializable data class User (val uuid: String, val

    login: String, val name: String, val email: String, @Json(name = “extra_info") val extraInfo: String?): Parcelable
  41. Serialization • Gson - The classic
 https://github.com/google/gson • Moshi -

    The modern
 https://github.com/square/moshi
 - Faster than Gson & without its quirks
 - Kotlin support (Kotlin reflection or CodeGen) • Kotlinx.serialization (KSerializer) - The new kid
 https://github.com/Kotlin/kotlinx.serialization
  42. KSerializer @Parcelize @Serializable data class User (val uuid: String, val

    login: String, val name: String, val email: String, @SerialName(“extra_info") val extraInfo: String?): Parcelable
  43. KSerializer @Parcelize @Serializable data class User (val uuid: String, val

    login: String, val name: String, val email: String, @SerialName(“extra_info") val extraInfo: String?): Parcelable
  44. @kotlinx.serialization.Serializable public final data class User public constructor(uuid: kotlin.String, login:

    kotlin.String, name: kotlin.String, email: kotlin.String, extraInfo: kotlin.String?) { public companion object { } public final val email: kotlin.String /* compiled code */ @kotlinx.serialization.SerialName public final val extraInfo: kotlin.String? /* compiled code */ public final val login: kotlin.String /* compiled code */ public final val name: kotlin.String /* compiled code */ public final val uuid: kotlin.String /* compiled code */ ... public object `$serializer` : kotlinx.serialization.KSerializer<User> { } }
  45. @kotlinx.serialization.Serializable public final data class User public constructor(uuid: kotlin.String, login:

    kotlin.String, name: kotlin.String, email: kotlin.String, extraInfo: kotlin.String?) { public companion object { } public final val email: kotlin.String /* compiled code */ @kotlinx.serialization.SerialName public final val extraInfo: kotlin.String? /* compiled code */ public final val login: kotlin.String /* compiled code */ public final val name: kotlin.String /* compiled code */ public final val uuid: kotlin.String /* compiled code */ ... public object `$serializer` : kotlinx.serialization.KSerializer<User> { } }
  46. jvm/Serialization.kt actual fun <T: Any> KClass<T>.serializer(): KSerializer<T> = this.java.invokeSerializerGetter() ?:

    throw SerializationException("Can't locate default serializer for class $this")
  47. jvm/Serialization.kt actual fun <T: Any> KClass<T>.serializer(): KSerializer<T> = this.java.invokeSerializerGetter() ?:

    throw SerializationException("Can't locate default serializer for class $this")
  48. jvm/Serialization.kt actual fun <T: Any> KClass<T>.serializer(): KSerializer<T> = this.java.invokeSerializerGetter() ?:

    throw SerializationException("Can't locate default serializer for class $this") • Example with Retrofit adapter:
 https://github.com/JakeWharton/SdkSearch (Kotlin multiplatform, coroutines and serialization)
 https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter
  49. Serialization • Gson vs Moshi vs Kotlinx Serialization… Oh my!

    • Field name policy issue: All libraries and plugins considered require some form of @SerializedName(“extra_info”) annotation to all camel cased fields • KSerializer: Multiplatform, no reflection, compiler plugin, more flexible adapters, but is its parsing faster?
  50. • Benchmark score using JMH (higher score is better)
 http://openjdk.java.net/projects/code-tools/jmh/

    • Original work comparing Moshi and Gson by Zac Sweers
 https://github.com/hzsweers/json-serialization-benchmarking • Our solution (at the time): Reflection + GsonValue adapters (for things that impact startup performance or APIs that we don’t control) Benchmark Mode Cnt Score Error Units
 moshi_reflective_fromJson thrpt 200 771.601 ± 8.017 ops/s gson_reflective_fromJson thrpt 200 637.594 ± 11.412 ops/s kserializer_fromJson thrpt 200 384.018 ± 3.459 ops/s moshi_reflective_toJson thrpt 200 1202.966 ± 6.690 ops/s gson_reflective_toJson thrpt 200 753.715 ± 18.660 ops/s kserializer_toJson thrpt 200 403.390 ± 1.846 ops/s Not so fast? 
 (results back in February)
  51. Just as fast now • Benchmark score using JMH (higher

    score is better)
 http://openjdk.java.net/projects/code-tools/jmh/ • Original work comparing Moshi and Gson by Zac Sweers
 https://github.com/hzsweers/json-serialization-benchmarking • Benchmark Mode Cnt Score Error Units
 moshi_reflective_fromJson thrpt 200 854.247 ± 6.300 ops/s gson_reflective_fromJson thrpt 200 1385.616 ± 14.821 ops/s kserializer_fromJson thrpt 200 1107.929 ± 51.456 ops/s moshi_reflective_toJson thrpt 200 1207.557 ± 8.226 ops/s gson_reflective_toJson thrpt 200 957.015 ± 4.439 ops/s kserializer_toJson thrpt 200 1175.760 ± 17.874 ops/s
  52. Just as fast now • Comparing with AutoValue: Benchmark Mode

    Cnt Score Error Units
 moshi_reflective_fromJson thrpt 200 854.247 ± 6.300 ops/s moshi_autovalue_fromJson thrpt 200 887.876 ± 6.539 ops/s gson_reflective_fromJson thrpt 200 1385.616 ± 14.821 ops/s gson_autovalue_fromJson thrpt 200 1435.158 ± 18.064 ops/s kserializer_fromJson thrpt 200 1107.929 ± 51.456 ops/s moshi_reflective_toJson thrpt 200 1207.557 ± 8.226 ops/s moshi_autovalue_toJson thrpt 200 1478.233 ± 2.913 ops/s gson_reflective_toJson thrpt 200 957.015 ± 4.439 ops/s gson_autovalue_toJson thrpt 200 1197.856 ± 2.830 ops/s kserializer_toJson thrpt 200 1175.760 ± 17.874 ops/s
  53. Migration Workflow • Convert from Java to Kotlin: Still an

    @AutoValue abstract class • Tedious and error prone process of manually converting each class to data class • In Java, accessing a data class field requires prefixing it with “get”
  54. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { write.run<Throwable> { //do something } return super.visitClass(klass, data) } }) } }
  55. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { write.run<Throwable> { //do something } return super.visitClass(klass, data) } }) } }
  56. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { write.run<Throwable> { //do something } return super.visitClass(klass, data) } }) } }
  57. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { write.run<Throwable> { //do something } return super.visitClass(klass, data) } }) } }
  58. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { write.run<Throwable> { //do something } return super.visitClass(klass, data) } }) } }
  59. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { write.run<Throwable> { } return super.visitClass(klass, data) } }) } }
  60. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { if (!klass.isData()) { write.run<Throwable> { klass.addModifier(KtTokens.DATA_KEYWORD) if (klass.isAbstract()) { klass.removeModifier(KtTokens.ABSTRACT_KEYWORD) } } } return super.visitClass(klass, data) } }) }
  61. class DataClassConverterAction : AnAction(“Data Class Converter") { override fun actionPerformed(event:

    AnActionEvent) { val project = event.project val file = event.getRequiredData(LangDataKeys.PSI_FILE) val write = WriteCommandAction.writeCommandAction(project) file.accept(object : KtTreeVisitor<Any>() { override fun visitClass(klass: KtClass, data: Any?): Void? { //... return super.visitClass(klass, data) } override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> { //do something } return super.visitNamedFunction(function, data) } }) }
  62. override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {

    //do something } return super.visitNamedFunction(function, data) }
  63. override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {

    val oldName = function.name ?: "" val newName = if (oldName.substring(0, 3) == "get") { oldName } else { "get${oldName.capitalize()}" } ReferencesSearch.search(function).findAll().forEach { it.handleElementRename(newName) } } return super.visitNamedFunction(function, data) }
  64. override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {

    val oldName = function.name ?: "" val newName = if (oldName.substring(0, 3) == "get") { oldName } else { "get${oldName.capitalize()}" } val factory = KtPsiFactory(project) val param = factory.createParameter("val $oldName: ${function.typeReference?.text}") ReferencesSearch.search(function).findAll().forEach { it.handleElementRename(newName) } function.containingClass()?.getValueParameterList()?.addParameter(param) function.delete() } return super.visitNamedFunction(function, data) }
  65. override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {

    val oldName = function.name ?: "" val newName = if (oldName.substring(0, 3) == "get") { oldName } else { "get${oldName.capitalize()}" } val factory = KtPsiFactory(project) val param = factory.createParameter("val $oldName: ${function.typeReference?.text}") ReferencesSearch.search(function).findAll().forEach { it.handleElementRename(newName) } function.containingClass()?.getValueParameterList()?.addParameter(param) function.delete() } return super.visitNamedFunction(function, data) }
  66. override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {

    val oldName = function.name ?: "" val newName = if (oldName.substring(0, 3) == "get") { oldName } else { "get${oldName.capitalize()}" } val factory = KtPsiFactory(project) val param = factory.createParameter("val $oldName: ${function.typeReference?.text}") ReferencesSearch.search(function).findAll().forEach { when (it.element.language) { JavaLanguage.INSTANCE -> it.handleElementRename(newName) KotlinLanguage.INSTANCE -> it.element.parent.replace(factory.createExpression(param.name!!)) } } function.containingClass()?.getValueParameterList()?.addParameter(param) function.delete()
  67. override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {

    val oldName = function.name ?: "" val newName = if (oldName.substring(0, 3) == "get") { oldName } else { "get${oldName.capitalize()}" } val factory = KtPsiFactory(project) val param = factory.createParameter("val $oldName: ${function.typeReference?.text}") ReferencesSearch.search(function).findAll().forEach { when (it.element.language) { JavaLanguage.INSTANCE -> it.handleElementRename(newName) KotlinLanguage.INSTANCE -> it.element.parent.replace(factory.createExpression(param.name!!)) } } function.containingClass()?.getValueParameterList()?.addParameter(param) function.delete()
  68. Some links • Source code for this example: 
 https://github.com/riclage/ConvertToDataClassPlugin

    • Intro to Intellij Plugins: 
 https://www.jetbrains.org/intellij/sdk/docs/basics.html • More examples:
 - Json to Data Class plugin: 
 https://github.com/wuseal/JsonToKotlinClass
 
 - Permissions dispatcher: 
 https://github.com/permissions-dispatcher/permissions-dispatcher-plugin
  69. Java Compatibility @Parcelize data class User(val uuid: String, val login:

    String, val name: String, val email: String, val extraInfo: String?): Parcelable { fun withExtraInfo(extraInfo: String): User { return copy(extraInfo = extraInfo) } companion object { @JvmStatic fun create(login: String, name: String, email: String) = User(UUID.randomUUID().toString(), login, name, email, null) } }
  70. Java Compatibility @Parcelize data class User(val uuid: String, val login:

    String, val name: String, val email: String, val extraInfo: String?): Parcelable { fun withExtraInfo(extraInfo: String): User { return copy(extraInfo = extraInfo) } companion object { @JvmStatic fun create(login: String, name: String, email: String) = User(UUID.randomUUID().toString(), login, name, email, null) } }
  71. The Importance of Tests • Ensure correct serialization • Ensure

    correct use of converted methods/fields • If needed, data classes can be mocked with Mockito (https://github.com/mockito/mockito/wiki/What%27s-new- in-Mockito-2#unmockable)
  72. Final result • 46 @AutoValue classes were generating 83 additional

    classes and 546 additional methods. • 4 dependencies removed (AutoValue + 3 plugins) / 1 dependency added (GsonValue) • Build time • Clean build: from ~5m42s to ~4m28s • Removing/adding a field: from ~54s to ~39s • Apk size remained roughly the same • Current app startup time with mixed serialisation approach: Up from ~1.2s to ~1.45s