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の実装を助けるライブラリとしてはmortarとdaggerを使用する.
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