[Android] HeartRails Express を使って駅検索アプリを作ってみた
HeartRails Express という便利な API
今回は HeartRails Express という API を使って、簡単な駅検索アプリを作る方法をご紹介したいと思います。HeartRails Express は路線や駅名データ、駅の地理情報などを XML または JSON (P) 形式で取得できる API です。無料で使うことができます!今回実装するのは以下の機能です。
- エリア一覧リストからエリアを選択
- 都道府県一覧リストから都道府県を選択
- 路線一覧から路線を選択
- 駅一覧から駅を選択
- GoogleMap アプリで駅の所在地を表示
ソースコード
今回実装したアプリのソースコードを GitHub に公開しました。ぜひ参考にしてください。
API をコールして JSON データを取得する
まずは API をコールし、JSON データを取得するところまで実装します。ということで API の仕様を調べてみます。とっても簡単!
路線一覧取得APIの仕様(使うところだけ抜粋)
リクエスト
パラメータ | 値 | 説明 |
---|---|---|
method | getLines | メソッド名 |
prefecture | string | URLエンコード(UTF-8)された都道府県名 |
レスポンス
フィールド | 説明 |
---|---|
response | 路線名の一覧 |
line | 路線名 |
上記 API は路線一覧の取得なので prefecture をパラメータに渡す必要があります。これが駅取得 API だと line パラメータに路線名を渡す、などといった感じになります。サンプルの URL を載せておきます。
http://express.heartrails.com/api/json?method=getLines&prefecture=%E5%AE%AE%E5%9F%8E%E7%9C%8C
これで取得できるデータは以下のようになります。
{"response":{"line":["JR仙山線","JR仙石線","JR利府線","JR大船渡線","JR常磐線","JR東北本線","JR気仙沼線","JR石巻線","JR陸羽東線","仙台市南北線","東北新幹線","阿武隈急行","仙台空港鉄道"]}}
仕様がわかったところで実装に移りましょう。まずは非同期処理にするために AsyncTaskLoader クラスを新規作成します。
package jp.classmethod.android.sample.searchstation; import android.content.Context; import android.support.v4.content.AsyncTaskLoader; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; /** * APIをコールする非同期タスク. */ public class ExpressLoader extends AsyncTaskLoader<ArrayList<HashMap<String, String>>> { /** エリア取得ID. */ public static final int AREAS = 0; /** 都道府県取得ID. */ public static final int PREFECTURES = 1; /** 路線取得ID. */ public static final int LINES = 2; /** 駅取得ID. */ public static final int STATIONS = 3; /** Logcat出力用タグ. */ private static final String TAG = ExpressLoader.class.getSimpleName(); /** パラメータにつける名前(エリアor都道府県or路線). */ private String mName; /** * コンストラクタ. * @param context Context * @param name パラメータにつける名前(エリアor都道府県or路線) */ public ExpressLoader(Context context, String name) { super(context); mName = name; forceLoad(); } @Override public ArrayList<HashMap<String, String>> loadInBackground() { ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>(); JSONObject obj = get(mName); if (obj != null) { } return list; } /** * Getリクエストを実行してBodyを取得する. * @param name パラメータにつける名前(エリアor都道府県or路線) * @return JSONObject */ private JSONObject get(String name) { // GET処理 return null; } }
似たような API なのでひとつのクラスですべて実行できるような設計にしたいと思います。どの API を叩くかは id プロパティを使って条件分岐を実装します。その元になるのが一番初めに定義している AREAS や PREFECTURES といった定数になります。
ハイライトで示している mName プロパティは URL パラメータにつける値です。getPrefecture (都道府県一覧取得) であればエリア名が渡され、getLines (路線一覧取得) であれば都道府県名が渡されるようにします。
以上を踏まえて get() メソッドを実装します。またパラメータを作る処理は getParams() メソッドに分けました。
/** * Getリクエストを実行してBodyを取得する. * @param name パラメータにつける名前(エリアor都道府県or路線) * @return JSONObject */ private JSONObject get(String name) { try { String url = "http://express.heartrails.com/api/json?" + getParams(name); HttpClient httpClient = new DefaultHttpClient(); HttpGet get = new HttpGet(url); HttpResponse res = httpClient.execute(get); HttpEntity entity = res.getEntity(); String body = EntityUtils.toString(entity); return new JSONObject(body); } catch (Exception e) { return null; } } /** * URLパラメータを返す. * @param name パラメータにつける名前(エリアor都道府県or路線) * @return URLパラメータ */ private String getParams(String name) { switch (getId()) { case AREAS: return "method=getAreas"; case PREFECTURES: return "method=getPrefectures&area=" + name; case LINES: return "method=getLines&prefecture=" + name; case STATIONS: return "method=getStations&line=" + name; } return null; }
ここまでで API をコールして JSONObject を取得するところまで実装できました。
あとは loadInBackground() で上記メソッドを呼び、ArrayList<HashMap<String, String>> オブジェクトになるように実装していきます。ちょっと長いように感じるかもしれませんが大したことはしていません。API のレスポンスでは、駅データの緯度 (latitude) は y, 経度 (longitude) は x になっているので注意してください。
@Override public ArrayList<HashMap<String, String>> loadInBackground() { ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>(); JSONObject obj = get(mName); if (obj != null) { // "選択してください"アイテムを先頭に追加 HashMap<String, String> notSelected = new HashMap<String, String>(); notSelected.put("name", "選択してください"); list.add(notSelected); try { // JSONからデータを取り出す JSONObject response = obj.getJSONObject("response"); JSONArray array = null; switch (getId()) { case AREAS: array = response.getJSONArray("area"); for (int i = 0; i < array.length(); i++) { HashMap<String, String> station = new HashMap<String, String>(); station.put("name", array.getString(i)); list.add(station); } break; case PREFECTURES: array = response.getJSONArray("prefecture"); for (int i = 0; i < array.length(); i++) { HashMap<String, String> station = new HashMap<String, String>(); station.put("name", array.getString(i)); list.add(station); } break; case LINES: array = response.getJSONArray("line"); for (int i = 0; i < array.length(); i++) { HashMap<String, String> station = new HashMap<String, String>(); station.put("name", array.getString(i)); list.add(station); } break; case STATIONS: array = response.getJSONArray("station"); for (int i = 0; i < array.length(); i++) { JSONObject item = array.getJSONObject(i); HashMap<String, String> station = new HashMap<String, String>(); station.put("name", item.getString("name")); station.put("latitude", item.getString("y")); station.put("longitude", item.getString("x")); list.add(station); } break; } } catch (Exception e) { return null; } } return list; }
これで非同期処理の実装が終わりました。次項から Activity を実装しましょう。
Activity の実装
最後に Activity の実装です。まずレイアウトを適当に作ります。エリア、都道府県、路線、駅を選択する Spinner と GoogleMap アプリに遷移する Button を置きます。
activity_search.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="10dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="エリア" style="@style/Title"/> <Spinner android:id="@+id/areas" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="都道府県" style="@style/Title"/> <Spinner android:id="@+id/prefectures" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="路線" style="@style/Title"/> <Spinner android:id="@+id/lines" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="駅" style="@style/Title"/> <Spinner android:id="@+id/stations" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="View"/> </LinearLayout>
以下のようなレイアウトになります。
次に FragmentActivity を継承した Activity を作ります。Spinner を選択したことをハンドリングする AdapterView.OnItemSelectedListener, GoogleMap を表示するボタンの View.OnClickListener, 非同期タスクのコールバックの LoaderManager.LoaderCallbacks<ArrayList<HashMap<String, String>>> をそれぞれ実装しましょう。
SearchActivity.java
package jp.classmethod.android.sample.searchstation; import android.app.ProgressDialog; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.view.View; import android.widget.AdapterView; import android.widget.SimpleAdapter; import android.widget.Spinner; import android.widget.Toast; import java.util.ArrayList; import java.util.HashMap; /** * 駅を検索するActivity. */ public class SearchActivity extends FragmentActivity implements View.OnClickListener, AdapterView.OnItemSelectedListener, LoaderManager.LoaderCallbacks<ArrayList<HashMap<String, String>>> { /** 自身のインスタンス. */ private final SearchActivity self = this; /** ProgressDialog. */ private ProgressDialog mProgressDialog; /** エリア選択Spinner. */ private Spinner mAreas; /** 都道府県選択Spinner. */ private Spinner mPrefectures; /** 路線選択Spinner. */ private Spinner mLines; /** 駅選択Spinner. */ private Spinner mStations; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); mAreas = (Spinner) findViewById(R.id.areas); mPrefectures = (Spinner) findViewById(R.id.prefectures); mLines = (Spinner) findViewById(R.id.lines); mStations = (Spinner) findViewById(R.id.stations); mAreas.setOnItemSelectedListener(self); mPrefectures.setOnItemSelectedListener(self); mLines.setOnItemSelectedListener(self); findViewById(R.id.view).setOnClickListener(self); getSupportLoaderManager().initLoader(ExpressLoader.AREAS, null, self); } // LoaderManager.LoaderCallbacks @Override public Loader<ArrayList<HashMap<String, String>>> onCreateLoader(int i, Bundle bundle) { } @Override public void onLoadFinished( Loader<ArrayList<HashMap<String, String>>> loader, ArrayList<HashMap<String, String>> list) { } @Override public void onLoaderReset(Loader<ArrayList<HashMap<String, String>>> loader) { } // View.OnClickListener @Override public void onClick(View view) { } // AdapterView.OnItemSelectedListener @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { } @Override public void onNothingSelected(AdapterView<?> adapterView) { } }
onCreate() ではレイアウトからの View の取得とリスナのセット、そしてエリア情報の取得をコールしています。
次に LoaderManager.LoaderCallbacks<ArrayList<HashMap<String, String>>> のメソッド内の処理を実装しましょう。
@Override public Loader<ArrayList<HashMap<String, String>>> onCreateLoader(int i, Bundle bundle) { mProgressDialog = ProgressDialog.show(self, "検索中", "しばらくお待ち下さい..."); String name = bundle == null ? "" : bundle.getString("name"); return new ExpressLoader(self, name); } @Override public void onLoadFinished( Loader<ArrayList<HashMap<String, String>>> loader, ArrayList<HashMap<String, String>> list) { mProgressDialog.dismiss(); if (list == null) { // 何らかの理由により取得できない Toast.makeText(self, "エラー", Toast.LENGTH_SHORT).show(); return; } // 取得したデータを対象のSpinnerにセットする SimpleAdapter adapter = new SimpleAdapter( self, list, android.R.layout.simple_spinner_item, new String[] {"name"}, new int[] {android.R.id.text1}); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); switch (loader.getId()) { case ExpressLoader.AREAS: mAreas.setAdapter(adapter); break; case ExpressLoader.PREFECTURES: mPrefectures.setAdapter(adapter); break; case ExpressLoader.LINES: mLines.setAdapter(adapter); break; case ExpressLoader.STATIONS: mStations.setAdapter(adapter); break; } getSupportLoaderManager().destroyLoader(loader.getId()); } @Override public void onLoaderReset(Loader<ArrayList<HashMap<String, String>>> loader) { }
onLoadFinished() が非同期処理完了後の処理になります。取得してきたデータのリストを SimpleAdapter にセットし loader インスタンスから getId() で取得した ID を元に、どの API のレスポンスか判定し、対象の Spinner にセットします。
次に AdapterView.OnItemSelectedListener を実装しましょう。
@Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { if (i == 0) { // アイテムが選択されていない return; } // アイテムを取得してリクエストにセット HashMap<String, String> item = (HashMap<String, String>) adapterView.getAdapter().getItem(i); Bundle bundle = new Bundle(); bundle.putString("name", item.get("name")); // 実行するメソッドを設定 int loaderId = -1; switch (adapterView.getId()) { case R.id.areas: loaderId = ExpressLoader.PREFECTURES; break; case R.id.prefectures: loaderId = ExpressLoader.LINES; break; case R.id.lines: loaderId = ExpressLoader.STATIONS; break; } getSupportLoaderManager().initLoader(loaderId, bundle, self); } @Override public void onNothingSelected(AdapterView<?> adapterView) { }
onItemSelected() メソッドではまずはじめにインデックスが 0 ではないか判定しています。ExpressLoader で「選択してください」というアイテムを一番初めに入れていたので、0番目のインデックスは未選択なので何もしないようにします。次に adapterView.getId() でどの Spinner が対象なのか判定し getSupportLoaderManager().initLoader() で非同期処理をコールします。例えば「エリアの Spinner が変更されたら指定のエリアの都道府県を取得しにいく」といった感じです。
最後は View.OnClickListener の実装です。
@Override public void onClick(View view) { if (mStations.getSelectedItemPosition() == 0) { // 駅が選択されていない Toast.makeText(self, "駅が選択されていません", Toast.LENGTH_SHORT).show(); return; } // GoogleMapアプリで表示 HashMap<String, String> station = (HashMap<String, String>) mStations.getSelectedItem(); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setClassName("com.google.android.apps.maps","com.google.android.maps.MapsActivity"); intent.setData(Uri.parse("geo:" + station.get("latitude") + "," + station.get("longitude") + "?z=16")); startActivity(intent); }
こちらも Spinner と同様に、まず0番目のアイテムの場合は処理を行わないようにします。1番目以降であれば GoogleMap を表示する Intent を投げます。ここは確実に GoogleMap アプリを開くために setClassName() で Activity のクラス名を指定します。setData() に緯度・経度をセットします。パラメータの最後についている z は拡大率です。16ぐらいが駅を表示する大きさにはちょうど良さそうだったのでこの値にしています。
以上ですべての実装が完了しました!
実行してみる
ではアプリを実行してみましょう。まずメイン画面が表示され、エリア情報が取得されている状態になっていると思います。
ひとつずつ選択していき、すべての項目を埋めます。
最後に「View」ボタンをタップすると GoogleMap アプリが起動し、指定した駅が表示されるはずです!
まとめ
今回は HeartRails Express という API を題材に、サービス連携アプリのひと通りの実装を解説してみました。今回は駅情報の API でしたが巷にはいろいろと便利な情報が取得できる API が公開されているので、その API を使ってあなただけの面白いアプリを作ってみてはいかがでしょうか?
今後も面白そうな API を見つけたらいろいろとアプリを作ってみたいと思います!