Home > android > 初めてのAndroid -第7章 世界との接続-

初めてのAndroid -第7章 世界との接続-

Ed Burnetteさんの「Hello, Android」の翻訳を、日本Androidの会幹事メンバーで監修しました。

初めてのAndroid 初めてのAndroid
作者: Ed Burnette, 日本Androidの会(監訳), 長尾高弘
出版社/メーカー: オライリージャパン
発売日: 2009/05/18
メディア: 大型本

Androidとは何かからはじまり、音をならしたりOpenGLを使ってみたり「数独」アプリを作ってみたりと、幅広いです。
幅広いと複雑になりがちですが、サンプルが非常にシンプルな形に纏められているので、とても分かりやすくなっています。

これに先立ち、2009年5月11日(月)に開催される日本Androidの会の月例イベントで先行即売会が行われます。
特典として購入者全員にGoodies君デザインのTシャツがもらえるそうです。

第7章「世界との接続」

私は第7章「世界との接続」などの監修をさせて頂きました。
この章ではAndroidとWebを繋ぐ方法について紹介しています。
いわゆるマッシュアップと呼ばれるアプリです。
最終的にWeb APIを叩くアプリが作れるようになります。
内容に関係することを少しご紹介します。

#この章で紹介されているソースコードはAndroid SDK1.5でもコンパイルでき、実行できることを独自に確認しています。

まとめ

エントリが長いので、先にまとめます。

  • Webマッシュアップの注意点はロード時間
  • ストレスレスなアプリにはマルチスレッド
  • AndroidのマルチスレッドはHandlerで実現
  • 初めてのAndroidはそういったAPIのクセまでちゃんと網羅
  • 「AsyncTask」はWebマッシュアップに便利なAPI
  • AsyncTaskは独自スレッドとmainスレッドが同居するAPI
  • サンプルで確認してみる

こういった内容です。
ご興味があれば読み進めて下さい。

Webマッシュアップの落とし穴

インターネットを利用するときによく落ちる穴はAndroidManifest.xmlの設定とUI Threadでしょうか。
前者は、インターネットに接続することを許可することを明示する必要があります。

<manifest>
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>

UI Threadについては少し慣れが必要です。
一言で言うと、Androidはシングル・スレッド モデルを採用しています。
すなわち、画面描画はmainと呼ばれるスレッドのみが担当します。
独自に作ったスレッドで画面描画を行うとエラーでアプリが落ちます。

このことについて、Android Blogでちょうど言及がありました。

Webと連携するアプリ
一番簡単な例

ボタンをクリックするとWeb上の画像を拾ってきて描画します。

public void onClick(View v) {
    Bitmap b = loadImageFromNetwork();
    mImageView.setImageBitmap(b);
}

これは正常に動作しますが、ヒドいです。
画像をダウンロードして描画するまで、ユーザ操作を一切受け付けません。
ユーザはストレスの塊です。

じゃぁどうするか?
この処理を別スレッドで実行することが思いつきます。

よくある画面描画のエラー

独自スレッドで先の処理を行います。

public void onClick(View v) {
  new Thread(new Runnable() {
    public void run() {
      Bitmap b = loadImageFromNetwork();
      mImageView.setImageBitmap(b);
    }
  }).start();
}

一見うまくいっているように見えますが、これはエラーになります。
上述した通り、Androidはシングル・スレッド モデルです。
別スレッドで画面描画はできません。

これを回避するのがHandlerです。

Handlerによるマルチスレッド

先ほどエラーになる例を、正しく動くように修正します。

Handler mHandler = new Handler();
public void onClick(View v) {
  new Thread(new Runnable() {
    public void run() {
      final Bitmap b = loadImageFromNetwork();
      mHandler.post(new Runnable() {
        public void run() {
          mImageView.setImageBitmap(b);
        }
      });
    }
  }).start();
}

新規スレッドでloadImageFromNetwork()を実行し、取得した画像をImageViewに設定するのはmainスレッドです。
HandlerからpostされたRunnableはmainスレッドにより実行されます。
このようにAndroid開発に慣れていない時に失敗しがちな部分についても本書ではしっかり扱っています。

疑似マルチスレッドで画面描画できるメソッドはHandlerの他にも、

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

などがありますが、中身は全てHandlerです。

Webマッシュアップのプチテクニック

Webマッシュアップでは、
「Webアクセス」→「取得データを弄る」→「画面描画」
という一連の流れがとても多いです。

例えば、
「周辺情報をWebから取得」→「取得データの整形」→「画面描画」
という感じでしょうか。

このような処理をユーザにストレス無く提供するにはマルチスレッドは必須です。
つまり、「周辺情報をWebから取得」→「取得データの整形」の処理は別スレッドで処理します。
これを実現するのがHandlerでした。

