2015/05/24

RecyclerViewをModel-View-Presenterで書く

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");

以上.