2016年12月13日 星期二

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

寫程式應該要盡可能的避免duplicate code(重複程式碼),一樣的、沒有價值的東西,一直寫幹嘛?對吧!

在Android開發上,不管你是使用哪一種Architecture(MVC、MVP、MVVM...等等),都一定有Model層,Model是我們在程式開發上最原始、最簡單的東西。

但打開你的Project看看你的Model,會發現重複的method一直在出現【getter、setter、equals、hashCode、toString....】等等等。

現在,我們透過【AutoValue】就可以大幅的簡化它了。

這個由google maintain的github開源項目,在本文章撰寫時最新版本是v1.4.1



Library projects:

Gradle:

//auto-value
provided 'com.google.auto.value:auto-value:1.4.1'
annotationProcessor 'com.google.auto.value:auto-value:1.4.1'



以下使用一個Book類別做示範



Before:

public class Book {
    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;
    }
}
Book book = new Book("安森瓦舍", 87, "Anson");
System.out.println("書名:" + book.getName());
System.out.println("價格:" + book.getPrice());
System.out.println("作者:" + book.getAuthor());



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);
    }
}
Book book = Book.create("安森瓦舍", 87, "Anson");
System.out.println("書名:" + book.name());
System.out.println("價格:" + book.price());
System.out.println("作者:" + book.author());



如果你想使用Builder pattern的話

@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 builder()
                .name(name)
                .price(price)
                .author(author)
                .build();
    }

    public static Builder builder() {
        return new AutoValue_Book.Builder();
    }
    
    @AutoValue.Builder
    public abstract static class Builder {
        public abstract Builder name(String name);

        public abstract Builder price(int price);

        public abstract Builder author(String author);

        public abstract Book build();
    }
}
Book book = Book.create("安森瓦舍", 87, "Anson");
System.out.println("書名:" + book.name());
System.out.println("價格:" + book.price());
System.out.println("作者:" + book.author());



延伸閱讀:

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

2016年1月28日 星期四

【Android】Gson 使用介紹

Gson是Google在2008年就推出的一套Library

目的是提供開發者快速的將JSON字串(String)轉換成物件(Object)

以及將物件(Object)快速的轉換成JSON字串(String)

目前release到v2.8.1




Gradle:

//gson
compile 'com.google.code.gson:gson:2.8.1'



假設我們有這樣一組JSON字串要轉換成Object

[
    {
        "UserName":"Anson",
        "UserAge":20
    },
    {
        "UserName":"Kevin",
        "UserAge":30
    }
]

可以看出是由一個JSONArray包住兩個JSONObject
JSONObject裡面有兩個屬性,分別是UserName 與 UserAge
於是乎我們便定義了這樣的一個 Model 來裝這些資料

public class User {
    private String UserName;
    private int UserAge;

    public User(String userName, int userAge) {
        UserName = userName;
        UserAge = userAge;
    }

    public String getUserName() {
        return UserName;
    }

    public int getUserAge() {
        return UserAge;
    }
}



使用Gson將 JSON字串 轉換成 Array
只需要簡單一行就搞定!

String jsonString = "[{\"UserName\":\"Anson\",\"UserAge\":20},{\"UserName\":\"Kevin\",\"UserAge\":30}]";
User[] userArray = new Gson().fromJson(jsonString, User[].class);

如果想轉成 List 呢?

String jsonString = "[{\"UserName\":\"Anson123123\",\"UserAge\":20},{\"UserName\":\"Kevin\",\"UserAge\":30}]";
java.lang.reflect.Type listType = new TypeToken<Collection<User>>(){}.getType();
List<User> userList = new Gson().fromJson(jsonString , listType);



那如果是物件轉JSON字串呢?
也是很簡單~~

ArrayList<User> userList = new ArrayList();
for (int i=0;i<2;i++){
    User user = new User("Test"+i,i);
    userList.add(user);
}
String jsonString = new Gson().toJson(userList);
System.out.println(jsonString);

輸出結果:

[{"UserName":"Test0","UserAge":0},{"UserName":"Test1","UserAge":1}]



有發現上面的User class中

在第2.3行我們所定義的屬性為 UserNameUserAge

