[Android] 外部 API の結果を JSON で受け取って表示する簡単な例


Android 初心者です。新しい言語を学ぶとき、まずは RSSリーダ的なものか、外部APIでデータを取得する何かを作りながら学びたい派なのですが、何故か Android の入門的な書籍には方法が書いてないので、調べながらやってみました。

### 参考にさせて頂いた記事

- [Androidの汎用的な非同期通信クラスできたよー!](http://qiita.com/otoyo/items/70cfa89bfa93ffdf6c9a)
- [AsyncTaskにリスナーを追加してActivityで処理する](http://qiita.com/masahide318/items/97f7fc86ec002593b698)
- [Android AsyncTask with JSON Parsing](http://www.learn2crack.com/2013/10/android-asynctask-json-parsing-example.html)
- [JSONをパース（解析）する](http://techbooster.jpn.org/andriod/application/1645/)

### 今回の環境

> `Android Stuido`：1.0.1
> `JDK`：1.7.0_71
> `compileSdkVersion`：21
> `minSdkVersion`：15
> `targetSdkVersion`：21

## 通信用クラスの作成

AsyncTask を継承してクラスを作成し、コールバック用のインタフェースを実装します。実際に通信している部分は `doInBackground` メソッドです。

```java:AsyncJsonLoader.java
package com.example.hkusu.jsongetsample;

import android.os.AsyncTask;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class AsyncJsonLoader extends AsyncTask<String, Integer, JSONObject> {
    public interface AsyncCallback {
        void preExecute();
        void postExecute(JSONObject result);
        void progressUpdate(int progress);
        void cancel();
    }

    private AsyncCallback mAsyncCallback = null;

    public AsyncJsonLoader(AsyncCallback _asyncCallback) {
        mAsyncCallback = _asyncCallback;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mAsyncCallback.preExecute();
    }

    @Override
    protected void onProgressUpdate(Integer... _progress) {
        super.onProgressUpdate(_progress);
        mAsyncCallback.progressUpdate(_progress[0]);
    }

    @Override
    protected void onPostExecute(JSONObject _result) {
        super.onPostExecute(_result);
        mAsyncCallback.postExecute(_result);
    }

    @Override
    protected void onCancelled() {
        super.onCancelled();
        mAsyncCallback.cancel();
    }

    @Override
    protected JSONObject doInBackground(String... _uri) {
        HttpClient httpClient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet(_uri[0]);
        try {
            HttpResponse httpResponse = httpClient.execute(httpGet);
            if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                httpResponse.getEntity().writeTo(outputStream);
                outputStream.close();
                return new JSONObject(outputStream.toString());
            } else {
                httpResponse.getEntity().getContent().close();
                throw new IOException();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return null;
    }
}
```

## Fragment から呼び出す

今回はイベント開催サービスである [ATND](https://atnd.org/) のAPIを利用し、"Android" に関するイベントのタイトルを ListView に表示することにします。

レイアウトを用意して、

```xml:fragment_main.xml
<ListView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/listView" />
```

Fragment から呼び出します。`asyncJsonLoader.execute(uri)` で通信を指示し、通信が完了するとコールバックで `postExecute` メソッドが呼び出されます。

```java:MainActivity.java
package com.example.hkusu.jsongetsample;

import android.support.v7.app.ActionBarActivity;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new PlaceholderFragment())
                    .commit();
        }
    }

    public static class PlaceholderFragment extends Fragment {
        private final String uri = "http://api.atnd.org/events/?keyword=android&format=json";

        public PlaceholderFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main, container, false);
            return rootView;
        }

        @Override
        public void onStart() {
            super.onStart();
            AsyncJsonLoader asyncJsonLoader = new AsyncJsonLoader(new AsyncJsonLoader.AsyncCallback() {
                // 実行前
                public void preExecute() {
                }
                // 実行後
                public void postExecute(JSONObject result) {
                    if (result == null) {
                        showLoadError(); // エラーメッセージを表示
                        return;
                    }
                    try {
                        // 各 ATND イベントのタイトルを配列へ格納
                        ArrayList<String> list = new ArrayList<>();
                        JSONArray eventArray = result.getJSONArray("events");
                        for (int i = 0; i < eventArray.length(); i++) {
                            JSONObject eventObj = eventArray.getJSONObject(i);
                            JSONObject event = eventObj.getJSONObject("event");
                            //Log.d("title", event.getString("title"));
                            list.add(event.getString("title"));
                        }
                        // ListView 用のアダプタを作成
                        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(
                                getActivity(), android.R.layout.simple_list_item_1, list
                        );
                        // ListView にアダプタをセット
                        ListView listView = (ListView)getActivity().findViewById(R.id.listView);
                        listView.setAdapter(arrayAdapter);
                    } catch (JSONException e) {
                        e.printStackTrace();
                        showLoadError(); // エラーメッセージを表示
                    }
                }
                // 実行中
                public void progressUpdate(int progress) {
                }
                // キャンセル
                public void cancel() {
                }
            });
            // 処理を実行
            asyncJsonLoader.execute(uri);
        }

        // エラーメッセージ表示
        private void showLoadError() {
            Toast toast = Toast.makeText(getActivity(), "データを取得できませんでした。", Toast.LENGTH_SHORT);
            toast.show();
        }
    }
}
```

`AndroidManifest.xml` にて、通信の許諾を追加します。

```diff:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hkusu.jsongetsample" >

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

〜    
```

## 実機で動かしてみる

![Screenshot_2015-02-02-12-18-49.png](https://qiita-image-store.s3.amazonaws.com/0/35733/42a9f2e2-ec30-e52f-9e03-77f0a853127b.png "Screenshot_2015-02-02-12-18-49.png")

## ほか

学習やプロトタイプ的にやるなら上記の方法で良いと思うが、本格的にやるならモデル層を作ったり、HTTP アクセスや JSON パースは何かライブラリを使った方が良さげ。
⇒ [Androidの開発におけるベストプラクティス](https://github.com/futurice/android-best-practices/blob/master/translations/Japanese/README.ja.md)