Share
Embed
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
Transcript
85 slides
-
Outline • Android data model 演化史 • Kotlin 介紹 •
Kotlin classes/data classes 介紹 • Kotlin companion object 介紹 • Unit testing • What’s next? -
public class User { private String name; private String email;
private int age; private Date createdAt; } -
Data Model 演化史 • 定義 property • ⼿手動實作 get, set
• IDE 可以幫忙產⽣生 setter, getter -
Data Model 演化史 • 定義 property • ⼿手動實作 get, set
• IDE 可以幫忙產⽣生 setter, getter, equal, hash, toString -
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; } -
Parcelable • 遇到 parcelable • ⼿手動實作 • ⽤用 android studio
plugin 產⽣生 parcelable 實作 -
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];} }; -
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 ⾏行行 -
Parcelable • 遇到 parcelable • ⼿手動實作 • ⽤用 plugin 產⽣生
Parcelable 實作 • 當 class 越來來越多,property 越來來越多,開發的⼈人越來來越多 • 每次實作就成了了問題 -
AutoValue • 只要定義好 interface,就會⾃自動產⽣生 model • ⾃自動產⽣生 setter, getter, equal,
hash, toString • 有 plugin ⾃自動產⽣生 parcelable 實作 • Builder pattern -
@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(); } } -
AutoValue • 除此之外,加入兩兩個概念念: • Immutable Object: 預設產⽣生的 object 都是 value
object,沒有 setter,只有 getter • Null-safety: 每個 property 都需定義 NonNull, Nullable -
@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(); } } -
趨勢 • 經由上⾯面的例例⼦子,我們可以發現 Data Model 的趨勢為以下三點: • ⾃自動產⽣生 (Code Generation)
• Immutable Object • Null-safety -
Kotlin 介紹 val name: String = "Android" val email: String?
= null var name: String = "Android" -
Kotlin 介紹 val name: String = "Android" val email: String?
= null var name: String = "Android" name = "Android Taipei" -
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: -
class User constructor( name: String, email: String?, age: Int, createdAt:
Date ) { } • 定義 constructor -
class User( name: String, email: String?, age: Int, createdAt: Date
) { } • 可以省略略關鍵字 -
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 -
class User( var name: String, var email: String?, var age:
Int, var createdAt: Date ) { } • 更更簡單的寫法 -
class User( val name: String, val email: String?, val age:
Int, val createdAt: Date ) { } • 定義 immutable properties -
class User( val name: String = "No Name", val email:
String? = null, val age: Int = 0, val createdAt: Date = Date() ) { } • 給預設值 -
Data Classes • The compiler automatically derives the following members
from all properties declared in the primary constructor: • 根據你定義的 properties,⾃自動幫你產⽣生以下四種內容 • equals()/hashCode() • toString() • componentN() functions • copy() -
class User ( val name: String, val email: String?, val
age: Int, val createdAt: Date ) { } -
data class User( val name: String, val email: String?, val
age: Int, val createdAt: Date ) { } -
data class User( val name: String, val email: String?, val
age: Int, val createdAt: Date ) { } -
data class User( val name: String, val email: String?, val
age: Int, val createdAt: Date ) : AutoParcelable { } -
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(); } } -
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() 的實作如下 -
data class User( val name: String, val email: String?, val
age: Int, val createdAt: Date ) : AutoParcelable { } -
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) } } -
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) } } -
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); -
Companion Object • Kotlin 中沒有 static 的概念念,改⽤用 Companion Object •
Static method -> Companion method -
class User( val name: String, val email: String? ) {
companion object { fun myCompanionMethod() { // ... } } } -
class User( val name: String, val email: String? ) {
companion object { fun myCompanionMethod() { // ... } } } // Java 中使⽤用 User.Companion.myCompanionMethod(); -
Companion Object • Kotlin 中沒有 static 的概念念,改⽤用 Companion Object •
Static method -> companion method • 舉例例:ViewModel (MVVM 中的 ViewModel) -
User user = ... // age = 50 UserViewModel viewModel
= createViewModel(user) viewModel.getAgeString(); // "50 歲" -
class UserViewModel( context: Context, val user: User ) { val
ageString: String } -
class UserViewModel( context: Context, val user: User ) { val
ageString: String init { ageString = context.getString(R.string.age_is, user.age.toString()) } } -
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 歲" -
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) } } } -
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) } } } -
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) } } } -
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) } } } -
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(); -
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(); -
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); -
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 歲" -
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 -
Solution • 啟動 Mockito 2 的 option 功能 (opt-in
mocking of final classes/methods) • 在路路徑 test/resources/mockito-extensions 底下新增檔案: org.mockito.plugins.MockMaker • 檔案內容: mock-maker-inline -
如何開始導入 Kotlin 1.挑選最少⽤用到的 Data Model 2.對此 Data Model 寫測試 (encode/decode)
3.透過 Android Studio Converter (command+shift+a) 4.在⼀一個 commit 裡⼀一次做完 (要反悔悔也容易易) -
What’s Next • ViewModel 採⽤用 Kotlin • Presenter 採⽤用 Kotlin
• Helper 採⽤用 Kotlin • View 採⽤用 Kotlin • … -
What’s Next • 資料操作 • Collections: List, Set, Map •
map, filter, find…etc. • more… -
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