しかし、見ての通りソースコードが少し読み難く複雑になります。
これを解決してくれるのがAsyncTaskです。
Android SDK1.5からサポートされているAPIです。

AsyncTask

AsyncTaskは「独自スレッド」と「mainスレッド」が同居したAPIです。
このAPIは、
「独自スレッドでバックグラウンド処理」→(処理終了)→「mainスレッドを実行」
という一連の流れをサポートしてくれます。

最も簡単な実装は以下です。

public class DownloadTask extends AsyncTask<T1, T2, T3> {
     @Override
     protected T3 doInBackground(T1... args) {
         //独自スレッドでバックグラウンド実行したい処理
     }
     @Override
     protected void onPostExecute(T3 result) {
         //画面描画できるmainスレッドで実行したい処理
     }
 }

これを実行するには、

new DownloadTask().execute(args);

とするだけです。argsはdoInBackground()の引数に渡される値です。

これを実行すると、バックグラウンドでdoInBackground(args)が実行され、
この処理が終了すると、その返り値をonPostExecute()の引数に渡し、
onPostExecute()がmainスレッドで実行されます。

サンプル

早速AsyncTaskのサンプルを作ってみます。

public class DownloadTask extends AsyncTask<String, Integer, Bitmap> {
    //バックグラウンドで画像をダウンロードする
    @Override
    protected Bitmap doInBackground(String... params) {
        String uri = params[0];
        return downloadImage(uri);
    }
 
    //画像を描画する
    @Override
    protected void onPostExecute(Bitmap result) {
        mActivity.setResultImage(result);
    }
 }

これを実行するのが以下のActivityです。

public class AsyncTaskActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        findResouces();
    }
 
    // プログラムからアクセスするリソースを取得する
    private void findResouces() {
        mResultImage = (ImageView) findViewById(R.id.result_img);
        mLoadFrontBtn = (Button) findViewById(R.id.load_front_btn);
        mLoadFrontBtn.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                // mainスレッドで画像をダウンロードして描画する
                downloadAndUpdateImage(true);
            }
        });
 
        mLoadBackBtn = (Button) findViewById(R.id.load_back_btn);
        mLoadBackBtn.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                // 画像のダウンロードはバックグラウンドで実行しmainスレッドで描画
                downloadAndUpdateImage(false);
            }
        });
    }
 
    // 画像をダウンロードして表示する
    private void downloadAndUpdateImage(boolean isFront) {
        String uri = getUri();// URI文字列の取得
        DownloadTask task = new DownloadTask(this);
 
        // ダウンロードを開始し
        if (isFront) {
            // mainスレッドで画像をダウンロード
            Bitmap bit = task.downloadImage(uri);
            setResultImage(bit);//画像を描画
        } else {
            // バックグラウンドで画像をダウンロードしてmainスレッドで画像を描画
            task.execute(uri);
        }
    }
}
動作

バックグラウンドで動作していることが分かりやすいように、
mainスレッドでタイマーを連続描画しています。
バックグラウンドで画像をダウンロードする場合「Load(B)」はタイマーがちゃんとカウントアップされるのに対し、
mainスレッドで画像をダウンロードする場合「Load(F)」はタイマーがカウントアップされません。
これはmainスレッドが画像のダウンロード待ちのため、他の画面描画が待たされている為です。
ボタンがオレンジ色に反転するのさえ、戻りません。

ダウンロード

このアプリのEclipseプロジェクトを公開します。
どうぞ自由にご参考になって下さい。
AsyncTaskTest.zip

ソースコード

主なクラスのソースコードを公開しておきます。

AsyncTaskActivity.java

package com.adamrocker.android.test.async;
 
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.*;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.*;
 
