LoginSignup
43
44

More than 5 years have passed since last update.

AndroidでModelViewPresenter

Last updated at Posted at 2015-05-24

Model - View - Presenter と Android

  • Viewは, データの出力を行い, ユーザ操作を受け付けPresenterにこれを伝搬するUI層. AndroidではActivity, Fragment, Viewに相当する.
  • Modelは, SQLite3やWebAPI, SharedPreferenceなどのレポジトリ.
  • Presenterは, ModelとViewのブリッジ役を担う. Modelの内容を整形しViewにそれを伝搬する. またViewへの入力を適切なModelに伝搬する.

Androidアプリケーションのプログラミングで厄介な問題の1つにActivity/Fragment/Viewといった固有のライフサイクルをもつオブジェクトでバックグラウンドタスクを管理することが挙げられる.
Model-View-ControllerアーキテクチャやModel-Viewアーキテクチャは, ModelとView/Controllerが関係を持つ. AndroidではController/ViewにあたるActivity, Fragment, Viewがライフサイクルを持っている.
Modelとの通信においてはController/Viewがそのライフサイクルを終え, 不意に破棄, 再生成されることを念頭に設計しなければならない.

MVCは古き良きアーキテクチャであったが, Androidでは開発者が気にしなければならない課題が少なくない.
昨今のアプリケーションのFEPはキーボードやマウス、ジョイスティックコントローラといった類いのものではなくなっている.
マテリアルデザインはリッチなユーザインタフェースを要求し, より複雑・高度化された"display"と"input"はViewとControllerの境界を曖昧にした.
MVCでは無理がある. 無理矢理MVCを適用して, その結果ViewだかControllerだかハッキリしないActivityやFragmentがGod object化してしまうのも無理はない.

Presenterはバックグラウンドタスクを管理する. またPresenterはActivity, Fragment, Viewのライフサイクルから分離させる. これにより前述の厄介な問題を排除し, KISSの原則を促進する.
また, AndroidアプリケーションではしばしばActivityがGod object化する傾向にあるが, Presenterへの適切な責務分担がこれを解消する.

PresenterはFragmentではない. Presenterは前述の通り厄介なライフサイクルからは分離されたオブジェクトである. AndroidにはConfigurationChangeの概念とActivityのRecreationの概念があることを忘れてはいけない. 面倒で推奨もされていないギミックでこれらを避けようとすることはできるが問題を見え辛くしているにすぎない.
FragmentやLoaderの類いはConfigurationChangeの課題を多少解消してくれる(retain-instance)が根本的な解決ではないし, ActivityのRecreationには対応できていない.
またFragmentやLoaderがTestabilityの面で優れていないのも重要なポイントである.

- ConfigChange ActivityRecreate ProcessRestart
Activity, Fragment, View save/restore save/restore save/restore
Fragment.setRetainInstance(true) no change save/restore save/restore
Static variables and threads no change no change reset

PresenterはGod object化しないように適切に責務分割される必要がある(でないと従来のActivityと同じ轍を踏むことになる).
PresenterはFragmentよりも容易に責務分割できるはずだ. なぜならPresenterは厄介なライフサイクルの呪縛から解放されているのだから.

Presenter Example

View

ViewはPresenterへの参照を持ち, Presenterを生成し自身と関連づける.
PresenterのライフサイクルをActivityのそれから切り離すためにPresenterはstaticフィールドとして宣言する.
注意すべきポイントはPresenterのtakeViewメソッドがViewとの関連づけを行う点である.
View, つまりはActivity,Fragment,Viewへの参照がstaticフィールドに保持されるため, 上手く参照を破棄しないとメモリリークを引き起こす.
ライフサイクルが終わりを迎えるタイミングでPresenterに自身の破棄を要求し, かつstaticフィールドの参照をnullに設定する.

public class MainActivity extends ActionBarActivity {

    private static MainPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (presenter == null) {
            presenter = new MainPresenter();
        }
        presenter.takeView(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        presenter.takeView(null);
        if (isFinishing()) {
            presenter = null;
        }
    }

    public void onUpdateView(final CuteModel model) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                TextView v = ((TextView) findViewById(R.id.text));
                v.setText(v.getText() + ", " + model.getValue());
            }
        });
    }
}

