2013年7月21日日曜日

Android2.3.3でAsyncTaskLoaderを使おう

Android2.3.3でアプリを開発しています。

バックグラウンドで通信して、取ってきたデータをキャッシュしてユーザに提示するような、
よくあるアプリケーションを制作しています。
実際にはアップデートを担当しています。

現在、アプリのバックグラウンド処理にはAsyncTaskを利用しています。
AsyncTaskについて調べているとAndroid3.0からはAsyncTaskは非推奨になっていて、
代わりにAsyncTaskLoaderなるものが登場していることを知った。

http://www.srv-shinra.com/wordpress/?p=308

を見てもわかるようにそういうことだそうです。
APIレベルを上げてやればいいのですが、Android.2.3.3のユーザも結構いるため、
そう簡単に切り捨てることはできません。

でも、AsyncTaskはcancel処理あたりに問題があるという記事をチラホラみかけます。
例えば、

GCMのIntentServiceでAsyncTaskを実行したらsending message to a Handler on a dead threadの警告が出て悩まされる


に取り上げられていることが発生するみたいです。
回避するためにはThreadを使ったりするみたいですが、それも面倒です。

だから、Android2.3.3でもAsyncTaskLoaderを使いたいという結論に至りました。
でも、ネット上で実装している人の情報をあまり見つけることができません。
調べ方が悪いのか。。。

でも、SupportLibraryにLoaderManagerとかあるのになんでActivityにgetLoaderManagerメソッドがないの??

と思っていろいろ調べていたら、FragmentActivityに実装されていました。
なるほど、もうActivityで作るのは時代遅れなのですね。。

でも、ActivityにないLoaderManagerさえ自前で自作してしまえば
ActivityでもAsyncTaskLoaderが使えるようです。

Androidソースコードを見ると、LoaderManagerはAbstractなので実際に実装されているクラスは、
LoaderManagerImplというクラスがありました。

それを参考にしてLoaderManagerだけ実装してやれば実現できそうです。
HashMapで管理するLoaderManagerを実装して簡単に試してみたらできました。

でも、もうActivityはもう時代遅れなのか?!。。。

Androidソースを除いてたらSparseArrayなるクラスがあることを知った。

「AndroidのSparseArrayは本当に速いのか測定してみた」
http://thinking-megane.blogspot.jp/2012/06/androidsparsearray.html

ここを見ると以降SparseArrayを使おうと思いました。

結局、今回の調べごとで勉強したことは、
・FragmentActivityをつけば問題は解決されることと
・SparseArrayはHashMapより速い
でした。

収穫収穫!!

以下は実装途中ですが、こんな感じでLoaderManagerを実装すればActivity(Android2.3.3)でもAsyncTaskLoaderが使えるということです。
--------
package com.example.asyncloadertest;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;

public class TestLoader extends AsyncTaskLoader<String> {

    String result;

    public TestLoader(Context context) {
super(context);
    }

    @Override
    public String loadInBackground() {
DefaultHttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet("http://www.yahoo.co.jp/");
try {
    HttpResponse execute = client.execute(get);
    String res = EntityUtils.toString(execute.getEntity());
    return res;
} catch (ClientProtocolException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
return null;
    }

    @Override
    public void deliverResult(String data) {
if (isReset()) {
    if (this.result != null) {
this.result = null;
    }
    return;
}

this.result = data;

if (isStarted()) {
    super.deliverResult(data);
}
    }

    @Override
    protected void onStartLoading() {
if (this.result != null) {
    deliverResult(this.result);
}
if (takeContentChanged() || this.result == null) {
    // これをやっておくとonCreateLoaderで開始処理をしなくてよくなる
    forceLoad();
}
    }

    @Override
    protected void onStopLoading() {
super.onStopLoading();
cancelLoad();
    }

    @Override
    protected void onReset() {
super.onReset();
onStopLoading();
    }

    @Override
    public void dump(String prefix, FileDescriptor fd, PrintWriter writer,
    String[] args) {
super.dump(prefix, fd, writer, args);
writer.print(prefix);
writer.print("result=");
writer.println(this.result);
    }

}
--------
package com.example.asyncloadertest;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.HashMap;

import android.os.Bundle;
import android.app.Activity;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.Menu;
import android.widget.TextView;

public class AsyncTascLoaderTestActivity extends Activity implements LoaderCallbacks<String> {

    // Yahooをロードする
    public static final int TASK_001 = 0x00001;
    
