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

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

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管理平台看看,果然上傳一個檔案了


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