分類
Android Uncategorized

Android AsyncTask

相較於 HandlerThreadAndroid Worker Thread(Message+Handler+Looper) 這類比較底層的實作,確實可以掌控訊息傳遞機制的流程。
但在目的比較簡單的情況下,Android也有提供使用上較為方便的類別,AsyncTask。
事實上 AsyncTask 較廣為人知,在UI Thread 上的工作幾乎都可以勝任,其source code註解也有提到

AsyncTask enables proper and easy use of the UI thread.
This class allows to perform background operations and publish results on the UI thread
without having to manipulate threads and/or handlers

簡單的說 AsyncTask 讓你不需要操作 thread 和 handler 就能完成 UI thread 的工作。
但在註解第2段也有提到AsyncTask比較適合短時間的操作(幾秒鐘),長時間的操作請考慮使用 java.util.concurrent 套件。

AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework.
AsyncTasks should ideally be used for short operations (a few seconds at the most.)
If you need to keep threads running for long periods of time, it is highly recommended youuse the various APIs provided by the java.util.concurrent package such as Executor, ThreadPoolExecutor and FutureTask.

首先 AsyncTask 提供4個方法可以複寫,每個方法對應不同的目的。

onPreExecute() :

大多數的初始化工作都可以寫在這裡。除了建構式之外,onPreExecute是第一個被呼叫的。

doInBackground() :

需時操作,可以有參數以及返回值,參數型態必須在定義類別時決定,並在呼叫execute方法時傳入。
返回值也必須在定義類別時決定型態,該型態會相等於onPostExecute()的參數型態。

onProgressUpdate() :

可搭配呼叫 publishProgress方法,由於在doInBackground中不可對 UI thread 的 UI widget 作動作(會產生RuntimeException),若有需要在每次循環中更新UI,
可寫在onProgressUpdate()中。

onCancelled() :

當AsyncTask運行時可以中斷該AsyncTask,而中斷AsyncTask的後續處理,就寫在本方法中。
可以有參數,該參數型態也必須和doInBackground方法的參數型態相同。

onPostExecuted() :

doInBackground()完成的後續處理,當doInBackground完成後會回傳數值,其數值會被傳入onPostExecuted()方法中。
這裡通常放置需時工作完成後,更新UI的動作。

其中 onPreExecute , onPostExecute , onCancelled , onProgressUpdate 都是 run on ui thread ,

也就是說可以在這4個方法中調整 UI 。

首先是外部呼叫範例

package com.foxx.basic.asyctask;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import com.example.asyctask.R;
public class MainActivity extends Activity implements OnClickListener
{
    public static final String TAG = "asyctask";
    private TextView mTextView;
    private Button mStartButton;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.timeshower);
        mStartButton = (Button) findViewById(R.id.startbutton);
        mStartButton.setOnClickListener(this);
        mStartButton.setText("start a count time dialog");
    }
    @Override
    public void onClick(View view)
    {
        int resId = view.getId();
        switch (resId) {
            case R.id.startbutton:
                executeCountTimerTask();
                break;
        }
    }
    private void executeCountTimeTask()
    {
        SimpleCountTimer countTimer = new SimpleCountTimer(this);
        countTimer.execute(10d);
    }
    public TextView getTextView(){
        return mTextView;
    }
}

只需要注意第40行的 executeCountTimerTask 方法,該方法會在按鈕被按下時被呼叫,其中 10d 為計算秒數的結束時間。
以下是SimpleCountTimer.java

package com.foxx.basic.asyctask;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.util.Log;
public class SimpleCountTimer extends AsyncTask<Double, Integer, String>
{
    protected static final String TAG = "SimpleCountTimer";
    private static final long TIME_SLEEP_INTERVAL = 1000;
    private int mCountTime;
    private ProgressDialog mProgressDialog;
    private MainActivity mMainActivity;
    private Double mCountMax;
    public SimpleCountTimer(MainActivity activity) {
        Log.d(TAG, "in consturctor");
        mMainActivity = activity;
    }
    @Override
    protected String doInBackground(Double... params)
    {
        Log.d(TAG, "in doInBackground");
        mCountMax = params[0];
        while (!isCancelled() && mCountTime < mCountMax) {
            mCountTime++;
            publishProgress(mCountTime);
            threadSleep();
        }
        return "" + mCountTime;
    }
    private void threadSleep()
    {
        try {
            Thread.sleep(TIME_SLEEP_INTERVAL);
        } catch (InterruptedException e) {
            Log.d(TAG, "InterruptedException from threadSleep");
        }
    }
    @Override
    protected void onPreExecute()
    {
        Log.d(TAG, "onPreExecute");
        super.onPreExecute();
        mProgressDialog = new ProgressDialog(mMainActivity);
        mProgressDialog.setCanceledOnTouchOutside(false);
        mProgressDialog.setButton(DialogInterface.BUTTON_POSITIVE, "Cancel Count",
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which)
                    {
                        mProgressDialog.dismiss();
                        cancel(true);
                    }
                });
        mProgressDialog.show();
    }
    @Override
    protected void onProgressUpdate(Integer... values)
    {
        Log.d(TAG, "onProgressUpdate");
        mProgressDialog.setMessage("count time :" + values[0]);
    }
    @Override
    protected void onCancelled(String result)
    {
        Log.d(TAG, "onCancelled");
        mMainActivity.getTextView().setText("AsyncTask canceled!! count time at:" + result);
    }
    @Override
    protected void onPostExecute(String result)
    {
        Log.d(TAG, "onPostExecute");
        mProgressDialog.dismiss();
        mMainActivity.getTextView().setText("final time is: " + result);
    }
}

SimpleCountTimer 的工作相當簡單,從外部被呼叫以後,會產生一個Progress Dialog,並在其中顯示計算秒數,計算的時間取決於外部呼叫時傳入的參數而定。
第8行 extends AsyncTask<Double, Integer, String> , 如果沒有傳入參數的需要,可以設定為Void,以下說明各數值影響的方法

Double 為外部呼叫 execute 方法時傳入的參數型態定義,以及doInackground的參數

    private void executeCountTimeTask()
    {
        ...
        countTimer.execute(10d);
    }

此數值會被傳入到doInBackground方法中。

    @Override
    protected String doInBackground(Double... params)
    {
      ...
    }

 

Integer 會影響publishProgress傳入的參數,

    private int mCountTime;
    @Override
    protected String doInBackground(Double... params)
    {
        ...
        publishProgress(mCountTime);
        ...
    }

也會影響onProgressUpdate方法的參數

    @Override
    protected void onProgressUpdate(Integer... values)
    {
        ...
    }

 

String 影響 doInBackground 的回傳值,

    @Override
    protected String doInBackground(Double... params)
    {
        ...
    }

以及onCancelled 和 onPostExecute 的參數

    @Override
    protected void onCancelled(String result)
    {
     ...
    }
    @Override
    protected void onPostExecute(String result)
    {
     ...
    }

接著看看個別方法中的內容
第23行 doInBackground 取得外部傳入的數值,並設定為結束值,在while迴圈中遞增計算秒數,當秒數超過最大值或是有中斷發生,即會結束迴圈。
第45行 onPreExecute 方法初始化了 mProgressDialog,並在 mProgressDialog 設定了一個 Button,
button 被按下會中斷 doInBackground 方法,當 cancell(false) , 會讓 isCancelled() 回傳 true,但這種作法對被 block 住的 doInBackground 是沒有效果的 ,
為了保證被中斷,這裡使用 cancell(true),當 cancell(true),除了讓 isCancelled() 回傳 true 以外,還會丟出 InterruptedException ,
InterruptedException 會被 threadSleep 捕捉, 捕捉之後便會呼叫 onCancelled 方法。
第65行更新數值並顯示在 mProgressDialog中
第72行為中斷後的 UI 更新動作。
第79行為完成 doInBackground方法的 UI 更新動作。
接著來看看正常完成的log內容

