[Android]AsyncTaskLoaderを使い複数通信を実装する


※2017年6月追記
GoogleからAndroidアプリの設計について、いい資料がでています。
https://developer.android.com/topic/libraries/architecture/guide.html
この記事にあるとおり、Retrofit等を使ったほうがモダンな実装ができそうです。


AsyncTaskLoaderを使い複数の通信を実装する方法は沢山あると思います。

- Androidの通信まわりの実装について考える [メモ]

http://qiita.com/ux_design_tokyo/items/6a8e4202925e43e1101f

ここでは上記のリンク先の「4」で実装します。

- 通信クラスを作成
- ActivityまたはFragmentではなく通信クラスをAsyncTaskLoaderのコールバック先にする
- 1つの通信につき1つのメソッドを用意。戻り値は各通信のメソッド毎にインターフェースを用意
- JSONのパース処理は別のクラスにまとめる(処理が短く分ける必要がない時は通信クラスに書いてもいいと思います)
- LoaderManagerがキャッシュしているloaderは毎回削除しています。処理的には少し無駄がありそうです。

ここでは例としてYoutubeのリストとデータを取得する処理を行っています。

## 呼び出し側

~~~java:HogeActivity

connection.searchYoutube(this, this.searchWord, resultList.size() + 1, new  HogeConnection.SearchYoutubeListener() {

    @Override
    public void didFinish(final int err, final List<YoutubeVideoData> resultList) {

    // 通信完了時の処理
});

~~~

## 通信クラス

~~~java:HogeConnection

public class HogeConnection {
    // 通信成功
    public final static int CONNECTION_SUCCESS = 1;
    // 通信エラー
    public final static int CONNECTION_ERROR = CONNECTION_SUCCESS + 1;

    private int loaderId; // 注意 JsonLoader以外を使う場合はインスタンスに1つのIDではなくLoaderの種類毎にIDを分ける必要がある

    private LoaderManager manager;

    private  Activity activity;


    public interface SearchYoutubeListener {
        public void didFinish(final int err, final List<YoutubeVideoData> resultList);
    }

    public interface GetYoutubeVideoDataListener {
        public void didFinish(final int err, final YoutubeVideoData data);
    }

    public  UtasumaConnection(Activity activity) {
        this.activity = activity;
        this.manager = activity.getLoaderManager();
        this.loaderId = hashCode();
    }

    private JsonLoader createAndLoadJsonLoader(String urlText) {
        JsonLoader jsonLoader = new JsonLoader(activity, urlText);
        jsonLoader.forceLoad();
        loader = jsonLoader;
        return  jsonLoader;
    }

    /**
     * 検索ワードからYoutubeの動画を検索します。
     * @param searchWord 検索ワード
     * @param index 検索開始位置
     * @param searchYoutubeListener
     */
    public void searchYoutube(String searchWord, final int index, final SearchYoutubeListener searchYoutubeListener) {
        final String urlText = "http://gdata.youtube.com/feeds/api/videos?q="
                + Util.createParameter(searchWord)
                + "&start-index=" + index
                + "&max-results=10&v=2&alt=json";

        // 既にローダーがある場合は破棄
        if (manager.getLoader(loaderId) != null) {
            manager.destroyLoader(loaderId);
        }

        manager.initLoader(loaderId, null, new LoaderManager.LoaderCallbacks<JSONObject>() {
            @Override
            public Loader<JSONObject> onCreateLoader(int i, Bundle bundle) {
                return createAndLoadJsonLoader(activity, urlText);
            }

            @Override
            public void onLoadFinished(Loader<JSONObject> objectLoader, JSONObject jsonObject) {
                if (jsonObject != null) {
                    List<YoutubeVideoData> resultList =  HogeJsonAnalytics.analysisYoutubeSearchResultList(jsonObject);
                    searchYoutubeListener.didFinish(CONNECTION_SUCCESS, resultList);
                }
                // エラー時処理
                else {
                    searchYoutubeListener.didFinish(CONNECTION_ERROR, null);
                }
            }

            @Override
            public void onLoaderReset(Loader<JSONObject> objectLoader) {
                searchYoutubeListener.didFinish(CONNECTION_SUCCESS, new ArrayList<YoutubeVideoData>());
            }
        });
    }



    /**
     * Youtube動画を検索します。
     * @param videoId 検索する動画ID
     * @param listener
     */
    public void getYoutubeVideoData(final String videoId, final GetYoutubeVideoDataListener listener) {

        final String urlText = "http://gdata.youtube.com/feeds/api/videos/" + videoId + "?alt=json";

        // 既にローダーがある場合は破棄
        if (manager.getLoader(loaderId) != null) {
            manager.destroyLoader(loaderId);
        }

        manager.initLoader(loaderId, null, new LoaderManager.LoaderCallbacks<JSONObject>() {
            @Override
            public Loader<JSONObject> onCreateLoader(int i, Bundle bundle) {
                return createAndLoadJsonLoader(activity, urlText);
            }

            @Override
            public void onLoadFinished(Loader<JSONObject> objectLoader, JSONObject jsonObject) {
                if (jsonObject != null) {
                    YoutubeVideoData youtubeVideoData =  HogeJsonAnalytics.analysisYoutubeSearchResult(jsonObject);
                    listener.didFinish(CONNECTION_SUCCESS, youtubeVideoData);
                }
                // エラー時処理
                else {
                    listener.didFinish(CONNECTION_ERROR, null);
                }
            }

            @Override
            public void onLoaderReset(Loader<JSONObject> objectLoader) {
                listener.didFinish(CONNECTION_SUCCESS, null);
            }
        });
    }

    public void cancel() {       
        if (loader != null) {
            loader.cancelLoad();
        }
    }

    public void destroy() {
        manager.destroyLoader(loaderId);
    }
}
~~~

## AsyncTaskLoaderを使いJSONを読み込むクラス

~~~java:JsonLoader

public class JsonLoader extends AsyncTaskLoader<JSONObject> {

    private String urlText;

    public JsonLoader(Context context, String urlText) {
        super(context);
        this.urlText = urlText;
    }

    @Override
    public JSONObject loadInBackground() {
        HttpClient httpClient = new DefaultHttpClient();

        StringBuilder uri = new StringBuilder(urlText);
        HttpGet request = new HttpGet(uri.toString());
        HttpResponse httpResponse = null;

        try {
            httpResponse = httpClient.execute(request);
        } catch (Exception e) {
            Log.d("JsonLoader", "Error Execute");
            return null;
        }

        int status = httpResponse.getStatusLine().getStatusCode();

        if (HttpStatus.SC_OK == status) {
            try {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                httpResponse.getEntity().writeTo(outputStream);
                String data;
                data = outputStream.toString(); // JSONデータ

                JSONObject rootObject = new JSONObject(data);

                return rootObject;

            } catch (Exception e) {
                Log.d("JsonLoader", "Error");
            }
        } else {
            Log.d("JsonLoader", "Status" + status);
            return null;
        }
        return null;
    }
}

~~~

## Jsonを欲しいObjectに変換してくれるクラス

~~~java:HogeJsonAnalytics

public class HogeJsonAnalytics {
    /**
     * JSON解析用タグ
     */
    final static private String KEY_FEED = "feed";
    final static private String KEY_ENTRY = "entry";
    final static private String KEY_MEDIA_GROUP = "media$group";
    final static private String KEY_VIDEO_ID = "yt$videoid";
    final static private String KEY_TITLE = "title";
    final static private String KEY_DURATION = "yt$duration";
    final static private String KEY_SECONDS = "seconds";
    final static private String KEY_THUMBNAIL = "media$thumbnail";
    final static private String KEY_URL = "url";
    final static private String KEY_COUNT_INFO = "yt$statistics";
    final static private String KEY_VIEW_COUNT = "viewCount";
    final static private String KEY_$T = "$t";
    final static private String KEY_RAITING_INFO = "yt$rating";
    final static private String KEY_RAITING_LIKE = "numLikes";
    final static private String KEY_RAITING_DISLIKE = "numDislikes";

    static final private String KEY_RANKING_LIST = "rankingList";

    /**
     * Youtube Data api レスポンスの「entry」の解析を行います。
     * @param item
     * @return
     */
    static private YoutubeVideoData analysisEntry(JSONObject item) {

        String title = null;
        String videoId = null;
        String thumbnailUrl = null;
        int seconds = 0;
        int likeCount = 0;
        int dislikeCount = 0;
        int viewCount = 0;


        JSONObject mediaGroup = item.optJSONObject(KEY_MEDIA_GROUP);
        if (mediaGroup != null) {
            JSONObject videoIdObject = mediaGroup.optJSONObject(KEY_VIDEO_ID);
            if (videoIdObject != null) {
                videoId = videoIdObject.optString(KEY_$T);
            }

            // サムネイルURL 5つあるうちのはじめのデータを使用
            JSONArray thumbnailArray = mediaGroup.optJSONArray(KEY_THUMBNAIL);
            if (thumbnailArray != null && 0 < thumbnailArray.length()) {
                JSONObject thumbnailInfo = thumbnailArray.optJSONObject(0);
                if (thumbnailInfo != null) {
                    thumbnailUrl = thumbnailInfo.optString(KEY_URL);
                }
            }

            // 再生時間
            final JSONObject durationObject = mediaGroup.optJSONObject(KEY_DURATION);
            if (durationObject != null) {
                seconds = durationObject.optInt(KEY_SECONDS);
            }
        }


        // タイトル
        final JSONObject titleObject = item.optJSONObject(KEY_TITLE);
        if (titleObject != null) {
            title = titleObject.optString(KEY_$T);
        }


        // ビュー数
        JSONObject viewInfo = item.optJSONObject(KEY_COUNT_INFO);
        if (viewInfo != null) {
            viewCount = viewInfo.optInt(KEY_VIEW_COUNT);
        }

        // いいね、よくない情報
        JSONObject ratingInfo = item.optJSONObject(KEY_RAITING_INFO);
        if (ratingInfo != null) {
            likeCount = ratingInfo.optInt(KEY_RAITING_LIKE);
            dislikeCount = ratingInfo.optInt(KEY_RAITING_DISLIKE);
        }

        YoutubeVideoData entry = new YoutubeVideoData(videoId, title,
                thumbnailUrl, seconds, viewCount, likeCount, dislikeCount);

        return entry;
    }

    /**
     * 動画情報の解析を行います。
     * @param result
     */
    static public YoutubeVideoData analysisYoutubeSearchResult(JSONObject result) {

        if (result == null) {
            return null;
        }

        JSONObject entry = result.optJSONObject(KEY_ENTRY);

        if (entry == null || entry.length() == 0) {
            return null;
        }

        return analysisEntry(entry);
    }

    /**
     * Youtuebeの検索結果を解析します。
     * @param result
     */
    static public List<YoutubeVideoData> analysisYoutubeSearchResultList(JSONObject result) {

        // Youtube検索結果用リスト
        List<YoutubeVideoData> resultList = new ArrayList<YoutubeVideoData>();

        if (result == null) {
            return resultList;
        }

        JSONObject feed = result.optJSONObject(KEY_FEED);

        if (feed == null) {
            return resultList;
        }

        JSONArray entry = feed.optJSONArray(KEY_ENTRY);

        if (entry == null || entry.length() == 0) {
            return resultList;
        }


        for (int i = 0; i < entry.length(); i++) {

            final JSONObject item = entry.optJSONObject(i);
            if (item == null) {
                continue;
            }

            resultList.add(analysisEntry(item));
        }

        return resultList;
    }
}


~~~

## Utilクラス

~~~java:Util

public class Util {

    /**
     * 検索パラメータの区切り文字：全角スペース
     */
    final static private char WSPACE = '\u3000';

    /**
     * 検索パラメータの区切り文字：半角スペース
     */
    final static private char SPACE = '\u0020';

    /**
     * 検索パラメータを生成します。
     * @param searchWord
     * @return
     */
    public static String createParameter(String searchWord) {
        if (searchWord == null || "".equals(searchWord)) {
            // TODO
            return null;
        }

        String replaceWord = searchWord.replaceAll(String.valueOf(WSPACE), String.valueOf(SPACE));
        String[] searchWords = replaceWord.split(String.valueOf(SPACE));

        StringBuilder parameter = new StringBuilder();

        for (String word : searchWords) {
            parameter.append(word + "+");
        }
        return parameter.toString();
    }
}

~~~

## まとめ
今回はこの方法で実装しましたが、実装を進めてバグやもっといい方法が見つかるかもしれません。