[Android] HeartRails Express を使って駅検索アプリを作ってみた

2013.07.03

HeartRails Express という便利な API

今回は HeartRails Express という API を使って、簡単な駅検索アプリを作る方法をご紹介したいと思います。HeartRails Express は路線や駅名データ、駅の地理情報などを XML または JSON (P) 形式で取得できる API です。無料で使うことができます!今回実装するのは以下の機能です。

  1. エリア一覧リストからエリアを選択
  2. 都道府県一覧リストから都道府県を選択
  3. 路線一覧から路線を選択
  4. 駅一覧から駅を選択
  5. GoogleMap アプリで駅の所在地を表示

search_station03

ソースコード

今回実装したアプリのソースコードを GitHub に公開しました。ぜひ参考にしてください。

suwa-yuki/SearchStation

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 プロパティを使って条件分岐を実装します。その元になるのが一番初めに定義している AREASPREFECTURES といった定数になります。
ハイライトで示している 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> オブジェクトになるように実装していきます。ちょっと長いように感じるかもしれませんが大したことはしていません。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>

以下のようなレイアウトになります。

search_station01

次に FragmentActivity を継承した Activity を作ります。Spinner を選択したことをハンドリングする AdapterView.OnItemSelectedListener, GoogleMap を表示するボタンの View.OnClickListener, 非同期タスクのコールバックの LoaderManager.LoaderCallbacks>> をそれぞれ実装しましょう。

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>> のメソッド内の処理を実装しましょう。

@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ぐらいが駅を表示する大きさにはちょうど良さそうだったのでこの値にしています。
以上ですべての実装が完了しました!

実行してみる

ではアプリを実行してみましょう。まずメイン画面が表示され、エリア情報が取得されている状態になっていると思います。

search_station02

ひとつずつ選択していき、すべての項目を埋めます。

search_station03

最後に「View」ボタンをタップすると GoogleMap アプリが起動し、指定した駅が表示されるはずです!

search_station04

まとめ

今回は HeartRails Express という API を題材に、サービス連携アプリのひと通りの実装を解説してみました。今回は駅情報の API でしたが巷にはいろいろと便利な情報が取得できる API が公開されているので、その API を使ってあなただけの面白いアプリを作ってみてはいかがでしょうか?
今後も面白そうな API を見つけたらいろいろとアプリを作ってみたいと思います!

参考