07-10 17:07:40.535: D/SimpleCountTimer(11457): in consturctor
07-10 17:07:40.535: D/SimpleCountTimer(11457): onPreExecute
07-10 17:07:40.635: D/SimpleCountTimer(11457): in doInBackground
07-10 17:07:40.725: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:41.635: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:42.635: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:43.635: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:44.635: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:45.645: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:46.645: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:47.645: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:48.645: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:49.645: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:07:50.645: D/SimpleCountTimer(11457): onPostExecute

順序為 consturctor -> onPreExecute -> doInBackground -> onProgressUpdate(重複) -> onPostExecute
 
發生中斷的log內容

07-10 17:10:36.685: D/SimpleCountTimer(11457): in consturctor
07-10 17:10:36.685: D/SimpleCountTimer(11457): onPreExecute
07-10 17:10:36.765: D/SimpleCountTimer(11457): in doInBackground
07-10 17:10:36.855: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:10:37.765: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:10:38.765: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:10:39.765: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:10:40.765: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:10:41.775: D/SimpleCountTimer(11457): onProgressUpdate
07-10 17:10:41.775: D/SimpleCountTimer(11457): InterruptedException from threadSleep
07-10 17:10:41.805: D/SimpleCountTimer(11457): onCancelled

順序為 consturctor -> onPreExecute -> doInBackground -> onProgressUpdate(重複) -> onCancelled
 
 
 
 
 
 

分類
Android Uncategorized

Add UI Layout by java code

一般產生 Layout 的作法大多數是使用 xml 來達成,但在一些特殊情況下必須在 java code 建立 Layout。
本篇紀錄如何在 java code 中產生 Layout。
首先貼上示意圖
device-2015-07-16-131853
可以看到示意圖中主要有4個區塊,黑,藍,紅,綠,其中黑跟綠的width都佔滿屏,藍和紅的width佔滿屏的0.5。
而Height部份,黑佔0.2,其他區塊佔0.4。
以下為source code,
第18行的藉由 Display 來計算螢幕的 width 和 height 。
第38行為了在各元件中手動設定id,提供了generateID()方法,內容參考View.generateID()。
第52行createLayout()方法提供給Activity設定Layout。
第67,84,103,121行的initxxx方法,則是各個區塊的初始化內容。

import java.util.concurrent.atomic.AtomicInteger;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class ViewCreater
{
    private static final String TAG = "ViewCreater";
    private static int mId = 0;
    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
    private Context mContext;
    private Display mDisplay;
    private RelativeLayout upLayout;
    private RelativeLayout mMiddleLeftLayout;
    private RelativeLayout mMiddleRightLayout;
    private RelativeLayout mDownLayout;
    public ViewCreater(Context context) {
        mContext = context;
        initDisplay();
    }
    private void initDisplay()
    {
        WindowManager manager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        mDisplay = manager.getDefaultDisplay();
    }
    public static int generateId()
    {
        if (Build.VERSION.SDK_INT < 17) {
            for (;;) {
                final int result = sNextGeneratedId.get();
                int newValue = result + 1;
                if (newValue > 0x00FFFFFF)
                    newValue = 1;
                if (sNextGeneratedId.compareAndSet(result, newValue)) {
                    return result;
                }
            }
        } else {
            return View.generateViewId();
        }
    }
    public RelativeLayout createLayout()
    {
        RelativeLayout rootlayout = new RelativeLayout(mContext);
        rootlayout.setId(generateId());
        initUpLayout();
        initMiddleLeftLayout();
        initMiddleRightLayout();
        initDownLayout();
        rootlayout.addView(mDownLayout);
        rootlayout.addView(mMiddleLeftLayout);
        rootlayout.addView(upLayout);
        rootlayout.addView(mMiddleRightLayout);
        return rootlayout;
    }
    private void initUpLayout()
    {
        upLayout = new RelativeLayout(mContext);
        upLayout.setId(generateId());
        upLayout.setTag("upLayout");
        RelativeLayout.LayoutParams layoutParamsOfUp = new RelativeLayout.LayoutParams(
                mDisplay.getWidth() / 1, mDisplay.getHeight() / 5);
        layoutParamsOfUp.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
        upLayout.setLayoutParams(layoutParamsOfUp);
        TextView textViewOfUplayout = new TextView(mContext);
        textViewOfUplayout.setId(generateId());
        textViewOfUplayout.setText("upLayout");
        textViewOfUplayout.setTextSize(20);
        upLayout.addView(textViewOfUplayout);
        upLayout.setBackgroundColor(Color.BLACK);
    }
    private void initMiddleRightLayout()
    {
        mMiddleRightLayout = new RelativeLayout(mContext);
        mMiddleRightLayout.setId(generateId());
        RelativeLayout.LayoutParams layoutParamsOfMiddleRightLayout = new RelativeLayout.LayoutParams(
                mDisplay.getWidth() / 2, mDisplay.getHeight() * 2 / 5);
        TextView textViewOfMiddleRightLayout = new TextView(mContext);
        textViewOfMiddleRightLayout.setId(generateId());
        textViewOfMiddleRightLayout.setText("middleRightLayout");
        textViewOfMiddleRightLayout.setTextSize(20);
        textViewOfMiddleRightLayout.setTextColor(Color.parseColor("#ffff00"));
        mMiddleRightLayout.addView(textViewOfMiddleRightLayout);
        layoutParamsOfMiddleRightLayout.addRule(RelativeLayout.BELOW, upLayout.getId());
        layoutParamsOfMiddleRightLayout
                .addRule(RelativeLayout.RIGHT_OF, mMiddleLeftLayout.getId());
        mMiddleRightLayout.setLayoutParams(layoutParamsOfMiddleRightLayout);
        mMiddleRightLayout.setBackgroundColor(Color.RED);
    }
    private void initMiddleLeftLayout()
    {
        mMiddleLeftLayout = new RelativeLayout(mContext);
        mMiddleLeftLayout.setId(generateId());
        RelativeLayout.LayoutParams layoutParamsOfMiddleLeftLayout = new RelativeLayout.LayoutParams(
                mDisplay.getWidth() / 2, mDisplay.getHeight() * 2 / 5);
        TextView textViewOfMiddleLeftLayout = new TextView(mContext);
        textViewOfMiddleLeftLayout.setId(generateId());
        textViewOfMiddleLeftLayout.setText("middleLeftLayout");
        textViewOfMiddleLeftLayout.setTextSize(20);
        textViewOfMiddleLeftLayout.setTextColor(Color.parseColor("#ff0000"));
        mMiddleLeftLayout.addView(textViewOfMiddleLeftLayout);
        layoutParamsOfMiddleLeftLayout.addRule(RelativeLayout.BELOW, upLayout.getId());
        layoutParamsOfMiddleLeftLayout.addRule(RelativeLayout.ALIGN_LEFT, upLayout.getId());
        mMiddleLeftLayout.setLayoutParams(layoutParamsOfMiddleLeftLayout);
        mMiddleLeftLayout.setBackgroundColor(Color.BLUE);
    }
    private void initDownLayout()
    {
        mDownLayout = new RelativeLayout(mContext);
        mDownLayout.setId(generateId());
        RelativeLayout.LayoutParams layoutParamsOfDownLayout = new RelativeLayout.LayoutParams(
                mDisplay.getWidth() / 1, mDisplay.getHeight() * 2 / 5);
        layoutParamsOfDownLayout.addRule(RelativeLayout.BELOW, mMiddleLeftLayout.getId());
        TextView textViewOfDownLayout = new TextView(mContext);
        textViewOfDownLayout.setId(generateId());
        textViewOfDownLayout.setText("DownLayout");
        textViewOfDownLayout.setTextSize(20);
        textViewOfDownLayout.setTextColor(Color.parseColor("#ffffff"));
        mDownLayout.addView(textViewOfDownLayout);
        mDownLayout.setLayoutParams(layoutParamsOfDownLayout);
        mDownLayout.setBackgroundColor(Color.GREEN);
    }
}

 
有幾個必須注意的要點。