名子跟JSON裡面的屬性一樣

但這不符合JAVA中的命名規範(小寫開頭)

那如果我們將class中的屬性改成userNameuserAge

在進行轉換時就會失敗~因為大小寫與JSON字串不一致

於是我們可以加上Gson提供的一個很方便的功能

@SerializedName("對應的JSON屬性名稱")

我們可以將 model 改成這個樣子...

public class User {
    @SerializedName("UserName")
    private String name;
    
    @SerializedName("UserAge")
    private int age;

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

    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
}

這樣子就等於告訴Gson,在轉換JSON字串時需要將 Model 內的

nameUserName
ageUserAge

以及在 JSON字串 轉 物件 時,將

UserNamename
UserAgeage






來試試複雜一點的例子吧:

{
    "Id":123456,
    "Name":"張三",
    "Gender":true,
    "Phones":[
        {"Type":"Landline","Number":"02-2800-0000"},
        {"Type":"Mobile","Number":"0900-000-000"}
    ],
    "Hobbies":[
        "釣魚","睡覺","打籃球"
    ]
}

這組 JSON 裡面可以看出,這是在描述一個人
他叫做張三,性別男(假設男為true,女為false),電話有兩支,一支市話一支手機
興趣有三個,分別是釣魚.睡覺.打籃球
來定義 Model 吧:

public class Guest {
    @SerializedName("Id")
    private int id;

    @SerializedName("Name")
    private String name;

    @SerializedName("Gender")
    private boolean gender;

    @SerializedName("Phones")
    private List<Phone> phoneList;

    @SerializedName("Hobbies")
    private List<String> hobbyList;

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public boolean getGender() {
        return gender;
    }

    public List<Phone> getPhoneList() {
        return phoneList;
    }

    public List<String> getHobbyList() {
        return hobbyList;
    }

    @Override
    public String toString() {
        return new Gson().toJson(this, Guest.class);
    }

    public static class Phone {
        enum Type {
            Landline, Mobile
        }

        @SerializedName("Type")
        private Type type;

        @SerializedName("Number")
        private String number;

        public Type getType() {
            return type;
        }

        public String getNumber() {
            return number;
        }
    }
}

在這個 Guest 這個 class 中
先使用 SerializedName 將原本 JSON字串 的屬性名稱轉換成我們想要的名子
接著在42行定義了一個 inner class 來裝電話的資料(當然不一定要使用內部類)
可以看到43行 列舉(enum) 也是可以在 gson 轉換中直接使用的
在38行我 override 了 toString 方法,目的是印 log 比較方便
定義好了 Model 後,接著的資料轉換就輕鬆多了!

String jsonString = "{\"Id\":123456,\"Name\":\"張三\",\"Gender\":true,\"Phones\":[{\"Type\":\"Landline\",\"Number\":\"02-2800-0000\"},{\"Type\":\"Mobile\",\"Number\":\"0900-000-000\"}],\"Hobbies\":[\"釣魚\",\"睡覺\",\"打籃球\"]}";
Guest guest = new Gson().fromJson(jsonString, Guest.class);
System.out.println(guest.toString());

也是一行輕鬆搞定

輸出結果:

{"Gender":true,"Hobbies":["釣魚","睡覺","打籃球"],"Id":123456,"Name":"張三","Phones":[{"Number":"02-2800-0000","Type":"Landline"},{"Number":"0900-000-000","Type":"Mobile"}]}



與Gson類似的Library還可以參考FasterXML jackson

2015年8月29日 星期六

【Android】Serializable vs Parcelable



我們都知道在Android內如果要傳送資料必須透過Intent或Bundle

在送基本型態(int,String,boolean..)時應該沒有什麼爭議

但是如果要送一個 Object 呢?

這時你就有兩個選擇了,你可以使用
● Serializable
● Parcelable

稍微解釋一下這兩個interface

Serializable 是 Java 的 interface
Parcelable 是 Android 的 interface
在使用他們時,你的 Class 必須 implements 他們

既然都可以達到資料傳送的目的
那為什麼Android還要特地出一個 Parcelable 給大家使用呢?