Presenter

PresenterはModelとViewへの参照を持つ. Viewへの参照はView自身からtakeView()により関連づけされる. ModelはPresenterが生成し, 適切なタイミングでViewに変更内容を伝搬(updateView)する.
PresenterはViewのライフサイクルに左右されない. PresenterとViewの関係はModelの変更を伝搬する先がいる(view != null)かいない(view == null)かに留まる.
厄介なライフサイクルから解放されたPresenterは非同期ローディングや突発的な外部イベントにも柔軟に対応する.
この例ではCuteModelが非同期にvalueをロードし結果をコールバックしてくるが, "呼び出されたにも関わらずコールバック先が破棄された!"あるいは"コールバックを受け取れる状態ではない"といった不可思議な状態には陥らないし, 無用なキャンセラレーションの実装も必要最小限で済む. もはやAndroidアプリケーションでの非同期ローディングは怖くなくなった.

public class MainPresenter {
    private CuteModel model;
    private MainActivity view;

    public MainPresenter() {
        model = new CuteModel();
        model.query(new Listener() {
            @Override
            public void callback() {
                updateView();
            }
        });
    }

    public void takeView(MainActivity view) {
        this.view = view;
        updateView();
    }

    private void updateView() {
        if (view != null) {
            view.onUpdateView(model);
        }
    }
}

Model

Modelはビジネスロジックに集中できる. Listenerは存在する限り健全な状態であることが保証されている.

public class CuteModel {
    public interface Listener {
        void callback();
    }

    private int value = 0;

    public void query(final Listener listener) {
        Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
            @Override
            public void run() {
                value = 100;
                listener.callback();
            }
        }, 5000, TimeUnit.MILLISECONDS);
    }

    public int getValue() {
        return value;
    }
}

Next step.

必要なアーキテクチャは揃ったが, PresenterとViewの間ではお決まりのやり取りがその数だけ存在する.
コピーコードを避けるためにもここでKISSを促進するツールの導入を考えてみる.
また, Contextオブジェクトに絡むコンポーネントをstatic変数に保持するのにも精神的ストレスであるし, 解放漏れを引き起こしてしまった場合は目も当てられない.

これらを解決するにはMortarとDaggerの選択肢がある.

Writing RecyclerView by Model-View-Presenter

RecyclerViewの実装をMVPアーキテクチャベースで実装する.
MVPの実装を助けるライブラリとしてはmortardaggerを使用する.

MainApp.java

アプリケーションスコープのMortarScopeを提供するためgetSystemServiceをオーバライドする.
このMortarScopeはDaggerのObjectGraphをObjectGraphServiceとしてアプリケーションスコープの粒度でアプリ内に提供する.

public class MainApp extends Application {
  private MortarScope rootScope;

  @Override
  public Object getSystemService(String name) {
    if (rootScope == null) {
      rootScope = MortarScope.buildRootScope()
          .withService(ObjectGraphService.SERVICE_NAME, ObjectGraph.create(new RootModule()))
          .build("Root");
    }

    return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name);
  }
}

IDEの設定によってはgetSystemServiceの引数に渡せる定数を縛っていため警告が表示されるので無効化するか警告のレベルを下げておく.

RootModule.java

今回はDIライブラリとしてDaggerを採用している. Daggerのためにルートモジュールを作成しておく.

@Module(
    injects = MainRecyclerView.class
)
public class RootModule {
  @Provides
  @Singleton
  public MainPresenter provideMainPresenter() {
    return new MainPresenter();
  }
}

MainActivity.java

ActivityはMVPでいうところのViewに位置する. このサンプルではActivityは単なるアクティビティスコープを提供するコンポーネントに過ぎない.
PresenterのためにアクティビティスコープはBundleServiceRunnerを提供する.

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

    // Return the identifier of the task this activity is in. 
    // This identifier will remain the same for the lifetime of the activity.
    // Return Task identifier, an opaque integer.
    String scopeName = getLocalClassName() + "-task-" + getTaskId();
    MortarScope parentScope = MortarScope.getScope(getApplication());
    activityScope = parentScope.findChild(scopeName);
    if (activityScope == null) {
      activityScope = parentScope.buildChild()
          .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner())
          .build(scopeName);
    }
    BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);
  }

  @Override
  public Object getSystemService(String name) {
    return activityScope != null && activityScope.hasService(name) ? activityScope.getService(name)
        : super.getSystemService(name);
  }