1.即使加入所有UI widget之後,還是可以調整各個UI widget的屬性。

比如說若修改createLayout方法, 其中第14,15,16行重新修改了綠色區塊的width。

public RelativeLayout createLayout()
{
        RelativeLayout rootlayout = new RelativeLayout(mContext);
        rootlayout.setId(generateId());
        initUpLayout();
        initMiddleLeftLayout();
        initMiddleRightLayout();
        initDownLayout();
        rootlayout.addView(mDownLayout);
        rootlayout.addView(mMiddleLeftLayout);
        rootlayout.addView(upLayout);
        rootlayout.addView(mMiddleRightLayout);
       RelativeLayout.LayoutParams scaleLayoutParams = new RelativeLayout.LayoutParams(mDisplay.getWidth()/2, mDisplay.getHeight()*2/5);
       scaleLayoutParams.addRule(RelativeLayout.BELOW, mMiddleLeftLayout.getId());
       mDownLayout.setLayoutParams(scaleLayoutParams);
        return rootlayout;
}

綠色區塊的width即會變為滿版的0.5。如下圖
device-2015-07-08-160007
 

2.對Parent View來說,addView的順序和各child View的相依性並沒有一定的關係。

以本範例來說,Parent View就是rootView, 而child view就是 upLayout, mMiddleLeftLayout, mMiddleRightLayout, mDownLayout。
各元件的相依性指的是以藍色區塊來說,它必須是在黑色區塊下方,且貼齊左邊,code如下

private void initMiddleLeftLayout()
{
        ...
      layoutParamsOfMiddleLeftLayout.addRule(RelativeLayout.BELOW, upLayout.getId());
      layoutParamsOfMiddleLeftLayout.addRule(RelativeLayout.ALIGN_LEFT, upLayout.getId());
        ...
}

也就是說藍色區塊在位置的相依性上必須依賴黑色區塊,沒有黑色區塊,藍色區塊就不知道自己的位置在哪。
然而位置的相依性與被加入到rootView中的順序沒有關係。對rootView而言可以先加入藍色區塊再加入黑色區塊,如下

public RelativeLayout createLayout()
{
        ...
        rootlayout.addView(mMiddleLeftLayout);
        rootlayout.addView(upLayout);
        ...
}

 

3.調整UI Widget的重點在於LayoutParams。

以initMiddleLeftLayout方法來說,
第4~5行初始化layoutParamsofMiddleLeftLayout可設定其元件所佔的width, height。
第6~7行則是指定該元件的位置屬性。

private void initMiddleLeftLayout()
{
        ...
        RelativeLayout.LayoutParams layoutParamsOfMiddleLeftLayout = new RelativeLayout.LayoutParams(
                mDisplay.getWidth() / 2, mDisplay.getHeight() * 2 / 5);
        layoutParamsOfMiddleLeftLayout.addRule(RelativeLayout.BELOW, upLayout.getId());
        layoutParamsOfMiddleLeftLayout.addRule(RelativeLayout.ALIGN_LEFT, upLayout.getId());
        mMiddleLeftLayout.setLayoutParams(layoutParamsOfMiddleLeftLayout);
        ...
}

 

分類
Android

Android Error : The specified child already has a parent. You must call removeView() on the child's parent first.

Description:

當一個child view已經被加到A parent view,若有另一個B parent view再對該child view進行add view的動作,即會產生該Error。
簡單的說child view只能屬於一個parent view。
 

Solution:

在child view加入B parent view之前,A parent view 必須先移除child view。
 

Example:

最外層的RelativeLayout為了方便操縱,給予id為root_view,內層的text_view即為child view的角色。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
</RelativeLayout>

 
在第17行已經設定完UI Layout,也就是說根據xml的內容,mTextView已經被加到mRootParentView中。
第24行有另一個Parent View對mTextView進行addView的動作,若沒有在第21行 remove child view,即會產生錯誤。

package com.foxx.errortest;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class MainActivity extends ActionBarActivity
{
    private TextView mTextView;
    private RelativeLayout mRootParentView;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRootParentView = (RelativeLayout) findViewById(R.id.root_view);
        mTextView = (TextView) findViewById(R.id.text_view);
        mRootParentView.removeView(mTextView);
        RelativeLayout anotherParentLayout = new RelativeLayout(this);
        anotherParentLayout.addView(mTextView);
    }
}

 
 

分類
Android Uncategorized

Android Handler Thread

相較於實作基本的訊息傳送機制(Handler + Looper + Message) Android 有另一個已經幫我們完成 Looper 連結的 Thread,名稱為 Handler Thread
Handler Thread 其實相當簡單,source code 也僅150行,如下

/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package android.os;
/**
 * Handy class for starting a new thread that has a looper. The looper can then be
 * used to create handler classes. Note that start() must still be called.
 */
public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    /**
     * Constructs a HandlerThread.
     * @param name
     * @param priority The priority to run the thread at. The value supplied must be from
     * {@link android.os.Process} and not from java.lang.Thread.
     */
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
    /**
     * Call back method that can be explicitly overridden if needed to execute some
     * setup before Looper loops.
     */
    protected void onLooperPrepared() {
    }
    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
    /**
     * This method returns the Looper associated with this thread. If this thread not been started
     * or for any reason is isAlive() returns false, this method will return null. If this thread
     * has been started, this method will block until the looper has been initialized.
     * @return The looper.
     */
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
    /**
     * Quits the handler thread's looper.
     * <p>
     * Causes the handler thread's looper to terminate without processing any
     * more messages in the message queue.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p class="note">
     * Using this method may be unsafe because some messages may not be delivered
     * before the looper terminates.  Consider using {@link #quitSafely} instead to ensure
     * that all pending work is completed in an orderly manner.
     * </p>
     *
     * @return True if the looper looper has been asked to quit or false if the
     * thread had not yet started running.
     *
     * @see #quitSafely
     */
    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }
    /**
     * Quits the handler thread's looper safely.
     * <p>
     * Causes the handler thread's looper to terminate as soon as all remaining messages
     * in the message queue that are already due to be delivered have been handled.
     * Pending delayed messages with due times in the future will not be delivered.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p>
     * If the thread has not been started or has finished (that is if
     * {@link #getLooper} returns null), then false is returned.
     * Otherwise the looper is asked to quit and true is returned.
     * </p>
     *
     * @return True if the looper looper has been asked to quit or false if the
     * thread had not yet started running.
     */
    public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }
    /**
     * Returns the identifier of this thread. See Process.myTid().
     */
    public int getThreadId() {
        return mTid;
    }
}

 
在 run 方法已經幫我們做好了 Looper.prepare 以及 Looper.loop,我們只需要實作 HandlerThread 的 subclass,並複寫 onLooperPrepared() 方法,在該方法中定義初始化的工作,建構式可以輸入該 thread 名稱,方便之後除錯。
預設的 priority 為 Process.THREAD_PROCESS_DEFAULT
其他需要注意的部份為
1.與外部 Handler 的連結必須透過 getLooper 方法來完成。
2.啟動 HandlerThread 的方法還是一樣呼叫 start()。
3.可以透過呼叫 quit 以及 quitSafely 來中止 HandlerThread。
呼叫 quit 會立刻中止 looper,如果MessageQueue(MQ) 中還有訊息,這些訊息不會被處理。

4.呼叫 quitSafely 不會立刻中止 looper,looper 會處理完 Message Queue 中可以發送的訊息再停止。
如下

public class CustomizeHandlerThread extends HandlerThread
{
    private CustomizeHandler mHandler;
    public CustomizeHandlerThread(String name, int priority) {
        super(name, priority);
    }
    @Override
    protected void onLooperPrepared()
    {
        super.onLooperPrepared();
        mHandler = new CustomizeHandler(getLooper());
    }
    public Handler getHandler()
    {
        return mHandler;
    }
}

 
CustomizeHandler 則是自定義的 Handler subclass,不使用匿名內部類別的 Handler

