2017年8月17日 星期四

【Android】Kotlin data class 使用心得



『Kotlin 將會取代 Java』


這是無庸置疑的,就好比『Swift 將會取代 Objective-C』,既然身為 Android 開發者的我們終究要面對,不如趁早來使用它!在使用 Kotlin 之前,我的 model 一直都是用 auto-value 來實作,原本我已經認為 auto-value 是極致的簡潔了(之前還特地寫了一系列的文章介紹),但兩者相較之下 Kotlin 撰寫出來的程式碼又更勝一籌

●基本介紹
在 Kotlin 裡面,只要在 class 的前面加上 data 這個關鍵字,你的class就會自動升級為 data class(這不是廢話嗎!),在變成所謂的 data class之後,Kotlin 的 compiler 便會根據你的 properties 自動幫你 override 掉原先的equals()/hashCode()/toString()這三個 method(當然如果你有自己的規則也是可以自己實作),並且幫你生成相對應的 getter and setter 以及 copy()

Before:
public class User {
    private final String name;
    private int age;
    
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @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;
        return name != null ? name.equals(user.name) : user.name == null;

    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

After:
data class User(val name: String, var age: Int)




上面的 User 有兩個 property,其中 name 為常數(不可變),在Java裡面我們將它定義為 final,而且只提供 getter 沒有 setter;
在 Kotlin 中沒有 final 這個關鍵字,必須使用 val / var 來區分常數 / 變數

val = 常數 = 僅提供 getter
var = 變數 = 提供 getter and setter

data class 也很貼心的幫我們生成了一個名為 cope() 的 method,具體用法如下:
val anson = User(name = "Anson", age = 18)
val bnson = anson.copy(name = "Bnson")
val olderBnson = bnson.copy(age= 19)




●實作 Parcelable
如果我們要在 Kotlin 中去實作 Parcelable 介面,除了硬幹之外,其實也是有不錯的 3rd party library :

● Parceler
● PaperParcel
● Smuggler

這邊我使用的是 Smuggler 這套,用起來最為簡單方便

Before:
data class User(val name: String, val age: Int) : Parcelable {
    constructor(parcel: Parcel) : this(
            parcel.readString(),
            parcel.readInt())
 
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeInt(age)
    }
 
    override fun describeContents(): Int {
        return 0
    }
 
    companion object CREATOR : Parcelable.Creator<user> {
        override fun createFromParcel(parcel: Parcel): User {
            return User(parcel)
        }
 
        override fun newArray(size: Int): Array<user> {
            return arrayOfNulls(size)
        }
    }
}

After:
data class User (val name: String, val age: Int) : AutoParcelable 



● Extension data class?
試想一下,如果我們的 model 有繼承關係在呢? 如果有一些property是所有 model 都會去用到的,那麼已往在 Java 時我們常常會將設計成一個Base類,然後其他 model 再去繼承他,大概會變成這樣(以下省略 methods 實作):
public abstract class Base {
    public String token;

    public Base(String token) {
        this.token = token;
    }
}

public class User extends Base implements Parcelable{
    public String name;
    public int age;


    public User(String token, String name, int age) {
        super(token);
        this.name = name;
        this.age = age;
    }

    protected User(Parcel in) {
        super(in.readString());
        name = in.readString();
        age = in.readInt();
    }

    public static final Creator<user> CREATOR = new Creator<user>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(token);
        dest.writeString(name);
        dest.writeInt(age);
    }
}
但如果換到了 Kotlin,我們宣告成 data class 時便會造成一個奇怪的現象
究竟是 Base 要去實作 equals,toString...還是 User 呢 ?
其實 Kotlin 的官方部落格有探討過這個問題了 (原文)
那如果我們依舊要保留一樣的設計,去確保程式的嚴謹性時該怎麼辦呢?
這時只需要將原本的抽象類改成介面即可😃

interface Base {
    val token: String
}

data class User(override val token: String, val name: String, val age: Int) : Base, AutoParcelable

2016年12月14日 星期三

【Android】最簡潔的Model層 - AutoValue 使用介紹【三】

這篇要來說的是 AutoValue 如何搭配【Gson】一塊使用,

由於已經將 Model 內原先的field變更為 abstract method 了,

可想而知在 Gson de/serialize 的過程中會發生問題!