    LoaderManager loaderManager = new LoaderManager() {
        HashMap<Integer, Loader<String>> loaders = new HashMap<Integer, Loader<String>>();
        
        @Override
        public <String> Loader<String> restartLoader(int arg0, Bundle arg1,
        LoaderCallbacks<String> arg2) {
    return null;
        }
        
        /**
         * ローダの初期化
         */
        @Override
        public <String> Loader<String> initLoader(int arg0, Bundle arg1,
        LoaderCallbacks<String> arg2) {
            Log.d("Called","initLoader");
    Loader<String> loader = arg2.onCreateLoader(arg0, arg1);
    // do initialize...
    loaders.put(arg0, (Loader<String>) loader);
    return loader;
        }
        
        @Override
        public <String> Loader<String> getLoader(int arg0) {
            Log.d("Called","getLoader");
    return (Loader<String>) loaders.get(arg0);
        }
        
        @Override
        public void dump(String arg0, FileDescriptor arg1, PrintWriter arg2,
        String[] arg3) {
            // nothing
        }
        
        /**
         * 破棄
         * これより前にロードを止めるべき
         */
        @Override
        public void destroyLoader(int arg0) {
            Log.d("Called","destroyLoader");
            loaders.remove(arg0);
        }
    };
    // TextView
    TextView view;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_tasc_loader_test);
// ローダー初期化
loaderManager.initLoader(TASK_001, null, this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.async_tasc_loader_test, menu);
return true;
    }

    /**
     * ローダー作成
     */
    @Override
    public Loader<String> onCreateLoader(int arg0, Bundle arg1) {
        Log.d("Called","onCreateLoader");
// Create
TestLoader loader = new TestLoader(this);
return loader;
    }

    /**
     * バックグラウンド処理が終わったら
     */
    @Override
    public void onLoadFinished(Loader<String> arg0, String arg1) {
        Log.d("Called","onLoadFinished");
view = (TextView) findViewById(R.id.view);
if (arg0 != null && arg0.getId() == TASK_001) {
    view.setText(arg1);
}
    }

    /**
     * 処理がリセットされたら
     */
    @Override
    public void onLoaderReset(Loader<String> arg0) {
if (view != null)
    view.setText("Loader is reset");
    }


}
--------

jQueryでtableをまるごとソートするためには

HTMLでテーブルを組んだあとで、何かをキーにして並べ替えをしたい、
そういうこと(機能)を実装したい場合があると思います。

JSのsort関数(定義に関しては下の方にまとめているので見てください)と
jQueryを組み合わせることで結構簡単にできたので備忘録しときます。

改善、もっと簡単にできるってのがあればコメントお願い致します。

sort関数は配列を簡単に並べ替えることができます。

よくある例では以下のような配列をソートします。
var a = [2, 1, 4, 3];

それを応用して以下のような配列をソートします。
var b = [object, object, object];

具体例を見ながらの方がいいと思います。
例)以下のテーブルを入力されたキーをもとにソートし直したい

<a id='resort' href='#'/>並べなおす</a>
<table>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge1' value="0"/>
  </td>
  <td>
  ....
  </td>
</tr>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge2' value=""/>
  </td>
  <td>
  ....
  </td>
</tr>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge3' value="a"/>
  </td>
  <td>
  ....
  </td>
</tr>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge4'  value="4"/>
  </td>
  <td>
  ....
  </td>
</tr>



</table>

jQueryで並べなおす機能を実装するには以下の2ステップをコーディングするだけ。

1、ソート対象配列を取得
【実装】
var sortItem = $('.sort_item');

2、ソート関数を実装
【実装】
sortItem.sort(function(a, b){
  //ここに比較処理を書く
  //返却は-1, 0 , 1のどれか
});

いろいろ検証してsort関数について理解を深めていきます。
検証はChrome28を使ってます。

★検証パターン1
引数aとbには何が入ってくるのか!?
【ソースコード】
<html>
<head><title>sort test</title>
<meta charset="utf-8"/>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script>
$(document).ready(function(){
$("#resort").click(function(){
var sortItem = $('.sort_item');
sortItem.sort(function(a, b){
//検証パターン1
//出力
console.log(a);
console.log(b);
});
});
});
</script>
</head>
<body>
<a id='resort' href='#'/>並べなおす</a>
<table>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge1' value="0"/>
  </td>
  <td>
  0
  </td>
</tr>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge2' value=""/>
  </td>
  <td>
  ....
  </td>
</tr>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge3' value="a"/>
  </td>
  <td>
  a
  </td>
</tr>

<tr class='sort_item'>
  <td class='sort_key'>
    <input type='text' name='hoge4'  value="4"/>
  </td>
  <td>
  4
  </td>
</tr>
</body>
</html>

【結果】
出力は以下のようになりました。
これ順番に隣同士の要素を引数a、bに与えてるんですね。
-------

-------


さて、本題に戻ります。
これで引数に何が入ってくるかわかりました。

そこからキーを取って来てくらべればいいということです。

var aKey = $(a).find(".sort_key").val();
var bKey = $(b).find(".sort_key").val();
if (aKey > bKey) return -1;
else if (aKey < bKey) return 1;
else return 0;

という感じにすればいいということですね。

これでいろいろ応用が利くようになりますね!


■sort関数の定義
元情報(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)に基づいて話しています。

構文は【array.sort(compareFunction);】で定義されています。

この引数のcompareFunctionが今回のミソです。
元情報から引用すればcompareFunctionは「ソート順を定義する関数を指定します。省略された場合、配列を各要素の文字列比較に基づき辞書順にソートされます。」だそうです。


compareFunction が与えられた場合、配列の要素は比較関数の返り値に基づきソートされます。もし a と b が比較されようとしている要素の場合、
  • compareFunction(a, b) が 0 未満の場合、a を b より小さい添字にソートします。
  • compareFunction(a, b) が 0 を返す場合、a と b は互いに関して変えることなく、他のすべての要素に関してソートします。注意: ECMAScript 標準はこの振る舞いを保証しておらず、そのため一部のブラウザ (例えば、遅くとも 2003 年以前のバージョンの Mozilla) はこれを尊重していません。
  • compareFunction(a, b) が 0 より大きい場合、b を a より小さい添字にソートします。
(元情報より引用)