public class AsyncTaskActivity extends Activity {
    private TextView mTimerText;
    private EditText mUriEdit;
    private ImageView mResultImage;
    private Button mLoadFrontBtn;
    private Button mClearBtn;
    private Button mLoadBackBtn;
    private int mTime;
    private boolean mRunTimerFlag = false;
    private static final int MSG_UPDATE_TIMER = 0;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_UPDATE_TIMER:
                loopTimerCount();
                break;
            }
        }
    };
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        findResouces();
    }
 
    @Override
    public void onStop() {
        super.onStop();
    }
 
    // プログラムからアクセスするリソースを取得する
    private void findResouces() {
        mTimerText = (TextView) findViewById(R.id.timer_txt);
        mUriEdit = (EditText) findViewById(R.id.uri_edit);
        mResultImage = (ImageView) findViewById(R.id.result_img);
        mLoadFrontBtn = (Button) findViewById(R.id.load_front_btn);
        mLoadFrontBtn.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                downloadAndUpdateImage(true);
            }
        });
 
        mClearBtn = (Button) findViewById(R.id.clear_btn);
        mClearBtn.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                clearImage();
                clearTimer();
            }
        });
 
        mLoadBackBtn = (Button) findViewById(R.id.load_back_btn);
        mLoadBackBtn.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                downloadAndUpdateImage(false);
            }
        });
    }
 
    // 画像をダウンロードして表示する
    private void downloadAndUpdateImage(boolean isFront) {
        clearImage();// 画像を削除
        String uri = getUri();// URI文字列の取得
        DownloadTask task = new DownloadTask(this);
        zeroStartTimer();// タイマー開始
        if (isFront) {
            Bitmap bit = task.downloadImage(uri);
            setResultImage(bit);
            stopTimer();// タイマー停止
        } else {
            // ダウンロードを開始し、終了するとタイマーも停止する
            task.execute(uri);
        }
    }
 
    // タイマー表示を更新する
    private void updateTimer() {
        setTimer(mTime);
        mTime++;
    }
 
    // 50msec間隔でupdateTimer()を実行する
    private void loopTimerCount() {
        if (mRunTimerFlag) {
            updateTimer();
            mHandler.sendMessageDelayed(mHandler
                    .obtainMessage(MSG_UPDATE_TIMER), 50);
        }
    }
 
    // タイマーカウントを0から開始する
    void zeroStartTimer() {
        clearTimer();
        mRunTimerFlag = true;
        loopTimerCount();
    }
 
    // タイマーをクリアする
    void clearTimer() {
        mTime = 0;
        setTimer(mTime);
        mTimerText.invalidate();
    }
 
    // タイマーカウントを停止する
    void stopTimer() {
        mRunTimerFlag = false;
    }
 
    // TextEditorから文字列(URI形式のはず)を取得する
    private String getUri() {
        return mUriEdit.getText().toString();
    }
 
    // UIのImageを設定する
    void setResultImage(Bitmap result) {
        mResultImage.setImageBitmap(result);
    }
 
    // タイマーに文字を設定する
    private void setTimer(int time){
        mTimerText.setText(String.valueOf(time));
    }
 
    // 画像表示を削除する
    private void clearImage() {
        mResultImage.setImageBitmap(null);
    }
}

DownloadTask.java

package com.adamrocker.android.test.async;
 
import java.io.InputStream;
import java.net.URI;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
 
public class DownloadTask extends AsyncTask<String, Integer, Bitmap> {
    private HttpClient mClient;
    private HttpGet mGetMethod;
    private AsyncTaskActivity mActivity;
 
    public DownloadTask(AsyncTaskActivity activity) {
        mActivity = activity;
        mClient = new DefaultHttpClient();
        mGetMethod = new HttpGet();
    }
 