class CustomizeHandler extends Handler
{
    private static final String TAG = "CustomizeHandler";
    public CustomizeHandler(Looper looper) {
        super(looper);
    }
    @Override
    public void handleMessage(Message msg)
    {
        super.handleMessage(msg);
        switch (msg.what) {
            case 0:
                Log.d(TAG, "in handle message what 0");
                break;
            case 1:
                Log.d(TAG, "in handle message what 1");
                break;
        }
    }
}

 
MainActivity 使用範例

package com.foxx.handlerthread;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import com.example.handlerthread.R;
public class MainActivity extends ActionBarActivity implements OnClickListener
{
    private static final String TAG = "MainActivity";
    private Button mStartThreadButton;
    private TextView mResultTextView;
    private CustomizeHandlerThread mCustomizeWorkerThread;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initUIComponents();
        startWorkerThread();
    }
    private void initUIComponents()
    {
        mStartThreadButton = (Button) findViewById(R.id.startThread);
        mStartThreadButton.setOnClickListener(this);
        mResultTextView = (TextView) findViewById(R.id.textview);
    }
    private void startWorkerThread()
    {
        mCustomizeWorkerThread = new CustomizeHandlerThread("CustomizeHandlerThread",
                Process.THREAD_PRIORITY_BACKGROUND);
        mCustomizeWorkerThread.start();
    }
    @Override
    public void onClick(View view)
    {
        int uiId = view.getId();
        switch (uiId) {
            case R.id.startThread:
                sendMessageToWorkerHandler();
                break;
        }
    }
    private void sendMessageToWorkerHandler()
    {
        Handler handler = mCustomizeWorkerThread.getHandler();
        Message message = handler.obtainMessage(0);
        handler.sendMessage(message);
    }
    public TextView getResultTextView()
    {
        return mResultTextView;
    }
    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        mCustomizeWorkerThread.getHandler().getLooper().quit();
    }
}

 
若想將結果回傳給 MainActivity,可以考慮新增建構子,將 MainActivity 傳入CustomizeHandler.java

class CustomizeHandler extends Handler
{
    ...
    private MainActivity mMainActivity;
    public CustomizeHandler(Looper looper, MainActivity activity) {
        super(looper);
        mMainActivity = activity;
    }
    @Override
    public void handleMessage(Message msg)
    {
        super.handleMessage(msg);
        switch (msg.what) {
            case 0:
                setResultForMainActivity();
                break;
            case 1:
                Log.d(TAG, "in handle message what 1");
                break;
        }
    }
    private void setResultForMainActivity()
    {
        mMainActivity.runOnUiThread(new Runnable() {
            @Override
            public void run()
            {
                mMainActivity.getResultTextView().setText("0");
            }
        });
    }
}

 
修改相對應的 CustomizeHandlerThread

public class CustomizeHandlerThread extends HandlerThread
{
    ...
    private MainActivity mMainActivity;
    public CustomizeHandlerThread(MainActivity activity, String name, int priority){
        super(name, priority);
        mMainActivity = activity;
    }
    ...
}

 
最後修改 MainActivity.java

public class MainActivity extends ActionBarActivity implements OnClickListener
{
    ...
    private void startWorkerThread()
    {
        mCustomizeWorkerThread = new CustomizeHandlerThread(this,"CustomizeHandlerThread",
                Process.THREAD_PRIORITY_BACKGROUND);
        mCustomizeWorkerThread.start();
    }
    ...
}

 
HandlerThread 只使用一個 MessageQueue,所以是執行緒安全的,適合用於循序動作。
多個任務互相依賴結合的情況也適合用於 HandlerThread,可藉由 Message.what 區分不同任務,並判斷是否需要進行下一個任務。
基本上如果任務不需要並發執行,也不需要對 Looper 作太多的控制,使用 Handler Thread會比完全實作訊息傳送機制來的簡單和方便。
 
Android AsyncTask
Android Worker Thread(Handler + Looper + Message)
 

分類
Android Uncategorized

Android Worker Thread Advanced

上篇 Android Looper + Handler + Message + MessageQueue(Worker Thread)介紹 Worker Thread (Android訊息傳送機制)
本篇會持續示範訊息傳送機制的其他進階用法,

1.傳送訊息夾帶參數

這是基本建立訊息並使用Handler傳送訊息的用法

    private void sendMessageToWorkerHandler()
    {
        Handler handler = mCustomizeWorkerThread.getHandler();
        if (handler != null) {
            Message message = handler.obtainMessage(2);
            handler.sendMessage(message);
        }
    }

傳送訊息可以夾帶參數,讓Handler在處理訊息(handleMessage方法)時,可根據參數作更多處理 e.g.,
夾帶簡單整數型態arg1和arg2

    private void sendMessageWithArgsToWorkerHandler()
    {
        Handler handler = mCustomizeWorkerThread.getHandler();
        if (handler != null) {
            Message message = Message.obtain();
            message.what = 3;
            message.arg1 = 10;
            message.arg2 = 100;
            handler.sendMessage(message);
        }
    }

另外還可以夾帶自定義類別的物件,以下為自定義類別,為了簡單示範,只持有一個String和一個Int類別成員

package com.foxx.threads;
public class CustomizeMessageObject
{
    private String mName = "";
    private int mType;
    public CustomizeMessageObject(String name, int type){
        mName = name;
        mType = type;
    }
    public String getName()
    {
        return mName;
    }
    public void setName(String name)
    {
        mName = name;
    }
    public int getType()
    {
        return mType;
    }
    public void setType(int type)
    {
        mType = type;
    }
}

夾帶自定義物件的方式

    private void sendMessageWithObjectToWorkerHandler()
    {
        Handler handler = mCustomizeWorkerThread.getHandler();
        if (handler != null) {
            Message message = Message.obtain();
            message.what = 4;
            message.obj = new CustomizeMessageObject("Foxx", 123);
            handler.sendMessage(message);
        }
    }

修改在Activity發送訊息的部份,點擊按鈕會依序發送3個訊息

    @Override
    public void onClick(View view)
    {
        int uiId = view.getId();
        switch (uiId) {
            case R.id.startThread:
                sendMessageToWorkerHandler();
                sendMessageWithArgsToWorkerHandler();
                sendMessageWithObjectToWorkerHandler();
                break;
        }
    }

修改CustomizeWorkerHandler處理訊息的部份,針對message.what的數值作判斷

    public void handleMessage(Message message)
    {
        switch (message.what) {
            case 2:
                String result = HttpPageData.getPageData("https://www.google.com.tw/");
                setResultToMainActivity(result);
                break;
            case 3:
                Log.d(TAG, "message.arg1:" + message.arg1);
                Log.d(TAG, "message.arg2:" + message.arg2);
                break;
            case 4:
                CustomizeMessageObject messageObject = (CustomizeMessageObject) message.obj;
                Log.d(TAG, "MessageObject.name:" + messageObject.getName());
                Log.d(TAG, "MessageObject.type:" + messageObject.getType());
                break;
        }
    }

以下是Logcat的輸出

06-12 11:34:01.463: D/WorkerHandler(16289): message.arg1:10
06-12 11:34:01.463: D/WorkerHandler(16289): message.arg2:100
06-12 11:34:01.463: D/WorkerHandler(16289): MessageObject.name:Foxx
06-12 11:34:01.463: D/WorkerHandler(16289): MessageObject.type:123

 

2.提醒使用者訊息處理

假設處理訊息時並沒有提醒使用者,會讓使用者誤判點擊按鍵卻沒有反應造成使用者多次點擊,為了避免這種情況,最好在送出訊息時出現提示。
提示可由Dialog或是ProgressDialog組成。
首先在MainActivity加入並建立提示元件,提供外部存取的方法

