読者です 読者をやめる 読者になる 読者になる

Scala を使った Android アプリで JSON データをリスト表示 - AsyncTask を使った非同期処理と ListView によるリスト表示

Java Android Scala

今回は以前に iUI や jQTouch で作成した JSON データをリスト表示するサンプル id:fits:20100715 id:fits:20100731 の Android アプリ版を作成してみました。(JSON を返すサーバー処理は id:fits:20100713 の Sinatra サンプルを使用)

Android版の画面は以下の通りです。(エミュレータ上での実行画面)

(1) DBのリスト表示画面

(2) テーブルのリスト表示画面(information_schema 選択時)

(3) テーブルの詳細表示画面(COLUMNS 選択時)

ちなみに、使用した環境は前回 id:fits:20100909 と同じです。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20100916/ です。

事前準備(sbtプロジェクト作成など)

sbt のプロジェクト作成や sbt-android-plugin の設定に関しては、前回 id:fits:20100909 を参照。

AsyncTask による非同期処理

今回は、JSON データを非同期に取得するために AsyncTask を使いました。
AsyncTask を使えば、Thread や Handler とかを直接使用せずに、バックグラウンド処理と処理結果の UI への反映(UI スレッド上で実施される)を容易に実現できます。

  • doInBackground の処理はバックグラウンドで実行
  • onPostExecute の処理(結果の反映)は UI スレッドで実行

doInBackground の引数には execute メソッド呼び出し時の引数が渡され、doInBackground の戻り値が onPostExecute に渡されます。

ただし、Scala で AsyncTask のサブクラスを直接実装したところ、エミュレータでの実行時に doInBackground メソッド呼び出しで AbstractMethodError が発生しました。(ビルドは正常に完了)
原因は追究していませんが、可変長引数が影響していると考えています。

実行時に AbstractMethodError が発生した実装例(Scala
・・・
import android.os.AsyncTask
import org.json.JSONArray

class JsonLoadTask extends AsyncTask[String, Unit, Option[JSONArray]] {
    override def doInBackground(params: String*): Option[JSONArray] = {
        ・・・
    }
    ・・・
}


実行時の AbstractMethodError 発生を回避するため、Java で以下のようなサブクラスを作成し、Scala 側で可変長引数のメソッドをオーバーライドしなくて済むようにしました。今回はバックグラウンド処理の進捗は管理しないため、AsyncTask の 2つ目の Generic タイプは Void にしました。

src/main/java/fits/sample/SingleAsyncTask.java
package fits.sample;

import android.os.AsyncTask;

class SingleAsyncTask<T, U> extends AsyncTask<T, Void, U> {
    protected U doInBackground(T... params) {
        return (params.length > 0)? doSingleTask(params[0]): null;
    }

    //Scala のサブクラス側でオーバーライドするためのメソッド定義
    protected U doSingleTask(T param) {
        return null;
    }
}

上記クラスのサブクラスを Scala で実装したものが以下です。
バックグラウンド処理結果の反映処理はコンストラクタの引数で渡せるようにしています。

src/main/scala/JsonLoadTask.scala
package fits.sample

import android.util.Log
import scala.io.Source
import org.json._

/**
 * JSON を非同期的にロードする処理を担うタスククラス
 */
class JsonLoadTask(val proc: Option[JSONArray] => Unit) 
    extends SingleAsyncTask[String, Option[JSONArray]] {

    //JSON のロード処理を実装(バックグラウンド処理)
    override def doSingleTask(url: String): Option[JSONArray] = {
        try {
            //指定の URL から JSON データ取得
            val json = Source.fromURL(url).mkString
            Some(new JSONArray(json))
        }
        catch {
            case e: Exception => 
                Log.e("json", e.getMessage(), e)
                None
        }
    }

    //UI への結果反映処理(UIスレッドで処理)
    override def onPostExecute(result: Option[JSONArray]) {
        proc(result)
    }
}

リスト表示(ListView)

Android でリスト表示を行うには以下のような作業を行います。

  • ListActivity のサブクラスを作成し、リストアイテムを提供する Adapter を setListAdapter メソッドで設定
  • レイアウトリソースに ListView を定義して id 属性に @id/android:list を設定、リストアイテムが空だった場合に表示するウィジェットを定義し @id/android:empty を設定
  • リストアイテム(1行のセル)用のレイアウトリソース作成

ListActivity のサブクラスを以下のように実装しました。Adapter には ArrayAdapter を使用し、JSON から取得した DB 名を配列化して設定しています。
ArrayAdapter には、第2引数でリストアイテム用のレイアウトリソースを指定し、第3引数で値を表示させるウィジェットの id を指定します。

src/main/scala/JsonListActivity.scala
package fits.sample

import android.app.ListActivity
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import android.widget.ListView
import org.json._

/**
 * DB をリスト表示するクラス
 */
class JsonListActivity extends ListActivity {

    override def onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.main)

        loadJson(getResources().getString(R.string.db_url))
    }

    //リストアイテムをクリックした際の処理
    override def onListItemClick(l: ListView, v: View, p: Int, id: Long) {
        //クリックしたアイテムを取得
        val selectedItem = l.getItemAtPosition(p)

        val intent = new Intent(this, classOf[DbActivity])
        intent.putExtra("DB", selectedItem.toString())

        //DbActivity 画面を起動
        startActivity(intent)
    }

    //JSON データを取得する
    private def loadJson(url: String) {
        //JSON取得時の処理
        val proc: Option[JSONArray] => Unit = {
            case Some(json) =>
                //DB名(table_schema の値)のリスト作成
                val dbList = for (i <- 0 until json.length()) 
                    yield json.optJSONObject(i).getString("table_schema")

                //DB名のリストを配列化し ArrayAdapter に設定
                //第2引数でリストアイテム用のレイアウトリソースを指定し
                //第3引数で DB 名を表示させるウィジェットの id を指定
                val adapter = new ArrayAdapter(this, R.layout.item, R.id.name, dbList.toArray)
                //adapter を設定
                setListAdapter(adapter)

            case None =>
        }

        //非同期処理の実行
        new JsonLoadTask(proc).execute(url)
    }
}

