2015年2月19日木曜日

Androidの通信処理はAsyncTaskで処理すると楽

前回アニメ番組表APIの通信処理(http://tommyproguram.blogspot.jp/2015/02/apiandroid.html)で、Thread, Hadlerを使った処理を行ったが、UIスレッド、通信スレッドの行き来を意識するのが面倒だった。
なので、今回はそれを簡単に実装できるAsyncTaskクラスを使って通信処理を書き直してみる。

AsyncTaskとは

「エイシンクタスク」って読むらしい。 このクラスはexecute()メソッドを実行することで、下記の順でメソッドが呼ばれる。メソッドごとに実行スレッドが違うのが特徴

メソッド名意味使用例
onPreExecute非同期処理する前に何かする場合に使用ダイアログで通信中を表示
doInBackground非同期処理通信処理、DB処理、データ読み込み
onProgressUpdate非同期処理中にUI更新する時使用
publishProgress()でこの処理を実行
%とかバーで処理状態表示
onPostExecute非同期処理で得られた結果を元にUI更新ダイアログ消去
通信処理結果表示

使用上の注意

めっちゃ便利そうだけど、何でもかんでもAsyncTaskで平行処理するのはよくない
UIが絡むとそれだけコストがかかる。このクラスは必ずUIスレッドを経由するため、UIスレッドを絡めない処理は無難にThreadクラスで処理した方が良い
UI関係の起こりやすく、発見しにくかったバグを2つ紹介する
  • AsyncTaskを複数使用するときの処理
    • Android2.3以前
    • execute()メソッドを実行する度に並列処理されている
    • Android3.0以降
    • execute()メソッドを実行すると現在実行中のexecute()が終了してから処理が実行される
    この違いを意識しないと、通信中に表示するダイアログやヌルポに影響が出るので注意
  • 処理をキャンセルするときの処理
  • 通信中画面遷移したりすると、通信キャンセルせざるおえない場合がある
    キャンセル処理をAsyncTask #onCancelled()にオーバライドしておき、
    AsyncTask #cansel()を実行することで、上記メソッドが呼ばれキャンセルできる仕組みだが、 非同期処理中 ( doInBackground() ) にキャンセルすると下記の2パターンに分岐する
    • Android2.3以前
    • すぐAsyncTask #onCancelled()が呼ばれる
    • Android3.0以降
    • doInBackground()が終了してからAsyncTask #onCancelled()が呼ばれる
    まぁ結局どっちもdoInBackground()が終了するまで処理は実行されるんだけど、onCancelled()がきても非同期処理が続くことを考えた実装が必要になる。
    画面遷移のため非同期処理をキャンセルする場合は、onProgressUpdate()にも注意。doInBackground()は途中で終わらないので、画面遷移後の存在しないView更新でヌルポしたりして痛い目にあうw
    対処方はdoInBackground()処理にisCancelled()メソッドでこまめに今キャンセル中か確認すること!


実装方法

AsyncTask<Params、Progress、Result>で表される。ジェネリクスはそれぞれ下記の意味
意味使用箇所
Params非同期通信の引数になる。execute()の引数で渡す →
doInBackground()の引数
Progress非同期処理中UIスレッド更新に渡す引数publishProgress()の引数で渡す →
onProgressUpdate()の引数
Result非同期処理結果のクラスdoInBackground()の戻り値 →
onPostExecute()の引数
今回の場合はこれなので
  • Params : リクエストするJSONのURL → String
  • Progress : 使用しない → Void
  • Result : レスポンスのJSONを文字列にしたもの → String
AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>() {
    @Override
    protected String doInBackground(String... params) {
        // TODO !非同期処理はアブストラクトメソッドなので必須!
        return null;
    }
};
で、あとは非同期処理中に通信処理。通信処理後にListViewの更新かける処理記述
AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>() {

    /**通信スレッド処理.
     * 引数のURLからリクエストし、レスポンスをJSONデータ文字列に変換
     * @param url リスエスト先
     * @return JSONデータの文字列
     */
    @Override
    protected String doInBackground(String... url) {
        return getAnimeInfo(url[0]);
    }

    /**UIスレッドの処理.
     * 引数のJSON文字列からタイトル配列を取得し、ListViewに表示
     * @param JSON文字列
     */
    @Override
    protected void onPostExecute(String json) {
        mListView.setAdapter(new ArrayAdapter<String>(
                MainActivity.this,
                android.R.layout.simple_expandable_list_item_1,
                getTitleForJSON(json)));
    }
};
最後に、Activityでexecute()を呼べば完成!※行削減の為コメ消去
public class MainActivity extends Activity{
    ListView mListView = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(mListView = new ListView(this));
        task.execute("http://animemap.net/api/table/osaka.json");
    }

    AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>() {
        @Override
        protected String doInBackground(String... url) {
            return getAnimeInfo(url[0]);
        }
        @Override
        protected void onPostExecute(String json) {
            mListView.setAdapter(new ArrayAdapter<String>(MainActivity.this,
                    android.R.layout.simple_expandable_list_item_1,getTitleForJSON(json)));
        }
    };
    
    public static String getAnimeInfo(String url) {
        AndroidHttpClient client = AndroidHttpClient.newInstance(null);
        HttpGet get = new HttpGet(url);
        StringBuilder sb = new StringBuilder();
        try {
            HttpResponse response = client.execute(get);
            BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String line = null;
            while ((line = br.readLine()) != null) sb.append(line);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            client.close();
        }
        return sb.toString();
    }

    public static String[] getTitleForJSON(String jsonInfo) {
        final ArrayList<String> tmp = new ArrayList<String>();
        try {
            JSONArray json = new JSONObject(jsonInfo)
            .getJSONObject("response")
            .getJSONArray("item");
            for (int i = 0; i < json.length(); i++)
                tmp.add(((JSONObject) json.get(i)).getString("title"));
        } catch (JSONException e){
            e.printStackTrace();
        }
        return tmp.toArray(new String[0]);
    }
}
あ、マニフェストの書き忘れ注意!

0 件のコメント:

コメントを投稿