public class MainActivity extends ActionBarActivity implements OnClickListener
{
    ...
    private AlertDialog mProcessingMessageDialog;
    private void initUIComponents()
    {
        initProcessingMessageDialog();
        ...
    }
    private void initProcessingMessageDialog()
    {
        mProcessingMessageDialog = new AlertDialog.Builder(this).create();
        mProcessingMessageDialog.setTitle("Processing...");
        mProcessingMessageDialog.setCanceledOnTouchOutside(false);
    }
    public AlertDialog getProcessingMessageDialog(){
        return mProcessingMessageDialog;
    }

接著在CustomizeWorkerHandler加入操作和呼叫點
第5行當該Handler收到從Looper傳送過來的訊息,立即顯示ProcessingMessageDialog
第21行當該Handler處理完Message,關閉ProcessingMessageDialog

class CustomizeWorkerHandler extends Handler
{
    public void handleMessage(Message message)
    {
        showProcessingDialog();
        switch (message.what) {
            case 2:
                String result = HttpPageData.getPageData("https://www.google.com.tw/");
                setResultToMainActivity(result);
                break;
            case 3:
                Log.d(TAG, "message.arg1:" + message.arg1);
                Log.d(TAG, "message.arg2:" + message.arg2);
                break;
            case 4:
                CustomizeMessageObject messageObject = (CustomizeMessageObject) message.obj;
                Log.d(TAG, "MessageObject.name:" + messageObject.getName());
                Log.d(TAG, "MessageObject.type:" + messageObject.getType());
                break;
        }
        closeProcessingDialog();
    }
    private void showProcessingDialog()
    {
        mMainActivity.runOnUiThread(new Runnable() {
            @Override
            public void run()
            {
                mMainActivity.getProcessingMessageDialog().show();
            }
        });
    }
    private void closeProcessingDialog()
    {
        mMainActivity.runOnUiThread(new Runnable() {
            @Override
            public void run()
            {
                mMainActivity.getProcessingMessageDialog().dismiss();
            }
        });
    }
}

 

3.雙重Handler溝通

CustomizeWorkerHandler 的 showProcessingDialog 和 closeProcessingDialog方法其實是為了調用UI Thread的UI元件。
如果把處理UI Thread的內容集中寫在CustomizeWorkerHandler,會調用多次的 mMainActivity.runOnUiThread方法,
導致CustomizeWorkerHandler類別的冗長,其實可以在UI Thread中建立另一個UI Handler,當CustomizeWorkerHandler有結果產生時
藉由UI Handler發送訊息到UI Thread,訊息包含處理完的結果,把結果的處理轉回到 UI Thread中。
先新增UIHandler類別並建立實體,提供存取方法,把結果的處理放在handleMessage方法中,順便修正Magic Number

public class MainActivity extends ActionBarActivity implements OnClickListener
{
    ...
    public static final int GET_PAGE_DATA = 0;
    public static final int SEND_MESSAGE_ARGS = 1;
    public static final int SEND_MESSAGE_OBJECT = 2;
    private UIHandler mHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        ...
        initUIHandler();
        ...
    }
    private void initUIHandler()
    {
        mHandler = new UIHandler();
    }
    public UIHandler getHandler()
    {
        return mHandler;
    }
    class UIHandler extends Handler
    {
        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what) {
                case GET_PAGE_DATA:
                    String result = (String) msg.obj;
                    mResultTextView.setText(result);
                    break;
                case SEND_MESSAGE_ARGS:
                    mResultTextView.setText(""+msg.arg1+msg.arg2);
                    break;
                case SEND_MESSAGE_OBJECT:
                    CustomizeMessageObject messageObject = (CustomizeMessageObject) msg.obj;
                    mResultTextView.setText(""+messageObject.getName()+messageObject.getType());
                    break;
            }
            mProcessingMessageDialog.dismiss();
        }
    }

修改CustomizeWorkerHandler在handlMessage方法取得UI Handler,並使用UI Handler發送包含結果的訊息,

class CustomizeWorkerHandler extends Handler
{
    ...
    @Override
    public void handleMessage(Message message)
    {
        Message messageToUIHandler = mMainActivity.getHandler().obtainMessage(message.what);
        switch (message.what) {
            case MainActivity.GET_PAGE_DATA:
                messageToUIHandler.obj = HttpPageData.getPageData("https://www.google.com.tw/");
                break;
            case MainActivity.SEND_MESSAGE_ARGS:
                messageToUIHandler.arg1 = message.arg1;
                messageToUIHandler.arg2 = message.arg2;
                break;
            case MainActivity.SEND_MESSAGE_OBJECT:
                CustomizeMessageObject messageObject = (CustomizeMessageObject) message.obj;
                messageToUIHandler.obj = messageObject;
                break;
        }
        mMainActivity.getHandler().sendMessage(messageToUIHandler);
    }
}

如此就能在UI Thread中處理結果。
 

分類
Android Uncategorized

Android Looper + Handler + Message + MessageQueue

本篇紀錄如何在Android中使用 Handler + Looper + Message + Queue。
為了讓App保持快速的回應性(避免產生ANR),長期任務不可執行在 UI thread上,這些長期任務包括,網路存取,讀寫檔案,資料庫存取,圖形處理等等。
在Android中使用Thread和在一般的Java中使用方式相當類似,但有一些同步化問題必須去解決,如 Race Condition, Blocking等等
為了方便解決這些問題,Android定義訊息傳送機制來確保訊息傳送的正確性以及安全性,Android訊息傳送機制包含4個角色,各為Looper,Handler,Message,MessageQueue。
 
Message :
訊息,由一組描述和一些資料物件所組成,可藉由Handler傳送到MessageQueue中,並由Looper取出再回傳給同一個Handler,當需要傳遞參數時,可夾帶在Message中。
建立Message的方式盡量使用Message.obtain(), 或是Handler.obtainMessage(),因為訊息會被放到緩衝池以重複使用。
 
MessageQueue :
訊息佇列,透過Looper建立,內容則是訊息組成,結構為無限制的連結串列,一個Looper只能有一個MessageQueue,
訊息會藉由Handler插入,並由Looper取出傳送到相對應的Handler。
 
Looper :
管理MessageQueue,會將存放於MessageQueue中的訊息依序送出到Handler上,讓Handler作進一步的處理,
Handler必須和Looper連結,否則無法傳送或接收訊息,而Thread也必須和Looper連結,才能讓MessageQueue和Thread相關連。
簡單的說Looper是Thread和MessageQueue的協調者。一個Thread只能有一個Looper。
 
Handler :
訊息處理者,在Android訊息傳送機制中,Handler是最常使用的類別,Handler可建立訊息,插入訊息到訊息佇列中,處理訊息。
一個Looper可以有多個Handler,Looper會自動將Message傳送給對應的Handler。
 
藉由以下的範例來說明彼此之間的互動,範例會盡量少用匿名內部類別來實作,而是把各個元件拆開為單一類別,比較容易了解
範例只有一個button和一個textview組成,當點擊button時,會去擷取 https://www.google.com.tw/ 的網頁資料
並顯示到textview上。
 
首先是 MainActivity,因為網路存取是長期任務,且android在Honeycomb SDK新增了NetworkOnMainThreadException,
若在Main thread(UI thread)進行網路操作,會丟出Exception。所以把網路操作放到CustomizeWorkerThread中執行,
第18行建立 mCustomizeWorkerThread 物件
第38行建立CustomizeWorkerThread物件並連結到參考,傳入this是為了把取得的網頁資料設定回MainActivity的textview
第39行呼叫start()方法以啟動mCustomizeWorkerThread的run方法。
第48行當按鈕按下時,呼叫sendMessageToWorkerHandler方法,該方法會建立message插入到MessageQueue
第55行取得mCustomizeWorkerThread的Handler,
第56行建立Message物件,其中數字2為Message的what欄位數值,當Handler處理Message時可以依此數值判斷行為。
第57行利用Handler將Message物件插入到MessageQueue中
第69行當activity結束時也告知Looper停止處理訊息