なお、(2) テーブルのリスト表示画面(src/main/scala/DbActivity.scala 参照)では SimpleAdapter を使って、もう少し複雑な処理を実施しています。(HashMap の List を SimpleAdapter に設定し、HashMap を Intent に格納し次画面に渡す等)


次に、レイアウトリソースにリスト表示の設定を行います。ListView の id 属性に @id/android:list を設定し、リストが空の場合に表示する TextView の id 属性に @id/android:empty を設定します。

src/main/res/layout/main.xml
<!-- (1) DBのリスト表示画面のレイアウト -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <!-- リスト表示(リストが空ではない場合の表示) -->
    <ListView 
        android:id="@id/android:list"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" />

    <!-- リストが空の場合の表示 -->
    <TextView 
        android:id="@id/android:empty"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="@string/empty" />
</LinearLayout>

リストアイテム用のレイアウトリソースを以下のように定義しました。
iPhone における「セルのアクセサリ」(リストアイテムの右端に表示される ">" 等の画像の事を指す)を Android で実現する上手い方法が思いつかなかったので、TextView の横幅を数値で指定し(styles.xml の listItemText スタイルで android:layout_width に 280dp を設定)、その隣に ImageView で画像を表示する事で対応しました。

src/main/res/layout/item.xml
<!-- リストアイテム(1行)用のレイアウト -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <TextView style="@style/listItemText"
        android:id="@+id/name"
        android:singleLine="true"
    />

    <!-- セルのアクセサリ画像を表示 -->
    <ImageView style="@style/listItemImage"
        android:id="@+id/img" 
        android:src="@drawable/chevron"
    />
</LinearLayout>

テーマの指定

Android のデフォルトテーマは背景色が黒なので、背景色が白いテーマ Theme.Light をアプリケーションに設定しています。
アプリケーションのテーマは AndroidManifest.xml の application 要素の theme 属性で設定することができます。

src/main/AndroidManifest.xml
<manifest ・・・>
  <!-- theme 属性に Theme.Light テーマを設定 -->
  <application android:theme="@android:style/Theme.Light" ・・・>
    <activity android:name=".JsonListActivity" ・・・>
      ・・・
    </activity>
    ・・・
  </application>
  ・・・
</manifest>

ちなみに今回は実施してませんが、style 要素を使って独自のカスタムテーマを作成する事もできるようです。