Share
Embed
Transcript
87 slides
-
public class User { } String uuid; String login; String
name; String email; String extraInfo; -
public final class User { } String uuid; String login;
String name; String email; String extraInfo; private final private final private final private final private final -
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 -
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 -
@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; } -
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; } -
public User withExtraInfo(String extraInfo) { return new User(uuid, login, name,
email, extraInfo); } -
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(); } -
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 -
• 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 -
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 -
@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(); } -
// 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; -
@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(); } -
@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); } } -
@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); } } -
Plugins • AutoValue With https://github.com/gabrielittner/auto-value-with • AutoValue Gson https://github.com/rharter/auto-value-gson •
AutoValue Parcel https://github.com/rharter/auto-value-parcel -
@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); } } -
@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); } -
@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); } -
@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); } -
// 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; -
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"); -
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); } } -
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); } -
@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); } } -
@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) } } -
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 -
Data classes vs AutoValue • Less external dependencies • Simpler/smaller
code • No “open” abstract classes • Faster build / less overhead from annotation processors • Plugin issues -
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? -
@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: -
@Parcelize data class User(val uuid: String, val login: String,
val name: String, val email: String, val extraInfo: String?): Parcelable To this: -
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 -
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 -
@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 -
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? -
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. -
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) -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
@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> { } } -
@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> { } } -
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") -
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") -
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 -
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? -
• 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) -
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 -
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 -
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” -
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) } }) } } -
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) } }) } } -
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) } }) } } -
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) } }) } } -
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) } }) } } -
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) } }) } } -
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) } }) } -
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) } }) } -
override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {
//do something } return super.visitNamedFunction(function, data) } -
override fun visitNamedFunction(function: KtNamedFunction, data: Any?): Void { write.run<Throwable> {
} return super.visitNamedFunction(function, data) } -
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) } -
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) } -
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) } -
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() -
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() -
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 -
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) } } -
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) } } -
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) -
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