package com.foxx.threads;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends ActionBarActivity implements OnClickListener
{
    private static final String TAG = "MainActivity";
    private Button mStartThreadButton;
    private TextView mResultTextView;
    private CustomizeWorkerThread mCustomizeWorkerThread;
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initUIComponents();
        startWorkerThread();
    }
    private void initUIComponents()
    {
        mStartThreadButton = (Button) findViewById(R.id.startThread);
        mStartThreadButton.setOnClickListener(this);
        mResultTextView = (TextView) findViewById(R.id.textview);
    }
    private void startWorkerThread()
    {
        mCustomizeWorkerThread = new CustomizeWorkerThread(this);
        mCustomizeWorkerThread.start();
    }
    @Override
    public void onClick(View view)
    {
        int uiId = view.getId();
        switch (uiId) {
            case R.id.startThread:
                sendMessageToWorkerHandler();
                break;
        }
    }
    private void sendMessageToWorkerHandler()
    {
        Handler handler = mCustomizeWorkerThread.getHandler();
        if(handler != null){
            Message message = handler.obtainMessage(2);
            handler.sendMessage(message);
        }
    }
    public TextView getResultTextView()
    {
        return mResultTextView;
    }
    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        mCustomizeWorkerThread.getHandler().getLooper().quit();
    }
}

 
以下為CustomizeWorkerThread的內容,
第10行為了取代匿名內部類別,另外建立了CustomizeWorkerHandler,該Handler負責處理從Looper傳送過來的Message
第18行的run方法相當重要,在run方法中連結了Looper,Thread,Handler三者。
第20行的Looper.prepare方法會建立Looper以及在該Looper中的MessageQueue,並連結到當前的Thread(CustomizeWorkerThread)
第21行建立CustomizeWorkerHandler物件並連結Looper
第22行Looper開始循序處理MessageQueue,為無限迴圈。當Message插入到MessageQueue中,Looper就會擷取該Message並發送到相關的Handler。
第25行回傳Handler讓外部可以藉由取得的Handler插入訊息
第31行即為CustomizeWorkerHandler,繼承 Handler
第41行必須複寫HandleMessage方法,當Looper傳送Message時,該方法即會被呼叫(callback)
第43行根據Message的what數值判斷行為
第45行就是網路存取的行為,HttpPageData.getPageData 會回傳google的網頁資料
第46行把取得的網頁資料設定到MainActivity的textview中

package com.foxx.threads;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
public class CustomizeWorkerThread extends Thread
{
    private static final String TAG = "WorkerThread";
    private CustomizeWorkerHandler mHandler;
    private MainActivity mMainActivity;
    public CustomizeWorkerThread(MainActivity activity) {
        mMainActivity = activity;
    }
    @Override
    public void run()
    {
        Looper.prepare();
        mHandler = new CustomizeWorkerHandler(mMainActivity);
        Looper.loop();
    }
    public CustomizeWorkerHandler getHandler()
    {
        return mHandler;
    }
}
class CustomizeWorkerHandler extends Handler
{
    private static final String TAG = "WorkerHandler";
    private MainActivity mMainActivity;
    public CustomizeWorkerHandler(MainActivity activity) {
        mMainActivity = activity;
    }
    public void handleMessage(Message message)
    {
        switch (message.what) {
            case 2:
                String result = HttpPageData.getPageData("https://www.google.com.tw/");
                setResultToMainActivity(result);
                break;
        }
    }
    private void setResultToMainActivity(final String result)
    {
        mMainActivity.runOnUiThread(new Runnable() {
            @Override
            public void run()
            {
                mMainActivity.getResultTextView().setText(result);
            }
        });
    }
}

 
接著簡單描述整個訊息傳送機制的互動過程,

建立訊息傳送機制

在 Activity 中會持有CustomWorkThread的類別成員(mCustomizeWorkerThread),
並在onCreate方法中建立實體,然後呼叫start方法,start方法會啟動CustomizeWorkerThread中的run方法,
該run方法會將worker thread,Looper, Handler 三者連結起來,到目前為止是整個傳遞訊息機制的建立,

發送訊息

在Activity中為了發送訊息到MessageQueue中,必須先建立訊息再藉由取得mCustomerWorkThread的Handler將訊息發送到MessageQueue中。

處理訊息

當訊息已經被發送到MessageQueue中,Looper便會自動擷取訊息,並將訊息發送到相對應的Handler,
相對應的意思為哪個Handler發送訊息,哪個Handler就會接收到訊息。
Handler藉由handleMessage方法來處理訊息。


以下為HttpPageData.java

package com.foxx.threads;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
public class HttpPageData
{
    /**
     * 取得網頁資料
     * @return : 回傳string型態的網頁資料
     */
    public static String getPageData(String httpUrl){
        java.net.CookieManager cm = new java.net.CookieManager();
        java.net.CookieHandler.setDefault(cm);
        URL u = null;
        InputStream in = null;
        InputStreamReader r = null;
        BufferedReader br = null;
        StringBuffer message = null;
        try {
           u = new URL(httpUrl);
           in = u.openStream();
           r = new InputStreamReader(in, "BIG5");//UTF-8
           br = new BufferedReader(r);
           String tempstr = null;
           message = new StringBuffer();
           while ((tempstr = br.readLine()) != null) {
               message.append(tempstr);
           }
        } catch (Exception e) {
           e.getStackTrace();
           System.out.println(e.getMessage());
        } finally {
           try {
              u = null;
              in.close();
              r.close();
              br.close();
           } catch (Exception e) {
           }
    }
        return message.toString();
    }
}

 

分類
Refactoring

Example: Replace Parameter with explicit methods(使用明確的方法取代參數)

當長if-else或是switch出現在方法內時通常代表該方法做了一件以上的事。

 
為了保持方法簡短和清晰度,適當的情況下,我們應該盡量避免長if-else或switch的出現。
如果switch或是if-else的判斷條件(參數)是可以掌控的,那麼可以使用 Replace Parameter with explicit methods 來消除 switch 或是 if-else,
 
toggleUI方法相當簡單,根據state的值判斷進入哪個條件。e.g.,

    private void toggleUI(UIState state, int labelTextId)
    {
        if (state == UIState.LOAD_PANEL) {
            progressBar.setVisibility(View.VISIBLE);
            languageSpinner.setVisibility(View.GONE);
        } else if (state == UIState.LANGUAGE_FOUND) {
            progressBar.setVisibility(View.GONE);
            languageSpinner.setVisibility(View.VISIBLE);
        }
        label.setText(labelTextId);
    }

 
觀察所有toggleUI的呼叫點會發現每個呼叫點傳入的參數都是可以控制的。e.g.,

    private void setUI()
    {
        ...
        toggleUI(UIState.LOAD_PANEL, R.string.label_loading);
    }
    void onReadyPanelDialog(AlertDialog dialog)
    {
         ...
         toggleUI(UIState.LANGUAGE_FOUND, R.string.label_language);
         ...
    }

 
因此我們就能夠把 toggleUI 拆開,分為2個方法 ,e.g.,

    private void toggleUIForLoadPanel(int labelTextId){
        progressBar.setVisibility(View.VISIBLE);
        languageSpinner.setVisibility(View.GONE);
        label.setText(labelTextId);
    }
    private void toggleUIForLanguageFound(int labelTextId){
        progressBar.setVisibility(View.GONE);
        languageSpinner.setVisibility(View.VISIBLE);
        label.setText(labelTextId);
    }

 
建立toggleUIForLoadPanel,toggleUIForLanguageFound 後取代的原本 toggleUI 呼叫點,

    private void setUI()
    {
        ...
        toggleUIForLoadPanel(R.string.label_loading);
    }
    void onReadyPanelDialog(AlertDialog dialog)
    {
         ...
         toggleUIForLanguageFound(R.string.label_language);
         ...
    }

 
