顯示具有 Android 標籤的文章。 顯示所有文章
顯示具有 Android 標籤的文章。 顯示所有文章

2018年12月29日 星期六

【Android】開發測試平台DeployGate介紹



DeployGate 是一個提拱開發者使用的測試平台,可以輕鬆的上傳並且分享 iOS 以及 Android 應用程式,非常適合用於開發階段提供團隊內成員進行 app 的溝通與協作。以下介紹在 android studio 內使用 Gradle 進行自動打包並部屬 apk 的流程.



1. 相依 gradle-deploygate-plugin 插件

在專安內的 build.gradle 文件中加入:
dependency {
  classpath "com.deploygate:gradle:1.1.5"
}
在專案內的 app/build.gradle 文件中加入:
apply plugin: 'deploygate'




2. 配置 gradle 文件設定

在專安內的 app/build.gradle 文件中加入:
    .
    .
    .

deploygate {
    userName = "[username of app owner]"
    token = "[your API token]"

    apks {
        debug {
            message = "[debug message]"
            releaseNote = "[release note]"
        }

        release {
            message = "[release message]"
            releaseNote = "[release note]"
        }
    }
}

userName 與 token(API key)可以在 DeployGate 的 Account Settings 裡面找到
apks 這整段為非必要 (可加可不加)





3. 運行 deploygate gradle 腳本

打開 android studio 右測的 Gradle 視窗,找到 app-> Tasks -> deplotgate -> uploadDeployGateDebug 並執行,即可自動建置並部屬至 DeployGate







● 其他 gradle 配置設定

我的個人習慣是在開發專案的過程中,會將 keystore 放在專案內(debug/release 皆是),方便不同電腦在 build apk 時可以保持一致性,如果要自定義 debug keystore 的路徑可以這麼做:

1.在專案內的 app/ 資料夾內新增一個資料夾 secret (名稱可以隨便取,但下面路徑記得也要跟著換),用來裝 keystore




2.在專安內的 app/build.gradle 文件中加入:

