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; }
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
generates equals(), hashCode() and toString() for you • Plugin system for additional code generation: Parcelable implementation, Gson type adapters, etc. AutoValue to the rescue
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
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); } }
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); } }
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); } }
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); }
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); }
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); }
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); } }
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
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?
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:
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
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
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
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?
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.
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
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> { } }
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> { } }
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
• 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?
@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”
• 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
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)
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