再觀察 toggleUIForLoadPanel 和 toggleUIForLanguageFound 的唯一參數(int labelTextId)
發現該參數傳入的數值會跟著方法一致,
toggleUIForLoadPanel 傳入的是 R.string.label_loading
toggleUIForLanguageFound 傳入的是 R.string.label_language
因此可把參數直接刪除,在方法中直接指定即可,e.g.,

    private void toggleUIForLoadPanel(){
        progressBar.setVisibility(View.VISIBLE);
        languageSpinner.setVisibility(View.GONE);
        label.setText(R.string.label_loading);
    }
    private void toggleUIForLanguageFound(){
        progressBar.setVisibility(View.GONE);
        languageSpinner.setVisibility(View.VISIBLE);
        label.setText(R.string.label_language);
    }

 
最後刪除掉 toggleUI 方法。
把原本需要2個參數的toggleUI方法重構為2個不需要參數的方法,也讓方法的命名轉為更容易了解( toggleUIForLanguageFound , toggleUIForLOadPanel )
在重構結束後還會發現一些額外的驚喜,原本的 toggleUI 使用 UIState 列舉,而該列舉只使用在 toggleUI 內,隨著 toggleUI 被刪除,UIState 也不需要使用了。
 
 

分類
Android

Create Test Case for android project(Step by Step)

官網教學,參考 https://developer.android.com/training/testing.html
這裡為紀錄詳細步驟。
Target Class : RoundAmountSaving也就是要測試的Android app(被測目標),該app能幫你計算本金和,你只需要輸入目前儲蓄金額,年利率,以及儲蓄月數
它就能算出儲蓄多久可得到多少錢。
裡面的元件組成相當簡單,只有幾個EditText,Button,一個簡單的計算公式。

package com.foxx.round_amount_saving;
import com.foxx_round_amount_saving.R;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class RoundAmountSaving extends Activity
{
    private static final int PERCENT = 100;
    private static final int MONTH_COUNT = 12;
    private EditText mInputLend;
    private EditText mInputRate;
    private EditText mInputNumber;
    private Button mSubmit;
    private TextView mAmount;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initLayoutComponents();
    }
    private void initLayoutComponents()
    {
        mInputLend = (EditText) findViewById(R.id.input_lend);
        mInputNumber = (EditText) findViewById(R.id.input_number);
        mInputRate = (EditText) findViewById(R.id.input_rate);
        mSubmit = (Button) findViewById(R.id.submit);
        mSubmit.setOnClickListener(getClickListenerForSubmit());
        mAmount = (TextView) findViewById(R.id.amount);
    }
    private OnClickListener getClickListenerForSubmit()
    {
        return new OnClickListener() {
            @Override
            public void onClick(View v)
            {
                mAmount.setText("" + countAmount());
            }
        };
    }
    private double countAmount()
    {
        int lend = Integer.parseInt(mInputLend.getText().toString());
        double rateOfMonth = Double.parseDouble(mInputRate.getText().toString()) / MONTH_COUNT
                / PERCENT;
        int number = Integer.parseInt(mInputNumber.getText().toString());
        return lend * (Math.pow(rateOfMonth + 1, number));
    }
}

 
開始建立Android Test Project

1. Eclipse -> File -> New -> Other -> type : android test project -> next

 

2. type : RoundAmountSavingTest in project name -> next

 

3. select test target : RoundAmountSaving -> Finish

Note : RoundAmountSaving 是 project Name
完成後會產生android test project,名稱為 RoundAmountSavingTest,其中有設定好的package,接著開始來寫 test case
 

4.建立 junit test case

Eclipse -> File -> New -> Other -> type : junit test case -> next
 

5. 選擇 New Junit 3 test,輸入Name,並選擇Class under test : com.foxx.roundamountsaving.RoundAmountSaving -> Finish

 

6.修改RoundAmountSavingTest.java

由於RoundAmountSaving繼承Activity,為了測試Activity我們必須讓Test Case繼承ActivityInstrumentationTestCase2並呼叫父類別建構式

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import com.foxx.roundamountsaving.RoundAmountSaving;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
}

 
接著override setUp方法,並在方法中設定相關內容,包含測試類別的實體

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import com.foxx.roundamountsaving.RoundAmountSaving;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    private RoundAmountSaving mRoundAmountSaving;
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mRoundAmountSaving = getActivity();
    }
}

 
可以開始寫測試方法了,為了確保測試資料的完整性,先測試mRoundAmountSaving不可為null

public void testRoundAmountSavingNotNull(){
        assertNotNull(mRoundAmountSaving);
}

 
Run test!!!
加入RoundAmountSaving上的UI元件,測試這些元件的layout或是功能性,
預設情況在38~42行會出現 id cannot be resolved or is not a field 的錯誤,Eclipse 並不會自動修復,我們必須手動import RoundAmountSaving的R(第11行)

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.foxx.roundamountsaving.RoundAmountSaving;
import com.foxx.roundamountsaving.R;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    private RoundAmountSaving mRoundAmountSaving;
    private EditText mInputLend;
    private EditText mInputRate;
    private EditText mInputNumber;
    private Button mSubmit;
    private TextView mAmount;
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mRoundAmountSaving = getActivity();
        initLayoutComponents();
    }
    public void testRoundAmountSavingNotNull(){
        assertNotNull(mRoundAmountSaving);
    }
    private void initLayoutComponents()
    {
        mInputLend = (EditText) mRoundAmountSaving.findViewById(R.id.input_lend);
        mInputNumber = (EditText) mRoundAmountSaving.findViewById(R.id.input_number);
        mInputRate = (EditText) mRoundAmountSaving.findViewById(R.id.input_rate);
        mSubmit = (Button) mRoundAmountSaving.findViewById(R.id.submit);
        mAmount = (TextView) mRoundAmountSaving.findViewById(R.id.amount);
    }
}

 
加入UI元件不可為null的測試

public void testLayoutComponentsNotNull()
{
        assertNotNull(mInputLend);
        assertNotNull(mInputNumber);
        assertNotNull(mInputRate);
        assertNotNull(mSubmit);
}

 
Run test~
加入測試UI元件layout的測試,assertEquals的第2個參數必須參考RoundAmountSaving的UI xml的設定

public void testAmountLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mAmount.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.WRAP_CONTENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputNumberLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputNumber.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputRateLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputRate.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputLendLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputLend.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testSubmitLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mSubmit.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }

 
Run test~
為了測試UI元件的功能性,加入requestFocusLayoutComponent和setValueToLayoutComponent2個方法,
requestFocusLayoutComponent可讓UI元件取得焦點,取得焦點後再呼叫setValueToLayoutComponent這個方法可以在edittext中設定數值

private void requestFocusLayoutComponent(final View component)
    {
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run()
            {
                component.requestFocus();
            }
        });
    }
private void setValueToLayoutComponent(String value)
    {
        getInstrumentation().waitForIdleSync();
        getInstrumentation().sendStringSync(value);
        getInstrumentation().waitForIdleSync();
    }

 
設定數值到各UI元件

private void inputTestValueInInputNumber()
    {
        requestFocusLayoutComponent(mInputNumber);
        setValueToLayoutComponent("20");
    }
    private void inputTestValueInInputRate()
    {
        requestFocusLayoutComponent(mInputRate);
        setValueToLayoutComponent("10");
    }
    private void inputTestValueInInputLend()
    {
        requestFocusLayoutComponent(mInputLend);
        setValueToLayoutComponent("100");
    }

 
 
最後加入測試計算的正確性,expectedCountAmountResult 為預期的計算結果,先設定數值(inutTestValueInInputXXX)到各UI元件中
再呼叫TouchUtils.clickView模擬mSubmit被按下,最後從mAmount取得計算結果和expectedCountAmountResult比較是否相等
相等的話代表算法正確