android {

    .
    .
    .

    signingConfigs {
        debug {
            storeFile file("../app/secret/debug_keystore.jks")
            storePassword "[your debug password]"
            keyAlias "[your debug keyAlias]"
            keyPassword "[your debug password]"
        }
        release {
            storeFile file("../app/secret/release_keystore.jks")
            storePassword "[your release password]"
            keyAlias "[your release keyAlias]"
            keyPassword "[your release password]"
        }
    }
    buildTypes {
        debug {
            minifyEnabled false
            debuggable true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.debug
        }
        release {
            minifyEnabled false
            debuggable false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
}

2018年2月20日 星期二

【Android】如何取得SHA1指紋認證與MD5

在很多地方都會需要用到 SHA 指紋認證,要取得 SHA 之前必須先擁有 keystore , keystore 又分為正式版與測試版,測試版的keystore在安裝 Android SDK 時就會一併產生在電腦中,正式版 keystore 則需要自己打包產生,以 Windows 系統為例
測試版 keystore 路徑為 C:\Users\YOUR_NAME\.android\debug.keystore

要取得 SHA 有兩種方式:
1. cmd (windows 命令提示字元) 下 command line
2. 在 Android Studio 內運行 Gradle 指令快速取得(只適用 debug keystore)




● 在 Windows 環境下使用 keytool 取得簽署憑證的 SHA 雜湊值


1.打開 命令提示字元(cmd) 視窗 / (使用Git Bash 也行)
2.切換路徑到 JDK 的 bin 資料夾 指令 : cd C:\Program Files\Java\jdk1.8.0_144\bin (黃字部分自行替換)
3.查詢指令 : keytool -list -v -keystore "C:\Users\YOUR_NAME\.android\debug.keystore" (黃字部分自行替換)
4.打上密碼 (debug.keystore 預設密碼為 android)







● 在 Android Studio 內快速取得 dubug 認證


如果手邊有 AS IDE 的話,打開專案,點開測邊欄的 Gradle 視窗,找到 Tasks 資料夾,這裡面有一些預設寫好的腳本可以執行,接著找到android -> signingReport,直接執行便可以得到電腦內 debug.keystore 的指紋認證等資訊



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 使用介紹【三】

【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倍

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





2015年8月21日 星期五

【Android】SignalR 使用介紹

Blogger

這篇要來介紹的是 如何在Android使用SignalR完成簡易聊天室

我們要完成上圖畫面

Server 端的實作可以參考我的這篇文章
MVC - 使用SignalR完成簡易聊天室



1.Add SignalR Lib


別懷疑!微軟沒有上傳SignalR的Maven位置
原始碼請至github下載(有點雷.趕時間別去踩)
或是你可以跟我一樣來這裡直接下載jar檔引用
除了要引用signalr-client-sdk還有Gson也要


Manifest記得要加上網路權限

<uses-permission android:name="android.permission.INTERNET"/>


2.Layout


兩個檔案,主畫面與listview的item


先看activity_main的layout

<?xml version="1.0" encoding="utf-8"?>
<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">

    <ListView
        tools:listitem="@layout/item"
        android:id="@+id/chatLv"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:divider="#ddd"
        android:dividerHeight="1dp"
        />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginBottom="2dp"
        android:layout_marginTop="2dp"
        android:background="#000" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="5dp">
        <EditText
            android:id="@+id/messageEt"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:padding="5dp"
            />
        <Button
            android:id="@+id/sendBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:text="SEND"
            />
    </LinearLayout>
</LinearLayout>

item的layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:padding="5dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/nameTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="3dp"
        android:text="Name"
        />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=":"
        />
    <TextView
        android:id="@+id/messageTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="3dp"
        android:text="Message"
        />
</LinearLayout>


2.Java Code


這邊有三個檔案,稍微解釋一下:
MainActivity(主畫面)
ChatAdapter(ListView Adapter)
ChatData(資料容器)


先說MainActivity,裡面的四個常數的意義
HUB_URL:就是你signalR的DomainName+/signalr
HUB_NAME:Hub檔案名稱
HUB_EVENT_NAME:Hub觸發的事件名稱
HUB_METHOD_NAME:Call的method名稱
下面這張圖可以清楚對照,如果不知道藍框裡面是什麼東西
請先看這篇


public class MainActivity extends Activity {
    private static final String HUB_URL = "[你的url]/signalr";
    private static final String HUB_NAME = "你的Hub name";
    private static final String HUB_EVENT_NAME = "你的事件名稱";
    private static final String HUB_METHOD_NAME = "你的Call method name";
    private SignalRFuture<Void> mSignalRFuture;
    private HubProxy mHub;
    private String mName;

    private ChatAdapter mChatAdapter;
    private ListView mChatLv;
    private EditText mMessageEt;
    private Button mSendBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mChatLv = (ListView)findViewById(R.id.chatLv);
        mMessageEt = (EditText)findViewById(R.id.messageEt);
        mSendBtn = (Button)findViewById(R.id.sendBtn);
        mName = "Android-"+System.currentTimeMillis();
        mChatAdapter = new ChatAdapter(this,0,new ArrayList<ChatData>(),mName);

        mChatLv.setAdapter(mChatAdapter);
        mSendBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    String message = mMessageEt.getText().toString();
                    mHub.invoke(HUB_METHOD_NAME, mName, message).get();
                    mMessageEt.setText("");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        });

        HubConnection connection = new HubConnection(HUB_URL);
        mHub = connection.createHubProxy(HUB_NAME);
        mSignalRFuture = connection.start(new ServerSentEventsTransport(connection.getLogger()));
        //可以理解為訊息or事件監聽器
        mHub.on(HUB_EVENT_NAME, new SubscriptionHandler2<String, String>() {
            @Override
            public void run(String name,String message) {
                //使用AsyncTask來更新畫面
                new AsyncTask<String,Void,ChatData>(){
                    @Override
                    protected ChatData doInBackground(String... param) {
                        ChatData chatData = new ChatData(param[0],param[1]);
                        return chatData;
                    }
                    @Override
                    protected void onPostExecute(ChatData chatData) {
                        mChatAdapter.add(chatData);
                        mChatLv.smoothScrollToPosition(mChatAdapter.getCount()-1);
                        super.onPostExecute(chatData);
                    }
                }.execute(name,message);
            }
        }, String.class,String.class);

        //開啟連線
        try {
            mSignalRFuture.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        //關閉連線
        mSignalRFuture.cancel();
        super.onDestroy();
    }
}

ChatData資料容器

public class ChatData {
    private String name;
    private String message;

    public ChatData(String name, String message) {
        this.name = name;
        this.message = message;
    }

    public String getName() {
        return name;
    }
    public String getMessage() {
        return message;
    }
}

ChatAdapter,這邊比較需要講一下的只有
我將自己的聊天內容標註為紅色而已

public class ChatAdapter extends ArrayAdapter<ChatData>{
    private String mName;
    public ChatAdapter(Context context, int resource, List<ChatData> objects,String mName) {
        super(context, resource, objects);
        this.mName = mName;
    }