當然是因為 performance 的問題
Serializable 在使用上非常方便
只需要 implements 他就結束了,但是
Serializable 在Java就一直被大家所詬病
他是利用反射來實例化物件
但這種方法會造成大量的temporary objects(臨時物件)
VM也要花時間對這些對象進行garbage collection(GC)

當然 Parcelable 就不是利用反射了
而是我們在 implement 他時就實作他的method
透過泛型直接給定他Class


下面有個簡單的Sample
讓我們實際測試看看兩者之間的效能落差



1.專案配置


很簡單,就一個Layout 與 Activity 與兩個 Model Object



2.Layout


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_marginBottom="20dp"
        android:padding="10dp"
        android:text="測試執行50000次的\nParcelable/Serializable效能差異"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:padding="10dp"
        android:text="Parcelable 所花時間(ms):"
        android:id="@+id/pTimeTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:padding="10dp"
        android:text="Serializable 所花時間(ms):"
        android:id="@+id/sTimeTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:textColor="#cc0000"
        android:padding="10dp"
        android:text="效能落差(ms):"
        android:id="@+id/parcelableTimeTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="執行測試"
        android:id="@+id/button"
        />
</LinearLayout>


3.Java Code


DataP

import android.os.Parcel;
import android.os.Parcelable;

public class DataP implements Parcelable{
    private int a;
    private String b;
    private boolean c;

    public DataP(int a, String b, boolean c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }

    public boolean isC() {
        return c;
    }

    public void setC(boolean c) {
        this.c = c;
    }

    protected DataP(Parcel in) {
        a = in.readInt();
        b = in.readString();
    }

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

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

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(a);
        dest.writeString(b);
    }
}

DataS

import java.io.Serializable;

public class DataS implements Serializable {
    private int a;
    private String b;
    private boolean c;

    public DataS(int a, String b, boolean c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }

    public boolean isC() {
        return c;
    }

    public void setC(boolean c) {
        this.c = c;
    }
}

MainActivity
在兩個for迴圈內做5萬次的 put 與 get (單純模擬)
最後把執行時間相減來做出比較

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final Button button = (Button)findViewById(R.id.button);
        final TextView pTimeTv = (TextView)findViewById(R.id.pTimeTv);
        final TextView sTimeTv = (TextView)findViewById(R.id.sTimeTv);
        final TextView parcelableTimeTv = (TextView)findViewById(R.id.parcelableTimeTv);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bundle bundle = new Bundle();

                long pStart = System.currentTimeMillis();
                for(int i=0;i<50000;i++){
                    DataP p1 = new DataP(i,String.valueOf(i),true);
                    bundle.putParcelable("keyP" + i, p1);
                    DataP p2 = bundle.getParcelable("keyP"+i);
                }
                long pEnd = System.currentTimeMillis();
                long sStart = System.currentTimeMillis();
                for(int i=0;i<50000;i++){
                    DataS s1 = new DataS(i,String.valueOf(i),true);
                    bundle.putSerializable("keyS" + i,s1);
                    DataS s2 = (DataS)bundle.getSerializable("keyS"+i);
                }
                long sEnd = System.currentTimeMillis();

                long pSub = pEnd - pStart;
                long sSub = sEnd - sStart;
                long parcelableSub = (sSub>pSub)?(sSub-pSub):(pSub-sSub);

                pTimeTv.setText("Parcelable 所花時間(ms):"+pSub);
                sTimeTv.setText("Serializable 所花時間(ms):"+sSub);
                parcelableTimeTv.setText("效能落差(ms):"+parcelableSub);
            }
        });
    }
}


4.總結


從我的不專業測試得知結果為
創建5萬個物件(序/反列化)兩者之間時間差為0.7
Parcelable 大約是 Serializable 的兩倍

試想如果今天是10萬或20萬個物件呢,整體速度都會被拖慢!
你的App使用起來就會很卡頓!
所以還是多使用 Parcelable 吧!

兩者優缺點比較:

Serializable:
● 程式碼簡潔,閱讀容易
● 反序列化時需要強制轉型
● 速度較慢

Parcelable:
● 程式碼較複雜
● 速度較快,佔用資源較少

國外的說法是兩者之間效能差約10倍

個人是覺得沒那麼誇張
不過兩倍以上是有的