/var/log/messages

Feb 16, 2018 - 3 minute read - Comments - android

DroidKaigi2018UnitTestHandsOn 実装掘削メモ

とりあえず、AndroidManifest によれば MAIN/LAUNCHER な Activity は AccountInputActivity ということなので、そちら始点で掘削着手。とりあえず開いてみたのが以下です。

  • presentation/view/activity/AccountInputActivity.java
  • presentation/view/activity/BaseActivity.java
  • presentation/view/fragment/AccountInputFragment.java
  • presentation/view/fragment/BaseFragment.java
  • presentation/view/contract/AccountInputContract.java
  • presentation/presenter/AccountInputPresenter.java

UnitTest Hands-on では上記で試験が書かれていたのは presenter 配下のもののみ、でした。確かに確認してみるに

  • BaseActivity では replaceFragment というメソッド定義のみ(引数で指定された Fragment を表示)
  • AccountInputActivity では onCreate の定義のみ(スーパークラスのメソッド呼び出し、setContentView、replaceFragment の呼び出しのみ)

あるいは Fragment および Contract についても試験が不要な作りになっているものと思われますが、BaseFragment から確認をすすめていきます。

BaseFragment

若干果てしない感があるのですが確認していきます。

先ず、クラス定義の先頭あたりが以下。

public abstract class BaseFragment<V extends BaseView, P extends BasePresenter<V>> extends Fragment {
  protected P presenter;

  private Unbinder unbinder;

  @NonNull
  protected abstract V getViewContract();

  @NonNull
  protected abstract P getPresenter();

  @LayoutRes
  protected abstract int getLayoutRes();

  @NonNull
  protected abstract Unbinder bindView(@NonNull View fragmentView);

属性二点とサブクラスで定義が強制されるメソッドの定義です。というかこれ、BaseView と BasePresenter の定義も確認したほうが良いのかな。先ず presenter/view/BaseView.java から。これは interface ですね。

public interface BaseView {
  void startActivity(@NonNull Intent intent);

  void startActivityForResult(@NonNull Intent intent, int requestCode);

  void finishActivity();

  @Nullable
  Context getContext();

  @Nullable
  FragmentActivity getActivity();
}

先走りますが、AccountInputContract.View はこれを継承してもう一つメソッド定義、になっています。

public interface AccountInputContract {
  interface View extends BaseView {
    void showInputError(@StringRes int errorRes);
  }

で、AccountInputFragment がこの AccountInputContrace.View を実装、という形になっています。ということは AccountInputFragmentBaseView を実装している、ということでもあるようです。次は BasePresenter を確認します。少しづつすすめます。

public abstract class BasePresenter<V extends BaseView> {
  protected V view;

  public void setView(V view) {
    this.view = view;
  }

こちらはいつか実装が記述されていることで abstract なクラス定義になっているようです。ちなみに AccountInputPresenter の定義を少しだけ確認してみるに

public class AccountInputPresenter extends BasePresenter<AccountInputContract.View> implements AccountInputContract.Interaction {

という書き方になっています。先に確認したとおり、AccountInputFragmentAccountInputContract.View です。ということは Presenter は Fragment を「保持」してる、ということになるようです。持たしてあげるメソッドが setView なのだろうと思われます (別途確認)。

BasePresenter の定義の残りを以下に引用します。

  public void onAttach() {
    // NOP
  }

  public void onViewCreated() {
    // NOP
  }

  public void onResume() {
    // NOP
  }

  public void onPause() {
    // NOP
  }

  public void onDestroyView() {
    // NOP
  }

  public void onDetach() {
    // NOP
  }

  public void startActivity(@NonNull Intent intent) {
    view.startActivity(intent);
  }

  public void startActivityForResult(@NonNull Intent intent, int requestCode) {
    view.startActivityForResult(intent, requestCode);
  }

  public void finishActivity() {
    view.finishActivity();
  }

  @Nullable
  public Context getContext() {
    return view.getContext();
  }

  @Nullable
  public FragmentActivity getActivity() {
    return view.getActivity();
  }
}

startActivity 以降では BaseView にて実装が強制されたメソッドを呼び出すまたは呼び出して戻り値を戻す形になっています。繰り返しになりますが、このアーキテクチャでは BaseView を Fragment が実装している、というのがポイントです。

BaseFragment の定義の確認に戻ります。onAttach の定義が以下です。

  @Override
  public void onAttach(Context context) {
    super.onAttach(context);

    presenter = getPresenter();
    presenter.onAttach();
    presenter.setView(getViewContract());
  }

ここで関連付けが行われているのがわかります。 Activity に attach された時点で

  • Presenter を取得して属性に設定
  • Presenter#onAttach を呼び出し
  • presenter の view 属性に getViewContract() の戻り値(基本的には Fragment のインスタンス参照)を設定

という形になっています。以降の定義を以下に引用します。

  @Nullable
  @Override
  public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return inflater.inflate(getLayoutRes(), container, false);
  }

  @Override
  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    unbinder = bindView(view);
    presenter.onViewCreated();
  }

  @Override
  public void onResume() {
    super.onResume();
    BusProvider.INSTANCE.get().register(presenter);
    presenter.onResume();
  }

  @Override
  public void onPause() {
    presenter.onPause();
    BusProvider.INSTANCE.get().unregister(presenter);
    super.onPause();
  }

  @Override
  public void onDestroyView() {
    presenter.onDestroyView();
    unbinder.unbind();
    super.onDestroyView();
  }

  @Override
  public void onDetach() {
    presenter.onDetach();
    super.onDetach();
  }

  public void finishActivity() {
    ofNullable(getActivity()).ifPresent(Activity::finish);
  }
}

スーパークラス側で layout の inflate を行っています。getLayoutRes メソッドで正しい ID を戻すように実装しなければ、ということになります。また、一部不明な部分がありますが、状態遷移時の callback において Presenter のメソッドが呼び出されています。基本的にはここトリガで様々な処理を行っていく形になるはずです。

以下、要確認事項を列挙しておきます。

  • onViewCreated にて呼び出されている bindView について (unbinder 属性に戻り値設定)
  • onResume にて呼び出されている BusProvider のメソッドについて
  • onPause にて呼び出されている BusProvider のメソッドについて
  • onDestroyView でも unbinder 属性が使われている

Unit Test の Hands-on 資料まとめ Hugo-Octopress テーマ修正

comments powered by Disqus