    Bitmap downloadImage(String uri) {
        try {
            mGetMethod.setURI(new URI(uri));
            HttpResponse resp = mClient.execute(mGetMethod);
            if (resp.getStatusLine().getStatusCode() < 400) {
                InputStream is = resp.getEntity().getContent();
                Bitmap bit = createBitmap(is);
                is.close();
                return bit;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
 
    private Bitmap createBitmap(InputStream is) {
        return BitmapFactory.decodeStream(is);
    }
 
    //バックグラウンドで画像をダウンロードする
    @Override
    protected Bitmap doInBackground(String... params) {
        String uri = params[0];
        return downloadImage(uri);
    }
 
    //画像を描画して、タイマーを停止する
    @Override
    protected void onPostExecute(Bitmap result) {
        mActivity.setResultImage(result);
        mActivity.stopTimer();
    }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<TextView android:id="@+id/timer_txt" android:textSize="30px"
		android:textColor="#fff" android:textStyle="bold" android:gravity="center"
		android:text="0" android:layout_width="fill_parent"
		android:layout_height="wrap_content" />
	<EditText android:id="@+id/uri_edit"
		android:text="http://www.android.com/intl/ja/media/android_vector.jpg"
		android:hint="Image address" android:layout_width="fill_parent"
		android:layout_height="wrap_content" />
	<LinearLayout android:orientation="horizontal"
		android:layout_width="fill_parent" android:layout_height="wrap_content">
		<Button android:id="@+id/load_front_btn" android:text="Load(F)"
			android:layout_width="fill_parent" android:layout_height="wrap_content"
			android:layout_weight="1" />
		<Button android:id="@+id/clear_btn" android:text="Clear"
			android:layout_width="fill_parent" android:layout_height="wrap_content"
			android:layout_weight="1" />
		<Button android:id="@+id/load_back_btn" android:text="Load(B)"
			android:layout_width="fill_parent" android:layout_height="wrap_content"
			android:layout_weight="1" />
	</LinearLayout>
	<ImageView android:id="@+id/result_img" android:gravity="center"
		android:layout_width="fill_parent" android:layout_height="wrap_content" />
</LinearLayout>
このエントリをはてなブックマークに登録 Deliciousにブックマーク
関連のありそうなエントリ

Comments:6

fudou 09-06-22 (月) 23:45

>Handlerによるマルチスレッド
>先ほどエラーになる例を、正しく動くように修正します。

こんにちは。fudouです。

この例でUIはアップデートできるようになったのですが、dialogを表示しようとすると
ERROR/AndroidRuntime(7990): java.lang.RuntimeException: Can’t create handler inside thread that has not called Looper.prepare()
となってしまいます。
スレッドの外側で表示しようとするとエラーは出ませんが、ダイアログが表示されません。
どうすればダイアログを出しつつUIを更新できるのでしょうか?
(初めてのandroidを読んだのですがわかりませんでした・・・)

ヒントでもいただければ幸いです。

adamrocker 09-06-23 (火) 7:36

>fudouさん
コメントありがとう御座います。
もう少し詳しい情報を頂きたいのですが。

・mImageView.setImageBitmap(b);の部分にダイアログを表示しようとして落ちるのでしょうか?
・ダイアログを出しながらUIを変更というのは、ダイアログの裏にあるUIということでしょうか?

fudou 09-06-24 (水) 4:07

>mImageView.setImageBitmap(b);の部分にダイアログを表示しようとして落ちるのでしょうか?
下記のコードのコメント部をご覧ください。イメージをロード中にお待ちくださいを表示しようとしています。

>ダイアログを出しながらUIを変更というのは、ダイアログの裏にあるUIということでしょうか?
その通りです。

コード抜粋

bt.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
//ここだとエラーは出ないがダイアログも表示されない
// showwaitdialog(”wait”, “please”);
new Thread(new Runnable() {
public void run() {
// ここだとCan’t create handler inside thread that has not
// called Looper.prepare()
showwaitdialog(”wait”, “please”);
mHandler.post(new Runnable() {
public void run() {
setImage(iv, uri);
dialog.dismiss();
}
});
}
}).start();
}
});

adamrocker 09-06-24 (水) 8:08

>fudouさん
画面描画はpostメソッドの引数のRunnableオブジェクトで実施する必要があります。
一度お試し下さい。
#できるかどうかを確認していないので憶測ですが…

fudou 09-06-27 (土) 14:39

教えていただいたことを参考に色々やっていたので返信が遅れて申し訳ありません。

1.dialogの表示をpostで投げるようにしたところエラーは出なくなりましたが、ダイアログは表示されることなく画像がロードされます。
2.dismissをコメントアウトしたところ、画像ロード後にダイアログが表示されるので、表示自体に問題ないことはわかりました。
3.(意味があるかどうかわかりませんが)dialogの表示自体を、画像ロードのスレッドとは別の新しいスレッドに分けてやってみましたが、けっkは1(および2)と変化ありません。

エラーが出なくなるのはいいのですが、ダイアログが出ないのでは意味がないので困ってしまっています。
お時間があるときにでもアドバイスいただければ幸いです。

adamrocker 09-06-28 (日) 20:35

>fudouさん
一度確認したいのですが、やりたいのは、ネットから画像をダウンロードしている時に、
例えば「ダウンロード中」というダイアログを表示したいということであってますか?

Comment Form
Remember personal info

*
To prove that you're not a bot, enter this code
Anti-Spam Image

Trackbacks:2

Trackback URL for this entry
http://www.adamrocker.com/blog/255/hello-android-chapter7-the-connected-world.html/trackback/
Listed below are links to weblogs that reference
初めてのAndroid -第7章 世界との接続- from throw Life
trackback from Java Programming の初学者 Memo 09-05-10 (日) 0:58

[android][Book]初めてのAndroid…

先程、書いていました「初めてのAndroid」*1ですが、せっかくなので私は、購入してないけど書籍情報を載せておきます。 初めてのAndroid 作者: Ed Bur (more…)

trackback from Android Zaurusはてな館 09-05-10 (日) 20:20

JNI遅くないよ。SQLite悪くないよ。TraceView…

id:minghaiさんがSKKをAndroidに実装*1されてて、SQLite遅すぎワロタwwwとかおっしゃってたので、なんでそんなに遅いんだろと思って、ごにょごに (more…)

Home > android > 初めてのAndroid -第7章 世界との接続-

Author
Search
Feeds
Meta

Return to page top