ps.如果不了解 Gson 可以先參考我的另一篇文章

不做任何處理的話會發現在 call Gson 的 fromJson() 時會噴出 Exception,

java.lang.RuntimeException: Failed to invoke public com.yourpackagename.Book() with no args
如果要讓 Gson 成功的序列化,就必須給它一個新的 parse 規則(預設規則不適用在我們的 AutoValue Model 身上),

這邊我們一樣會用到一個 library【auto-value-gson

接下來只需要幾個簡單步驟:
一、在 Model 類別內新增靜態的 typeAdapter() method
二、新增一個抽象類實作 TypeAdapterFactory,並添加一個可產生物件的靜態 method
三、使用 GsonBuilder 來 create Gson,並給予我們自訂義的 TypeAdapterFactory




Gradle:

//auto-value-gson
provided 'com.ryanharter.auto.value:auto-value-gson:0.4.6'
annotationProcessor 'com.ryanharter.auto.value:auto-value-gson:0.4.6'



After:

//步驟一

@AutoValue
public abstract class Book {
    public abstract String name();
    public abstract int price();
    public abstract String author();

    public static Book create(String name, int price, String author) {
        return new AutoValue_Book(name, price, author);
    }

    public static TypeAdapter<Book> typeAdapter(Gson gson) {
        return new AutoValue_Book.GsonTypeAdapter(gson);
    }
}
//步驟二

@GsonTypeAdapterFactory
public abstract class GsonAdapterFactory implements TypeAdapterFactory {
    public static TypeAdapterFactory create() {
        return new AutoValueGson_GsonAdapterFactory();
    }
}
//步驟三

String jsonString = "{\"author\":\"Anson\",\"name\":\"安森瓦舍\",\"price\":87}";
Gson gson = new GsonBuilder().registerTypeAdapterFactory(GsonAdapterFactory.create()).create();
//serialize
Book book = gson.fromJson(jsonString, Book.class);
//deserialize
System.out.println(gson.toJson(book));



這邊特別要注意的是【步驟二】的 return new AutoValueGson_GsonAdapterFactory() 的部分,
如果你在【步驟一】的 model 中沒有 typeAdapter 的靜態方法,那麼在 build project 時,AutoValueGson_GsonAdapterFactory 是不會被創建出來的。所以步驟可別亂掉!




最後,我在github上創建了一個Project來演示AotoValue與Rxjava2Retrofit2搭配使用的範例。




延伸閱讀:

● 最簡潔的Model層 - AutoValue 使用介紹【一】
● 最簡潔的Model層 - AutoValue 使用介紹【二】

2016年12月13日 星期二

【Android】最簡潔的Model層 - AutoValue 使用介紹【二】

一般來說,在Android若要採用較高效能的Parcelable來做資料傳遞,我們需要在Model內實作【writeToParcel、describeContents、CREATOR】,

這實在是一個很繁雜的功夫,也就是前一篇提過的duplicate code,

還好,在我們使用了AutoValue後,就能一併使用另一個plugin(auto-value-parcel),順便解決這個煩死人的實作。

ps.關於Parcelable與Serializable的效能差異可以參考我的另一篇文章



Gradle:

//auto-value-parcel
annotationProcessor 'com.ryanharter.auto.value:auto-value-parcel:0.2.5'



Before:

public class Book implements Parcelable {
    private String name;
    private int price;
    private String author;

    public Book(String name, int price, String author) {
        this.name = name;
        this.price = price;
        this.author = author;
    }

    @Override
    public String toString() {
        return "Book{"
                + "name=" + name
                + ", price=" + price
                + ", author=" + author
                + "}";
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
    
    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(
                    in.readString(),
                    in.readInt(),
                    in.readString()
            );
        }
        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(price);
        dest.writeString(author);
    }
}



After:

@AutoValue
public abstract class Book implements Parcelable {
    public abstract String name();
    public abstract int price();
    public abstract String author();

    public static Book create(String name, int price, String author) {
        return new AutoValue_Book(name, price, author);
    }
}


基本上在implement Parcelable 後,就不需要做事囉!

有沒有發現,我們的Model層越來越簡潔,越來越可愛了♡♡




延伸閱讀:

● 最簡潔的Model層 - AutoValue 使用介紹【一】
● 最簡潔的Model層 - AutoValue 使用介紹【三】