MainRecyclerView.java

RecyclerViewを拡張し, Presenterと関連できるMainRecyclerViewを定義する.

public class MainRecyclerView extends RecyclerView {
    @Inject
    MainPresenter presenter;

MainRecyclerViewはMVPでいうViewに位置するため, ViewHolderとそれを更新するメソッドもこのクラスに含めておく.

    static class MainViewHolder extends RecyclerView.ViewHolder {
        private TextView titleTextView;
        private TextView summaryTextView;

        public MainViewHolder(View itemView) {
            super(itemView);
            titleTextView = (TextView) itemView.findViewById(android.R.id.text1);
            summaryTextView = (TextView) itemView.findViewById(android.R.id.text2);
        }

        // called from Presenter.
        public void setText(String title, String summary) {
            titleTextView.setText(title.toUpperCase());
            summaryTextView.setText("-" + summary);
        }
    }

RecyclerView自体のレイアウト定義もこのクラスの責務になる.
setLayoutManagerでのレイアウト指定はコンストラクタで済ませておく.
ただし, AdapterについてはModelとの関連やビジネスロジックを含むためPresenter側に定義する.

    public MainRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);

        // レイアウトの決定はViewの責務.
        this.setLayoutManager(new LinearLayoutManager(context));

        // Modelとの関連づけはPresenterの責務
        // this.setHasFixedSize(false);
        // this.setAdapter(recyclerViewAdapter);

        ObjectGraphService.inject(context, this);
    }

RecyclerViewのリストアイテムを選択した場合のイベントはPresenterに伝える.

    private final OnClickListener itemClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            int position = MainRecyclerView.this.getChildPosition(v);
            // Delegate to Presenter
            presenter.onItemSelected(position);
        }
    };

    public MainViewHolder createViewHolder(ViewGroup parent) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(android.R.layout.simple_list_item_2, parent, false);
        v.setOnClickListener(itemClickListener);
        return new MainViewHolder(v);
    }

あとはMortarでお決まりのコードを書いておく.

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        presenter.takeView(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        presenter.dropView(this);
        super.onDetachedFromWindow();
    }

MainPresenter.java

最後にPresenter. こちらはRecyclerViewのAdapterに相当する責務を書く.
RecyclerViewをセットアップし,

    @Override
    protected void onLoad(Bundle savedInstanceState) {
        MainRecyclerView recyclerView = getView();
        recyclerViewAdapter = new RecyclerViewAdapter(getView());

        // Modelとの関連づけはPresenterの責務
        recyclerView.setHasFixedSize(false);
        recyclerView.setAdapter(recyclerViewAdapter);

        // レイアウトの決定はViewの責務
        // recyclerView.setLayoutManager(new LinearLayoutManager(context));

リストアイテムが選択されたときの処理を記述し,

    public void onItemSelected(int position) {
        Log.i("yuki", "Item Selected! " + datasource.get(position));
    }

Adapterの処理を追加して仕上げる.

    private class RecyclerViewAdapter
            extends RecyclerView.Adapter<MainRecyclerView.MainViewHolder> {
        private MainRecyclerView recyclerView;

        RecyclerViewAdapter(MainRecyclerView recyclerView) {
            this.recyclerView = recyclerView;
        }

        @Override
        public MainRecyclerView.MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return recyclerView.createViewHolder(parent);
        }

        @Override
        public void onBindViewHolder(MainRecyclerView.MainViewHolder view, int position) {
            view.setText(datasource.get(position), datasource.get(position));
        }

        @Override
        public int getItemCount() {
            return datasource.size();
        }
    }

当然ModelとViewへの参照も持つ.

public class MainPresenter extends ViewPresenter<MainRecyclerView> {
    private RecyclerViewAdapter recyclerViewAdapter;
    private List<String> datasource
            = Arrays.asList("data1", "data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9");

完全なコードは下記にある.
MvpWithRecyclerView - GitHub

Reference

43
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
44