    private class ViewHolder {
        TextView nameTv;
        TextView messageTv;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ChatData chatData = getItem(position);
        ViewHolder holder;
        if(convertView==null){
            holder = new ViewHolder();
            convertView = LayoutInflater.from(getContext()).inflate(R.layout.item, null);
            holder.nameTv = (TextView) convertView.findViewById(R.id.nameTv);
            holder.messageTv = (TextView) convertView.findViewById(R.id.messageTv);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        holder.nameTv.setText(chatData.getName());
        holder.messageTv.setText(chatData.getMessage());

        if(chatData.getName().equals(mName)){
            holder.nameTv.setTextColor(Color.RED);
        }
        return convertView;
    }
}


3.完成了!來看效果吧


android與brower對話





2015年8月19日 星期三

【Android】Azure Storage 使用介紹

微軟的Azure是一套很強大的雲端平台

Azure提供非常非常多的服務

我們最常使用的包括Web站台、SQL Database、儲存體等等

優點是支援各種語言(Java , Node.js , ASP , php ....等等)好處很多,而且收費便宜

想要使用Azure請先自行去微軟申請,這邊就不多加介紹如何申請了(首次使用免費一個月)

這篇是介紹如何使用Android 操作 Azure儲存體



1.建立Azure儲存體服務


進入你的Azure訂用帳戶,新增儲存體的服務
新增 -> 資料服務 -> 儲存體 -> 快速建立
填寫想要的URL 與 離你最近的位置 與 備份選項


按下建立後,要等一小段時間,好了之後可以看到畫面是這樣子
點選剛剛建立的儲存體進入管理畫面


按下容器 -> 建立容器
填寫名稱 與 存取權限(這邊因為要給Android存取,所以選公用)


好了之後回到儀表板,點選下面的管理存取金鑰
我們要記下2個東東,儲存體帳戶名稱 與 金鑰 (可以先隨便開個記事本把他們複製貼上)



2.回到Android


在gradle內Add Azure Lib



compile 'com.microsoft.azure.android:azure-storage-android:0.5.1'


3.在專案內建立一個AsyncTask


這是用來上傳檔案的Task

CONTAINER_NAME 是你剛剛自己取的
storageConnectionString 要依照格式
AccountName=剛剛複製下來的儲存體帳戶名稱
AccountKey=剛剛複製下來的金鑰



public class FileUploadTask extends AsyncTask {
    //容器名稱
    private static final String CONTAINER_NAME = "my-file";
    //連結字串
    private static final String storageConnectionString =
            "DefaultEndpointsProtocol=http;" +
            "AccountName=my01test01;" +
            "AccountKey=tTP+zfmrUGb6FhiPBW/fRNCrjnfwX1QkJIvLpMp0BpWvFhgGjhlq6Syn5DFGgMjTsvPwhC8GmB6db/jklDpzPw==";
    private ProgressDialog mDialog;
    private File mUploadFile;
    private Context mContext;

    public FileUploadTask(Context context,File uploadFile) {
        mContext = context;
        mUploadFile = uploadFile;
    }

    @Override
    protected void onPreExecute() {
        mDialog = new ProgressDialog(mContext);
        mDialog.setTitle("Tips");
        mDialog.setMessage("Uploading...");
        mDialog.setCancelable(false);
        mDialog.show();
        super.onPreExecute();
    }

    @Override
    protected Boolean doInBackground(Void... params) {
        boolean status;
        try {
            CloudStorageAccount storageAccount = CloudStorageAccount.parse(storageConnectionString);
            CloudBlobClient blobClient = storageAccount.createCloudBlobClient();
            CloudBlobContainer container = blobClient.getContainerReference(CONTAINER_NAME);
            container.createIfNotExists();

            String blobName = System.currentTimeMillis()+".txt";
            CloudBlockBlob blockBlob = container.getBlockBlobReference(blobName);
            blockBlob.upload(new FileInputStream(mUploadFile), mUploadFile.length());
            status = true;
        } catch (Exception e) {
            status = false;
        }
        return status;
    }

    @Override
    protected void onPostExecute(Boolean status) {
        if (status == true) {
            Toast.makeText(mContext,"檔案上傳成功", Toast.LENGTH_SHORT).show();
        }else{
            Toast.makeText(mContext,"檔案上傳失敗", Toast.LENGTH_SHORT).show();
        }
        mDialog.cancel();
        super.onPostExecute(status);
    }
}


4.主畫面與測試


在Activity裡面就一個Button,layout就不貼了
按下後產生一個檔案並上傳



public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button)findViewById(R.id.button1);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    //在Cache內產生檔案
                    String fileContent = "現在時間="+System.currentTimeMillis();
                    File saveFile = new File(MainActivity.this.getCacheDir(),"Test.txt");
                    FileOutputStream outStream = new FileOutputStream(saveFile);
                    outStream.write(fileContent.getBytes());
                    outStream.close();

                    //上傳
                    FileUploadTask fileUploadTask = new FileUploadTask(MainActivity.this,saveFile);
                    fileUploadTask.execute();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
    }
}


5.完成了!來看效果吧


按下按鈕後,跳出Dialog,結束後顯示Toast提示


回到Azure管理平台看看,果然上傳一個檔案了


將檔案下載下來,裡面的內容無誤