読者です 読者をやめる 読者になる 読者になる

もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

Androidのデザインパターンを考えてみた

Android API ListView デザインパターン

一番最新のやつはこっち

rei19.hatenablog.com

前提

新しいトライとしてAndroidを書き始めてだいたい半年くらいたったのですが、教科書通りにコードを書いていたもののAPIを呼ぶあたりの実装がどうにもしっくりこなくかったので、良い機会だと思い設計を見直す事にしました。 以下、めっちゃ参考にしたスライド。ありがたや。

iOS/Androidアプリエンジニアが理解すべき「Model」の振る舞い

ポイント

  • 基本的にModelは死なない + 通知で変更を伝える役割を持つ
  • ViewやVCでAPIを呼ぶのはユーザーの操作に影響されやすいからやめれ

作ってみた

以前、このポストでAtndのAPIを呼んで結果をListViewに表示してみるというのを勉強のアウトプットとして残したのですが、今回はこれを上記のポイントを踏まえた上でObserverパターンで書き直してみようと思います。

ざっと概要

  • ModelLocator・・・上のスライドにならい同じ名前にしてみる。シングルトンでアプリの起動時にModelのインスタンスを格納して保持しておく
  • Fragment ・・・ 観察者。Observerを実装してModelからの通知をキャッチする
  • Model・・・AtndのApiを管理する。ObservableとLoaderCallbacksを実装して「APIの呼び出し」と「イベントをObserverに通知」という役割を担う
  • Entity・・・AtndのEvent情報Entity。今回はIDとタイトルだけ持つ。てきとー

コード

  • ModelLocator
package me.rei_m.androidsample.model;

/**
 * Created by rei_m on 2015/01/25.
 */
public class ModelLocator {

    private final static ModelLocator instance = new ModelLocator();

    public static ModelLocator getInstance() {return instance;}

    private ModelLocator() {}

    private AtndApi atndApi;

    public AtndApi getAtndApi() {
        return atndApi;
    }

    public void setAtndApi(AtndApi atndApi) {
        this.atndApi = atndApi;
    }
}

アプリ起動時にAtndApiのモデルをセットしておく

// モデルの初期化
ModelLocator.getInstance().setAtndApi(AtndApi.createInstance());
  • AtndApi
package me.rei_m.androidsample.model;

import android.app.LoaderManager;
import android.content.Context;
import android.content.Loader;
import android.os.Bundle;

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

import java.util.ArrayList;
import java.util.List;
import java.util.Observable;

import me.rei_m.androidsample.entity.AtndEvent;
import me.rei_m.androidsample.util.HttpAsyncLoader;

/**
 * Atnd APIを管理するモデル.
 * APIにリクエストを飛ばし、取得した結果をリストで保持して、取得完了のイベントを通知する
 *
 * Created by rei_m on 2015/01/25.
 */
