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

Kotlin Data Model

Kotlin Data Model

2017/10/26 Android Taipei 分享

透過 Data Model 的演進,了解 Kotlin 的優點,並開始導入

現今 Data Model 的趨勢為以下三點:
1. 自動產生 (Code Generation)
2. Immutable Object
3. Null-safety

了解如何透過 Kotlin Data Classes 達到以上三點要求

Demo Project:
https://github.com/ch8908/KotlinDataClass

Chien Shuo (Kros)

October 26, 2017
Tweet

More Decks by Chien Shuo (Kros)

Other Decks in Programming

Transcript

  1. Outline • Android data model 演化史 • Kotlin 介紹 •

    Kotlin classes/data classes 介紹 • Kotlin companion object 介紹 • Unit testing • What’s next?
  2. public class User {
 private String name;
 private String email;

    
 private int age;
 
 private Date createdAt;
 }
  3. Data Model 演化史 • 定義 property • ⼿手動實作 get, set

    • IDE 可以幫忙產⽣生 setter, getter
  4. Data Model 演化史 • 定義 property • ⼿手動實作 get, set

    • IDE 可以幫忙產⽣生 setter, getter, equal, hash, toString
  5. public class User implements Parcelable {
 private String name;
 


    private String email;
 
 private int age;
 
 private Date createdAt;
 
 @Override
 public int hashCode() {
 int result = name != null ? name.hashCode() : 0;
 result = 31 * result + (email != null ? email.hashCode() : 0);
 result = 31 * result + age;
 result = 31 * result + (createdAt != null ? createdAt.hashCode() : 0);
 return result;
 }
 
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;
 
 User user = (User) o;
 
 if (age != user.age) return false;
 if (name != null ? !name.equals(user.name) : user.name != null) return false;
 if (email != null ? !email.equals(user.email) : user.email != null) return false;
 return createdAt != null ? createdAt.equals(user.createdAt) : user.createdAt == null;
 
 }
 
 @Override
 public String toString() {
 return "User{" +
 "name='" + name + '\'' +
 ", email='" + email + '\'' +
 ", age=" + age +
 ", createdAt=" + createdAt +
 '}';
 }
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 public String getEmail() {
 return email;
 }
 
 public void setEmail(String email) {
 this.email = email;
 }
 
 public int getAge() {
 return age;
 }
 
 public void setAge(int age) {
 this.age = age;
 }
 
 public Date getCreatedAt() {
 return createdAt;
 }
 
 public void setCreatedAt(Date createdAt) {
 this.createdAt = createdAt;
 }
  6. public class User implements Parcelable {
 private String name;
 


    private String email;
 
 private int age;
 
 private Date createdAt;
 
 @Override
 public int hashCode() {
 int result = name != null ? name.hashCode() : 0;
 result = 31 * result + (email != null ? email.hashCode() : 0);
 result = 31 * result + age;
 result = 31 * result + (createdAt != null ? createdAt.hashCode() : 0);
 return result;
 }
 
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;
 
 User user = (User) o;
 
 if (age != user.age) return false;
 if (name != null ? !name.equals(user.name) : user.name != null) return false;
 if (email != null ? !email.equals(user.email) : user.email != null) return false;
 return createdAt != null ? createdAt.equals(user.createdAt) : user.createdAt == null;
 
 }
 
 @Override
 public String toString() {
 return "User{" +
 "name='" + name + '\'' +
 ", email='" + email + '\'' +
 ", age=" + age +
 ", createdAt=" + createdAt +
 '}';
 }
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 public String getEmail() {
 return email;
 }
 
 public void setEmail(String email) {
 this.email = email;
 }
 
 public int getAge() {
 return age;
 }
 
 public void setAge(int age) {
 this.age = age;
 }
 
 public Date getCreatedAt() {
 return createdAt;
 }
 
 public void setCreatedAt(Date createdAt) {
 this.createdAt = createdAt;
 }
 
 @Override
 public int describeContents() { return 0; }
 
 @Override
 public void writeToParcel(Parcel dest, int flags) {
 dest.writeString(this.name);
 dest.writeString(this.email);
 dest.writeInt(this.age);
 dest.writeLong(this.createdAt != null ? this.createdAt.getTime() : -1);
 }
 
 public User() {}
 
 protected User(Parcel in) {
 this.name = in.readString();
 this.email = in.readString();
 this.age = in.readInt();
 long tmpCreatedAt = in.readLong();
 this.createdAt = tmpCreatedAt == -1 ? null : new Date(tmpCreatedAt);
 }
 
 public static final Creator<User> CREATOR = new Creator<User>() {
 @Override
 public User createFromParcel(Parcel source) {return new User(source);}
 
 @Override
 public User[] newArray(int size) {return new User[size];}
 };

  7. public class User implements Parcelable {
 private String name;
 


    private String email;
 
 private int age;
 
 private Date createdAt;
 
 @Override
 public int hashCode() {
 int result = name != null ? name.hashCode() : 0;
 result = 31 * result + (email != null ? email.hashCode() : 0);
 result = 31 * result + age;
 result = 31 * result + (createdAt != null ? createdAt.hashCode() : 0);
 return result;
 }
 
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;
 
 User user = (User) o;
 
 if (age != user.age) return false;
 if (name != null ? !name.equals(user.name) : user.name != null) return false;
 if (email != null ? !email.equals(user.email) : user.email != null) return false;
 return createdAt != null ? createdAt.equals(user.createdAt) : user.createdAt == null;
 
 }
 
 @Override
 public String toString() {
 return "User{" +
 "name='" + name + '\'' +
 ", email='" + email + '\'' +
 ", age=" + age +
 ", createdAt=" + createdAt +
 '}';
 }
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 public String getEmail() {
 return email;
 }
 
 public void setEmail(String email) {
 this.email = email;
 }
 
 public int getAge() {
 return age;
 }
 
 public void setAge(int age) {
 this.age = age;
 }
 
 public Date getCreatedAt() {
 return createdAt;
 }
 
 public void setCreatedAt(Date createdAt) {
 this.createdAt = createdAt;
 }
 
 @Override
 public int describeContents() { return 0; }
 
 @Override
 public void writeToParcel(Parcel dest, int flags) {
 dest.writeString(this.name);
 dest.writeString(this.email);
 dest.writeInt(this.age);
 dest.writeLong(this.createdAt != null ? this.createdAt.getTime() : -1);
 }
 
 public User() {}
 
 protected User(Parcel in) {
 this.name = in.readString();
 this.email = in.readString();
 this.age = in.readInt();
 long tmpCreatedAt = in.readLong();
 this.createdAt = tmpCreatedAt == -1 ? null : new Date(tmpCreatedAt);
 }
 
 public static final Creator<User> CREATOR = new Creator<User>() {
 @Override
 public User createFromParcel(Parcel source) {return new User(source);}
 
 @Override
 public User[] newArray(int size) {return new User[size];}
 };
 隨隨便便便便就超過 100 ⾏行行
  8. Parcelable • 遇到 parcelable • ⼿手動實作 • ⽤用 plugin 產⽣生

    Parcelable 實作 • 當 class 越來來越多,property 越來來越多,開發的⼈人越來來越多 • 每次實作就成了了問題
  9. AutoValue • 只要定義好 interface,就會⾃自動產⽣生 model • ⾃自動產⽣生 setter, getter, equal,

    hash, toString • 有 plugin ⾃自動產⽣生 parcelable 實作 • Builder pattern
  10. @AutoValue
 public abstract class User implements Parcelable {
 
 public

    abstract String name();
 
 @Nullable
 public abstract String email();
 
 public abstract int age();
 
 public abstract Date createdAt();
 
 public static User create(String name, String email, int age, Date createdAt) {
 return builder()
 .name(name)
 .email(email)
 .age(age)
 .createdAt(createdAt)
 .build();
 }
 
 public static Builder builder() {return new AutoValue_User.Builder();}
 
 @AutoValue.Builder
 public abstract static class Builder {
 
 public abstract Builder name(String name);
 
 public abstract Builder email(String email);
 
 public abstract Builder age(int age);
 
 public abstract Builder createdAt(Date createdAt);
 
 public abstract User build();
 }
 }
  11. AutoValue • 除此之外,加入兩兩個概念念: • Immutable Object: 預設產⽣生的 object 都是 value

    object,沒有 setter,只有 getter • Null-safety: 每個 property 都需定義 NonNull, Nullable
  12. @AutoValue
 public abstract class User implements Parcelable {
 
 public

    abstract String name();
 
 @Nullable
 public abstract String email();
 
 public abstract int age();
 
 public abstract Date createdAt();
 
 public static User create(String name, String email, int age, Date createdAt) {
 return builder()
 .name(name)
 .email(email)
 .age(age)
 .createdAt(createdAt)
 .build();
 }
 
 public static Builder builder() {return new AutoValue_User.Builder();}
 
 @AutoValue.Builder
 public abstract static class Builder {
 
 public abstract Builder name(String name);
 
 public abstract Builder email(String email);
 
 public abstract Builder age(int age);
 
 public abstract Builder createdAt(Date createdAt);
 
 public abstract User build();
 }
 }
  13. Kotlin 介紹 val name: String = "Android" val email: String?

    = null var name: String = "Android"
 name = "Android Taipei"
  14. Kotlin 介紹 val name: String = "Android" val email: String?

    = null var name: String = "Android"
 name = "Android Taipei" • val: Assign-once (read-only) local variable • var: Mutable variable:
  15. class User(
 name: String, email: String?,
 age: Int,
 createdAt: Date


    ) { var name: String = name, var email: String? = email,
 var age: Int = age,
 var createdAt: Date = createdAt } • 定義 properties
  16. class User(
 var name: String, var email: String?,
 var age:

    Int,
 var createdAt: Date
 ) { } • 更更簡單的寫法
  17. class User(
 val name: String, val email: String?,
 val age:

    Int,
 val createdAt: Date
 ) { } • 定義 immutable properties
  18. class User(
 val name: String = "No Name", val email:

    String? = null,
 val age: Int = 0,
 val createdAt: Date = Date()
 ) { } • 給預設值
  19. Data Classes • The compiler automatically derives the following members

    from all properties declared in the primary constructor: • 根據你定義的 properties,⾃自動幫你產⽣生以下四種內容 • equals()/hashCode() • toString() • componentN() functions • copy()
  20. class User (
 val name: String, val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) { }
  21. data class User(
 val name: String, val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) { }
  22. data class User(
 val name: String, val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) { }
  23. data class User(
 val name: String, val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) : AutoParcelable { }
  24. data class User(
 val name: String, val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) : AutoParcelable { } public class User implements Parcelable {
 private String name;
 
 private String email;
 
 private int age;
 
 private Date createdAt;
 
 @Override
 public int hashCode() {
 int result = name != null ? name.hashCode() : 0;
 result = 31 * result + (email != null ? email.hashCode() : 0);
 result = 31 * result + age;
 result = 31 * result + (createdAt != null ? createdAt.hashCode() : 0);
 return result;
 }
 
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;
 
 User user = (User) o;
 
 if (age != user.age) return false;
 if (name != null ? !name.equals(user.name) : user.name != null) return false;
 if (email != null ? !email.equals(user.email) : user.email != null) return false;
 return createdAt != null ? createdAt.equals(user.createdAt) : user.createdAt == null;
 
 }
 
 @Override
 public String toString() {
 return "User{" +
 "name='" + name + '\'' +
 ", email='" + email + '\'' +
 ", age=" + age +
 ", createdAt=" + createdAt +
 '}';
 }
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 public String getEmail() {
 return email;
 }
 
 public void setEmail(String email) {
 this.email = email;
 }
 
 public int getAge() {
 return age;
 }
 
 public void setAge(int age) {
 this.age = age;
 }
 
 public Date getCreatedAt() {
 return createdAt;
 }
 
 public void setCreatedAt(Date createdAt) {
 this.createdAt = createdAt;
 }
 
 @Override
 public int describeContents() { return 0; }
 
 @Override
 public void writeToParcel(Parcel dest, int flags) {
 dest.writeString(this.name);
 dest.writeString(this.email);
 dest.writeInt(this.age);
 dest.writeLong(this.createdAt != null ? this.createdAt.getTime() : -1);
 }
 
 public User() {}
 
 protected User(Parcel in) {
 this.name = in.readString();
 this.email = in.readString();
 this.age = in.readInt();
 long tmpCreatedAt = in.readLong();
 this.createdAt = tmpCreatedAt == -1 ? null : new Date(tmpCreatedAt);
 }
 
 public static final Creator<User> CREATOR = new Creator<User>() {
 @Override
 public User createFromParcel(Parcel source) {return new User(source);}
 
 @Override
 public User[] newArray(int size) {return new User[size];}
 };
 @AutoValue
 public abstract class User implements Parcelable {
 
 public abstract String name();
 
 @Nullable
 public abstract String email();
 
 public abstract int age();
 
 public abstract Date createdAt();
 
 public static User create(String name, String email, int age, Date createdAt) {
 return builder()
 .name(name)
 .email(email)
 .age(age)
 .createdAt(createdAt)
 .build();
 }
 
 public static Builder builder() {return new AutoValue_User.Builder();}
 
 @AutoValue.Builder
 public abstract static class Builder {
 
 public abstract Builder name(String name);
 
 public abstract Builder email(String email);
 
 public abstract Builder age(int age);
 
 public abstract Builder createdAt(Date createdAt);
 
 public abstract User build();
 }
 }
  25. class User(
 val name: String, val email: String?
 ) {

    fun copy(name: String = this.name, email: String? = this.email): User {
 return User(name = name, email = email)
 } } • copy() 的實作如下
  26. data class User(
 val name: String, val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) : AutoParcelable { }
  27. data class User(
 val name: String,
 val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) : AutoParcelable {
 fun updateAge(newAge: Int): User {
 return copy(age = newAge)
 }
 }
  28. data class User(
 val name: String,
 val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) : AutoParcelable {
 fun updateAge(newAge: Int): User {
 return copy(age = newAge)
 }
 }
  29. data class User(
 val name: String,
 val email: String?,
 val

    age: Int,
 val createdAt: Date
 ) : AutoParcelable {
 fun updateAge(newAge: Int): User {
 return copy(age = newAge)
 }
 } user = user.updateAge(18);
 assertThat(user.getAge()).isEqualTo(18);
  30. class User(
 val name: String, val email: String?
 ) {

    companion object {
 fun myCompanionMethod() {
 // ...
 }
 } }
  31. class User(
 val name: String, val email: String?
 ) {

    companion object {
 fun myCompanionMethod() {
 // ...
 }
 } } // Java 中使⽤用 User.Companion.myCompanionMethod();
  32. Companion Object • Kotlin 中沒有 static 的概念念,改⽤用 Companion Object •

    Static method -> companion method • 舉例例:ViewModel (MVVM 中的 ViewModel)
  33. User user = ... // age = 50 UserViewModel viewModel

    = createViewModel(user) viewModel.getAgeString(); // "50 歲"
  34. class UserViewModel(
 context: Context,
 val user: User
 ) {
 val

    ageString: String
 
 init {
 ageString = context.getString(R.string.age_is, user.age.toString())
 }
 }
  35. class UserViewModel(
 context: Context,
 val user: User
 ) {
 val

    ageString: String
 
 init {
 ageString = context.getString(R.string.age_is, user.age.toString())
 }
 } UserViewModel viewModel = new UserViewModel(context, user); viewModel.getAgeString(); // “50 歲"
  36. data class UserViewModel(
 val user: User,
 val ageString: String
 )

    {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 }
  37. data class UserViewModel(
 val user: User,
 val ageString: String
 )

    {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 }
  38. data class UserViewModel(
 val user: User,
 val ageString: String
 )

    {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 }
  39. data class UserViewModel private constructor(
 val user: User,
 val ageString:

    String
 ) {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 }
  40. data class UserViewModel private constructor(
 val user: User,
 val ageString:

    String
 ) {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 } // Java 中使⽤用 new UserViewModel();
  41. data class UserViewModel private constructor(
 val user: User,
 val ageString:

    String
 ) {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 } // Java 中使⽤用 new UserViewModel();
  42. data class UserViewModel private constructor(
 val user: User,
 val ageString:

    String
 ) {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 } // Java 中使⽤用 new UserViewModel(); UserViewModel viewModel = UserViewModel.Companion.create(context, user);
  43. data class UserViewModel private constructor(
 val user: User,
 val ageString:

    String
 ) {
 companion object {
 fun create(context: Context, user: User): UserViewModel {
 val ageString = context.getString(R.string.age_is, user.age.toString())
 return UserViewModel(user, ageString)
 }
 }
 } // Java 中使⽤用 new UserViewModel(); UserViewModel viewModel = UserViewModel.Companion.create(context, user); viewModel.getAgeString(); // "50 歲"
  44. Unit Test • Kotlin 所有的東⻄西,預設都是 final • 當要 Mock (2.0)

    時,會出現以下問題: org.mockito.exceptions.base.MockitoException: 
 Cannot mock/spy class com.example.kotlindataclass.model.xxx cannot mock/spy because :
 - final class
  45. Solution • 啟動 Mockito 2 的 option 功能 
 (opt-in

    mocking of final classes/methods) • 在路路徑 test/resources/mockito-extensions 底下新增檔案:
 org.mockito.plugins.MockMaker • 檔案內容:
 mock-maker-inline
  46. 如何開始導入 Kotlin 1.挑選最少⽤用到的 Data Model 2.對此 Data Model 寫測試 (encode/decode)

    3.透過 Android Studio Converter (command+shift+a) 4.在⼀一個 commit 裡⼀一次做完 (要反悔悔也容易易)
  47. What’s Next • ViewModel 採⽤用 Kotlin • Presenter 採⽤用 Kotlin

    • Helper 採⽤用 Kotlin • View 採⽤用 Kotlin • …
  48. Reference • Kotlin Data Classes
 https://kotlinlang.org/docs/reference/data-classes.html • Kotlin 的 data

    class 會對你的程式產⽣生什什麼樣的化學反應
 https://ingramchen.io/blog/2017/06/kotlin-data-class.html • Kotlin Koans
 https://kotlinlang.org/docs/tutorials/koans.html • Mock the unmockable
 https://github.com/mockito/mockito/wiki/What%27s-new-in- Mockito-2#mock-the-unmockable-opt-in-mocking-of-final-classesmethods