public void testCountAmount()
    {
        final double expectedCountAmountResult = 118.05448347663095;
        inputTestValueInInputLend();
        inputTestValueInInputRate();
        inputTestValueInInputNumber();
        TouchUtils.clickView(this, mSubmit);
        String result = (String) mAmount.getText();
        assertEquals(result, "" + expectedCountAmountResult);
    }

 
Run test~
其實在測試算法還可以加入更多的測試條件,如邊界值,特殊值等等
final RoundAmountSavingTest.java

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import android.test.TouchUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.foxx.roundamountsaving.RoundAmountSaving;
import com.foxx.roundamountsaving.R;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    private RoundAmountSaving mRoundAmountSaving;
    private EditText mInputLend;
    private EditText mInputRate;
    private EditText mInputNumber;
    private Button mSubmit;
    private TextView mAmount;
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mRoundAmountSaving = getActivity();
        initLayoutComponents();
    }
    public void testRoundAmountSavingNotNull(){
        assertNotNull(mRoundAmountSaving);
    }
    public void testLayoutComponentsNotNull()
    {
        assertNotNull(mInputLend);
        assertNotNull(mInputNumber);
        assertNotNull(mInputRate);
        assertNotNull(mSubmit);
    }
    private void initLayoutComponents()
    {
        mInputLend = (EditText) mRoundAmountSaving.findViewById(R.id.input_lend);
        mInputNumber = (EditText) mRoundAmountSaving.findViewById(R.id.input_number);
        mInputRate = (EditText) mRoundAmountSaving.findViewById(R.id.input_rate);
        mSubmit = (Button) mRoundAmountSaving.findViewById(R.id.submit);
        mAmount = (TextView) mRoundAmountSaving.findViewById(R.id.amount);
    }
    private void requestFocusLayoutComponent(final View component)
    {
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run()
            {
                component.requestFocus();
            }
        });
    }
    private void setValueToLayoutComponent(String value)
    {
        getInstrumentation().waitForIdleSync();
        getInstrumentation().sendStringSync(value);
        getInstrumentation().waitForIdleSync();
    }
    public void testCountAmount()
    {
        final double expectedCountAmountResult = 118.05448347663095;
        inputTestValueInInputLend();
        inputTestValueInInputRate();
        inputTestValueInInputNumber();
        TouchUtils.clickView(this, mSubmit);
        String result = (String) mAmount.getText();
        assertEquals(result, "" + expectedCountAmountResult);
    }
    private void inputTestValueInInputNumber()
    {
        requestFocusLayoutComponent(mInputNumber);
        setValueToLayoutComponent("20");
    }
    private void inputTestValueInInputRate()
    {
        requestFocusLayoutComponent(mInputRate);
        setValueToLayoutComponent("10");
    }
    private void inputTestValueInInputLend()
    {
        requestFocusLayoutComponent(mInputLend);
        setValueToLayoutComponent("100");
    }
    public void testAmountLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mAmount.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.WRAP_CONTENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputNumberLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputNumber.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputRateLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputRate.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputLendLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputLend.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testSubmitLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mSubmit.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
}
分類
Design Pattern

使用 Adapter 封裝未完成類別(unfinished class)

Adapter Pattern

將類別的介面轉換成另一個介面,使得原本不相容的類別可以互相一起工作。

使用的情況多半是在現有的介面無法滿足需求,必須轉接物件來完成功能,其實還可以用在封裝 unfinished class。
 
Description:
你需要未完成類別(unfinished class)的功能,由於該類別還未完成,你只能先建立dummy class 來滿足需求,可使用Adapter來封裝dummy class,
讓被影響的部份降至最低。
假設有個Person類別,該類別是由其他人來實作,但由於一些狀況目前還無法完成Person類別,不過現在你就需要從Person取得String name和int age,
首先建立DummyPerson,其中只實作需要的方法,如下

package com.foxx.person.adapter;
public class DummyPerson
{
    public String getName(){
        return "John";
    }
    public int getAge(){
        return 20;
    }
}

如果不用Adapter來封裝DummyPerson,也可以直接使用DummyPerson,但有個很大的缺點,
當你在10個位置使用DummyPerson,也代表將來Person完成後,你必須在這10個位置更換Person來取代DummyPerson,
需要修改的位置越多,出錯的機率也越大。使用Adapter來封裝DummyPerson就可限制修改的範圍只在Adapter中。
 
建立PersonAdapter類別

package com.foxx.person.adapter;
public class PersonAdapter
{
    private DummyPerson mPerson = new DummyPerson();
    public String getName(){
        return mPerson.getName();
    }
    public int getAge(){
        return mPerson.getAge();
    }
}

其中會有DummyPerson型態的類別成員,而getName以及getAge方法則委託給DummyPerson來完成
 
Client端呼叫

package com.foxx.person.adapter;
public class Client
{
    public void showPersonData(){
        PersonAdapter personAdapter = new PersonAdapter();
        System.out.println(personAdapter.getName());
        System.out.println(personAdapter.getAge());
    }
}

可以看到這裡只呼叫PersonAdapter的方法,Client並不知道回傳值是誰提供的。在Person類別未完成之前,都使用PersonAdapter來提供需要的功能
 
OK~ 現在Person類別完成了,如下

package com.foxx.person.adapter;
public class Person
{
    private String mName;
    private int mAge;
    public Person(String name, int age) {
        mName = name;
        mAge = age;
    }
    public String getName()
    {
        return mName;
    }
    public int getAge()
    {
        return mAge;
    }
}

 
現在只要修改PersonAdapter即可,不必更動到客戶端。

package com.foxx.person.adapter;
public class PersonAdapter
{
    private Person mPerson;
    public PersonAdapter(){
        mPerson = new Person("John",20);
    }
    public String getName(){
        return mPerson.getName();
    }
    public int getAge(){
        return mPerson.getAge();
    }
}

 
 
 
 
 

分類
Photograph

Shot For People Walking On Clean Modern Street

It’s no secret that the digital industry is booming. From exciting startups to global brands, companies are reaching out to digital agencies, responding to the new possibilities available. However, the industry is fast becoming overcrowded, heaving with agencies offering similar services — on the surface, at least.
Producing creative, fresh projects is the key to standing out. Unique side projects are the best place to innovate, but balancing commercially and creatively lucrative work is tricky. So, this article looks at how to make side projects work and why they’re worthwhile, drawing on lessons learned from our development of the ux ompanion app.

Why Integrate Side Projects?

Being creative within the constraints of client briefs, budgets and timelines is the norm for most agencies. However, investing in research and development as a true, creative outlet is a powerful addition. In these side projects alone, your team members can pool their expertise to create and shape their own vision — a powerful way to develop motivation, interdisciplinary skills and close relationships.

People think focus means saying yes to the thing you’ve got to focus on. But that’s not what it means at all.

Building into the identity and culture of an agency can also lead to new client work. These projects act as a road map, showing clients exciting new technologies and ideas that will differentiate you from competitors. One of our earliest projects turned our website into a brochure, optimized for the first iPad’s touch interactions. By demonstrating the final product, we went on to win a project to create a similar product for a new client.

How To Make Side Projects Work

sketchWe’re still working on achieving that perfect balance between commerce and creativity. But we have fresh inspiration on how it’s done from having worked on ux companion. The app gained a popular following in early October, as one of the first native apps to offer a full glossary of user experience terms and theory — but the development process was definitely a learning process.
Commercializing side projects alongside client work isn’t easy. Even if such projects are intended to generate additional revenue streams, they are not directly related to your core business. Those with a more qualitative aim, such as promoting expertise or technological experimentation, are even harder to justify.
A significant shift in mindset is required to support either type of side project — weighing the longer-term, incremental benefits against committing what would otherwise be immediately billable time. Many agencies do this with a time-bound model of 80% client time versus 20% time, inspired by Google’s successes with Gmail and Google Reader  which they have since (tellingly) phased out. I’d instead recommend the following guidelines.