public class AtndApi extends Observable
        implements LoaderManager.LoaderCallbacks<String>{

    public static AtndApi createInstance(){

        return new AtndApi();
    }

    private final int LOADER_ID = 1;

    private AtndApi(){}

    private Context mContext;
    private Loader mLoader;

    private List<AtndEvent> mList;

    public List<AtndEvent> getList(){
        return mList;
    }

    /**
     * Loaderの初期化を行い、APIへリクエストを飛ばす
     *
     * @param context Context
     * @param lm LoaderManager
     */
    public void fetchList(Context context, LoaderManager lm){
        mList = new ArrayList<>();
        mContext = context;
        mLoader = lm.initLoader(LOADER_ID, null, this);
        mLoader.forceLoad();
    }

    @Override
    public Loader<String> onCreateLoader(int id, Bundle args) {
        HttpAsyncLoader loader = new HttpAsyncLoader(mContext,
                "https://api.atnd.org/events/?keyword_or=google,cloud&format=json");
        loader.forceLoad();
        return loader;
    }

    @Override
    public void onLoadFinished(Loader<String> loader, String data) {

        if(loader.getId() == LOADER_ID){
            try {
                // APIの取得結果をEventオブジェクトに変換して格納する
                JSONObject json = new JSONObject(data);
                int evCnt = json.getInt("results_returned");
                if(evCnt > 0){
                    JSONArray events = json.getJSONArray("events");
                    for(int i=0;i<evCnt;i++){
                        JSONObject ev = events.getJSONObject(i).getJSONObject("event");
                        AtndEvent atndEvent = new AtndEvent();
                        atndEvent.setId(ev.getString("event_id"));
                        atndEvent.setTitle(ev.getString("title"));
                        mList.add(atndEvent);
                    }
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        // Observerに更新を通知する
        LoaderEvent event = new LoaderEvent(this);
        setChanged();
        notifyObservers(event);
    }

    @Override
    public void onLoaderReset(Loader<String> loader) {

    }
}
  • AtndEvent
package me.rei_m.androidsample.entity;

import java.io.Serializable;

/**
 * Created by rei_m on 2015/01/25.
 */
public class AtndEvent implements Serializable {

    private String id;
    private String title;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return title;
    }
}
  • Fragment
package me.rei_m.androidsample.fragment;

import android.app.Activity;
import android.os.Bundle;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;

import java.util.Observable;
import java.util.Observer;

import me.rei_m.androidsample.R;

import me.rei_m.androidsample.entity.AtndEvent;
import me.rei_m.androidsample.model.AtndApi;
import me.rei_m.androidsample.model.LoaderEvent;
import me.rei_m.androidsample.model.ModelLocator;

public class ObserverSampleFragment extends Fragment
        implements AbsListView.OnItemClickListener, Observer{

    private OnFragmentInteractionListener mListener;
    private ArrayAdapter<AtndEvent> mAdapter;

    /**
     * AtndAPIモデル
     */
    private AtndApi mAtndApi;

    /**
     * ファクトリメソッド
     *
     * @return インスタンス
     */
    public static ObserverSampleFragment newInstance() {
        ObserverSampleFragment fragment = new ObserverSampleFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    /**
     * Mandatory empty constructor for the fragment manager to instantiate the
     * fragment (e.g. upon screen orientation changes).
     */
    public ObserverSampleFragment() {
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnFragmentInteractionListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mAdapter = new ArrayAdapter<>(getActivity(),
                android.R.layout.simple_list_item_1, android.R.id.text1);

        // ModelLocatorからAtndAPIモデルを取得してフラグメントをObserverとして登録する
        mAtndApi = ModelLocator.getInstance().getAtndApi();
        mAtndApi.addObserver(this);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_item, container, false);

        AbsListView listView = (AbsListView) view.findViewById(android.R.id.list);
        listView.setAdapter(mAdapter);
        listView.setOnItemClickListener(this);

        // AtndAPIモデルから取得結果を取り出し、値が存在しない場合はリクエストを投げる
        // 実際にはAPIのリクエストパラメータとかは変わるはずなので、実用を考えたらもうちょい厳密な判定になるはずだけど。
        if(mAtndApi.getList() == null){
            mAtndApi.fetchList(view.getContext(), getLoaderManager());
        }else{
            mAdapter.addAll(mAtndApi.getList());
            mAdapter.notifyDataSetChanged();
        }

        return view;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // AtndAPIモデルからフラグメントをObserverから削除する
        mAtndApi.deleteObserver(this);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
        mAtndApi = null;
    }


    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (null != mListener) {
            // Notify the active callbacks interface (the activity, if the
            // fragment is attached to one) that an item has been selected.
            mListener.onFragmentInteraction(position);
        }
    }

    @Override
    public void update(Observable observable, Object data) {
        if (data instanceof LoaderEvent){
            // AtndAPIモデルから通知を受け取ったらモデルからAPIの取得結果を取り出して、ListViewにセットして、Viewを更新
            AtndApi atndApi = (AtndApi) ((LoaderEvent) data).getSource();
            mAdapter.addAll(atndApi.getList());
            mAdapter.notifyDataSetChanged();
        }
    }

    /**
     * This interface must be implemented by activities that contain this
     * fragment to allow an interaction in this fragment to be communicated
     * to the activity and potentially other fragments contained in that
     * activity.
     * <p/>
     * See the Android Training lesson <a href=
     * "http://developer.android.com/training/basics/fragments/communicating.html"
     * >Communicating with Other Fragments</a> for more information.
     */
    public interface OnFragmentInteractionListener {
        // TODO: Update argument type and name
        public void onFragmentInteraction(int id);
    }
}

だいたいこんな感じでしょうかー。

実行してみる

こちらを叩いてます(https://api.atnd.org/events/?keyword_or=google,cloud&format=json

f:id:Rei19:20150125224649p:plain

でけたー。コードはGitHubに上がってます(https://github.com/rei-m/androidSample) 普段はJava専門て訳でもないのでおかしなところがあれば教えて下さい!

続き

rei19.hatenablog.com