高瀬です。
今回はAndroidアプリを作ってみる。目指すものは、Webサーバからデータを取得し、それを画面に表示する、という機能の実現とする。
アプリとWebサーバとの通信はRESTの形で行う。アプリ側でのRESTの実装には
Spring for Androidを利用。このSpring for Androidを試してみることが本題なのだが、RESTのサーバを準備したり、JUnitでAndroidアプリのテストなどもやってみる。
ソースコードはGitHubの
andrestリポジトリを参照。
RESTについて
自分はRESTが何なのか分かっていないので、まず用語辞典を読んでみる。
RESTとは
http://e-words.jp/w/REST.html
やっぱり何だかよく分からないが、自分の認識を書くと以下のとおり。
- URLが一つのリソースを表す。
- リソースへの操作の種類をHTTPメソッド(GET=取得、POST=新規追加、PUT=更新、DELETE=削除)で表す。
- レスポンスはJSON形式で送信する。
上記の用語辞典のページに書かれているとおり、RESTは別にHTTP通信に限ったものでもないし、レスポンスがJSONでなくてはいけないわけでもないが、今のところはこのくらいの内容で理解しておくことにする。
ここではサーバからのデータの取得のみを実装する。
サーバ側
サーバ側はPHPのYiiフレームワークで用意する。RESTでの実装を補助してくれるエクステンションがあるので、これを利用してみる。
RESTFullYii
http://www.yiiframework.com/extension/restfullyii/
このエクステンションでは、ERestControllerクラスの派生クラスとしてコントローラを作成することで、GETやPOSTに対応する処理を書くことができるようになっている。また、URLとリソースを結びつけるURLフォーマットも用意されている。
作成したのは
PostControllerクラス。エクステンションのソースコードに含まれているREADME.mdの説明にしたがって、doRestList()、doRestView()メソッドを記述した。
doRestList()は、ID指定なしでGETメソッドによりアクセスされた場合に呼び出される。ここでは8件の記事のデータを返すようにした。
$data = array(
array(
"post_id" => 1,
"time" => "3月3日 12時42分",
"title" => "Evernoteが不正アクセス被害"
),
array(
"post_id" => 2,
"time" => "3月4日 18時3分",
"title" => "警戒区域で初 ストビュー撮影"
),
:
);
$this->renderJson($data);
|
記事の内容は2013年3月4日のYahoo!JAPANのニュースから拝借。
doRestView()は、ID指定ありでGETメソッドによりアクセスされた場合に呼び出される。ここでは、8件の記事のうち、IDが指している番号の記事の内容を返すようにした。上記の一覧取得では各記事の内容をpost_id、time、titleとしているが、こちらではこれらに加えてcontentを返すようにしている。例えば、1が指定された場合は以下の内容を返す。
array(
"post_id" => 1,
"time" => "3月3日 12時42分",
"title" => "Evernoteが不正アクセス被害、全ユーザーのパスワードをリセット",
"content" => "米Evernoteは2日、同社のシステムに何者かが不正アクセスしたことを公表した。 ..."
),
|
アプリ側ではこれらのデータをJSON形式で受け取り、解析する。
なお、この後登場するSpring for AndroidのRestTemplateが、HTTPレスポンスのタイプがapplication/jsonであることを期待しているので、HTTPヘッダのContent-Typeにapplication/jsonをセットしている。
アプリで通信をする際の準備
通信を行うアプリを開発する場合、以下の2点に注意する必要がある。
- パーミッションandroid.permission.INTERNETを許可する。
- 通信処理はUIスレッド(メインスレッド)で実行してはならない。
参考:
Androidアプリでインターネット接続する為に必要な設定(android.permission.INTERNET)
http://feedyomi.blog32.fc2.com/blog-entry-181.html
android.os.NetworkOnMainThreadExceptionエラーへの対応方法
http://garnote.com/2012/10/android-os-networkonmainthreadexception.html
パーミッションの許可を指定しないと、実行時に以下の例外が発生した。
I/O error: socket failed: EACCES (Permission denied);
nested exception is java.net.SocketException: socket failed: EACCES (Permission denied)
|
UIスレッドで通信をしようとすると、上記参考ページのとおりNetworkOnMainThreadExceptionが発生する。
パーミッションについては、参考ページのとおり
AndroidManifest.xmlでパーミッションの指定をすればよい。
通信処理を行うスレッドについては、AsyncTaskクラスなどを使用してワーカースレッドを作成する必要がある。
参考:
AsyncTask を利用した非同期処理
http://android.keicode.com/basics/async-asynctask.php
AsyncTaskLoaderを利用した非同期処理を行う
http://techbooster.org/android/application/13492/
時代は AsyncTask より AsyncTaskLoader
http://archive.guma.jp/2011/11/-asynctask-asynctaskloader.html
AsyncTaskLoaderの方が便利だが、対象OSバージョンがAndroid 3.0以降であること、コールバックメソッドが必ずActivityになければならないことから、少々使いにくい印象がある。そこで、AsyncTaskを使用して、非同期処理完了時に呼び出されるハンドラを指定できるようにしてみた。
ハンドラ用に
OnFetchListenerクラスを定義。インスタンスを生成したら、ハンドラとなるメソッドを定義する。そして、AsyncTaskクラスのインスタンス生成時にハンドラを渡しておく。
これなら、一つのActivityで複数の非同期処理を実行したい場合に、ハンドラを個別に指定することができる。
非同期処理の準備ができたら、いよいよ通信処理を実装していく。
Spring for Android
ここからが本題。AndroidでのHTTP通信はorg.apache.http.client.HttpClientを使用してもよいが、RESTを簡単に実装できるという、Spring for Androidを使ってみることにする。
Spring for Android | SpringSource.org
http://www.springsource.org/spring-android
Jackson JSON Processor
http://jackson.codehaus.org/
Spring for Androidでは、HTTPメソッドGET、POST、PUT、DELETEにそれぞれ対応する、getForObject()、postForObject()、put()、delete()というメソッドが用意されている。使い方は、delete()ならURLのみ、それ以外はURLとパラメータを指定して呼び出すだけである。
マニュアルにしたがって、まずはインストールから。
Spring for Androidのライブラリ(spring-android-rest-template-{version}.jarとspring-android-core-{version}.jar)をダウンロードしたら、以下の手順(Spring for Android Reference Documentation から抜粋)でプロジェクトに組み込む。
- Refresh the project in Eclipse so the libs/ folder and jars display in the Package Explorer.
- Right-Click (Command-Click) the first jar.
- Select the BuildPath submenu.
- Select Add to Build Path from the context menu.
- Repeat these steps for each jar.
libsフォルダに各jarファイルをコピーしたら、Eclipse上で右クリックし、「Add to Build Path」でビルドパスに追加。これでRestTemplateが使用できるようになる。
ついでに、JSONの解析にJacksonというライブラリを使用するので、同様にプロジェクトに組み込んでおく。ダウンロードは上記の参照ページから。
一覧表示
記事の一覧を取得する。記事データの構造は前述のとおり、post_id、time、title、contentの4つの要素で構成されている。まずはこの構造に合わせたクラスを用意しておく。一覧取得ではcontentは使用しないが、単一記事と共用で使用できるクラスとするために含めている。
public class Post {
private Long post_id;
private String time;
private String title;
private String content;
public String getPost_id() {
return this.post_id.toString();
}
public void setPost_id(Long post_id) {
this.post_id = post_id;
}
public String getTime() {
return this.time;
}
public void setTime(String time) {
this.time = time;
}
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
}
|
そして、AsyncTaskを継承したクラス
PostListを作成。以下が記事一覧を取得する部分。
private ArrayList<HashMap<String, String>> data;
:
protected Long doInBackground(String... params) {
// TODO Auto-generated method stub
RestTemplate rest;
String url = this.context.getString(R.string.post_url);
// Create a new RestTemplate instance
rest = new RestTemplate();
try {
Log.i("App", "start");
// Add the String message converter
rest.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
Post posts[] = rest.getForObject(url, Post[].class);
this.data.removeAll(null);
for (int i=0; i<posts.length; i++) {
HashMap<String, String> map = new HashMap<String, String>();
map.put("post_id", posts[i].getPost_id());
map.put("time", posts[i].getTime());
map.put("title", posts[i].getTitle());
this.data.add(map);
}
Log.i("App", "end");
}
catch (Exception e) {
Log.e("App", e.getMessage());
}
return null;
}
|
getForObject()でサーバへ記事の一覧を要求すると、結果がPostクラスの配列で得られる。これをListViewにセットできる形にするため、メンバー変数dataに保存した。
アクセスするURLはres/values/
params.xmlファイルに定義している。
<resources>
<string name="post_url">http://hostname/path/index.php/api/post/</string>
</resources>
|
api/post/ にGETでアクセスすることで、サーバ側のPostController、doRestList()が呼び出され、記事の一覧取得が行われる。
単一記事表示
単一記事を取得する場合も、一覧取得とほぼ同様の処理となる。ソースは
PostDetailクラスを参照。
getForObject()の結果は、取得される記事は1件だけなので、Postクラスのインスタンスが1つだけ返される。
またアクセスするURLは api/post/1 などのように記事のIDを付加している。これによりサーバ側ではdoRestView()メソッドが呼び出され、該当の記事の取得が行われる。
JUnit
サーバからのデータ取得と画面表示ができたら、テストコードも書いてみる。Androidでのテストについては以下のページを参照。
Androidアプリ開発でテストを始めるための基礎知識
http://www.atmarkit.co.jp/fsmart/articles/androidtest01/01.html
Testing Fundamentals
http://developer.android.com/tools/testing/testing_android.html
さらに、非同期処理の部分については以下も参照。
AsyncTaskをJUnitでテストする方法
http://wavetalker.blog134.fc2.com/blog-entry-68.html
テストはプロジェクトを「Android Test Project」で作成する。ActivityのテストをするにはActivityInstrumentationTestCase2の派生クラスを作成する。
上記@ITの記事では見つけられなかったが、ActivityInstrumentationTestCase2の派生クラスを作る際、引数なしのデフォルトコンストラクタがないとテストが実行されなかった。デフォルトコンストラクタは自分で記述する必要がある。
AndrestcliActivityTestクラスを作成。以下のテストを行う。
- 記事の一覧が取得できること。
- 取得した記事の件数が一致すること。
- リストビューのアイテム数が記事の数と一致すること。
public void testGetPostList() throws Exception {
// 非同期処理完了時のハンドラーを定義
OnFetchListener handlerForTest = new OnFetchListener() {
@Override
public void onFetch(Context context, AsyncBase asyncTask) {
// unlock testcase's thread.
signal.countDown();
}
};
// create subclass of test target asynctask.
PostList posts = new PostList(activity, handlerForTest);
// execute asynctask.
// AndrestcliActivityのOnCreate()で実行されているものとは別にもう一度実行する
posts.execute("");
// wait for asynctask.
signal.await(30, TimeUnit.SECONDS);
PostList result = activity.getPostList();
assertNotNull("記事の一覧が取得できること。", result);
assertEquals("取得した記事の件数が一致すること。", 8, result.getData().size());
ListView listView1 = (ListView)activity.findViewById(R.id.listView1);
assertNotNull("リストビューが取得できること。", listView1);
assertEquals("リストビューのアイテム数が記事の数と一致すること。", 8, listView1.getCount());
}
|
サーバのモックをしていないので、サーバが動作していないとテストに失敗してしまうが、とりあえずこれでテストの実行を確認することができる。実施しているのは、記事の一覧が取得できることと、それをListViewで表示できていること、としている。
実は、テスト対象のAndrestcliActivityはOnCreate()で記事一覧の取得を行っており、テストコードでも記事一覧の取得を行っているので、一覧取得が2回動いてしまっている。テストをするには都合の悪い作りだっただろうか。
おわり
通信処理をUIスレッドで行ってはいけない、という制限がなんとも面倒だ。画面に記事を表示するというだけの機能しかないのに、ずいぶんと手間がかかった印象がある。
しかし、もっと多機能なアプリを作るにはどのみち非同期の処理は必要になってくるだろうし、一度非同期の処理を作ってしまえば後は使いまわすだけなので、実質的には取っ掛かりが少々面倒、というところだろうか。
Spring for Androidはそれほど苦労もなく使えたので、便利なライブラリだと思う。GET以外のメソッドについては別の機会に試してみる。
アプリが通信機能を持つことは多いだろう、覚えておいて損はないはず。