/var/log/messages

Feb 19, 2018 - 7 minute read - Comments - android

DroidKaigi2018UnitTestHandsOn 実装掘削メモ

要確認事項について

以下、順に確認してみます。

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

先ず bindView という abstract なメソッドについて。サブクラスの AccountInputFragment の定義を見てみるに以下。

  @NonNull
  @Override
  protected Unbinder bindView(@NonNull View fragmentView) {
    return ButterKnife.bind(this, fragmentView);
  }

Butterknife って View Injection なライブラリらしい。確かに以下な記述がありますね。これで紐をつけてくれる模様。

  @BindView(R.id.account)
  EditText accountView;

次は BusProvider について。これは例えば Activity - Fragment 間でのメッセージのやりとり?なんかに使えるらしい。BaseFragment では以下な形で register/unregister しています。

  @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();
  }

presenter が登録されている、ということは Fragment - Presenter なやりとりなのかどうか。

あと、onDestroy で云々、な以下の件ですが

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

最後で解放はしておこうね、という理解にとどめておきます。

再度全体整理?

以下に列挙しておきます。

  • Fragment は BaseFragment を継承
  • BaseFragment は BasePresenter を保持
  • Fragment は BaseView を実装して getViewContract にて自分を戻す
  • Framgment と Presenter はお互いの参照を保持している形

ここまで確認できている状態で AccoutInputFragment に関する定義の周辺を確認します。

AccountInput

以下を確認の方向です。

  • presentation/fragnment/AccountInputFragment.java
  • presentation/contract/AccountInputContract.java
  • presentation/presenter/AccountInputPresenter.java

もしかすると Activity のあたりまで確認が必要かもしれません。とりあえず、AccountInputFragment については簡易な実装なのでポイントを以下に列挙するにとどめます。

  • @BindView なアノテーション (id が指定されている) が付いた EditText 属性
  • public static な newInstance メソッド
  • AccountInputCntract.View (BaseView 継承) を戻す getViewContract メソッド
  • 実は AccountInputFragment は AccountInputContract.View を実装していて getViewContract では this を戻します
  • getPresenter では毎回 new でオブジェクト生成して戻しています
  • レイアウト XML の ID を戻す getLayoutRes メソッド
  • ButterKnife.bind の戻り値を戻す bindView メソッド
  • showInputError も実装しています

bindView はこっちで実装しなくても、という気はしますが、基本的に固有の記述、という形になっています。また、AcountInputContract の定義が以下です。

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

  interface Interaction {
    void onClickViewRepositoryButton(@Nullable String input);
  }
}

おそらく、Interaction は presenter 側で実装しているのではないかと思われます。これから確認します。

AccountInputPresenter の定義を以下に引用します。

public class AccountInputPresenter extends BasePresenter<AccountInputContract.View> implements AccountInputContract.Interaction {
  @NonNull
  private final AccountValidator accountValidator;

  public AccountInputPresenter(@NonNull AccountValidator accountValidator) {
    this.accountValidator = accountValidator;
  }

  @Override
  public void onClickViewRepositoryButton(@Nullable String input) {
    AccountValidator.Result result = accountValidator.validate(input);
    if (!result.isValid()) {
      view.showInputError(result.getReasonRes());
      return;
    }
    assert input != null;
    startRepoListScreen(input);
  }

  void startRepoListScreen(@NonNull String account) {
    ofNullable(getActivity()).ifPresent(context -> startActivity(RepoListActivity.newIntent(context, account)));
  }
}

一つ一つが簡潔で読みやすいです (仕組みがわかってさえいれば)。以下にポイントを列挙します。

  • BasePresenter を継承
  • AccountInputContract.Interaction を実装
  • AccountValidator 型の属性を保持 (コンストラクタの引数でオブジェクト取得するので @NonNull なアノテーション付き)
  • onClickViewRepositoryButton 実装 (accountInputContract.Interaction 実装による)

AccountValidator も確認します。domain/usecase/AccountValidator.java です。実装以下です。これもつくり的に非常に面白いです。

public class AccountValidator {
  public Result validate(@Nullable String account) {
    if (TextUtils.isEmpty(account)) {
      return new Result(false, R.string.error_field_required);
    }
    if (!isValid(account)) {
      return new Result(false, R.string.error_invalid_account);
    }
    return new Result(true, 0);
  }

  private boolean isValid(@NonNull String account) {
    return account.length() > 0 && account.matches("[a-zA-Z0-9]+");
  }

  public enum Factory implements Singleton<AccountValidator> {
    INSTANCE;

    private final AccountValidator instance = new AccountValidator();

    @NonNull
    @Override
    public AccountValidator get() {
      return instance;
    }
  }

  public static class Result {
    private final boolean isValid;

    @StringRes
    private final int reasonRes;

    public Result(boolean isValid, int reasonRes) {
      this.isValid = isValid;
      this.reasonRes = reasonRes;
    }

    public boolean isValid() {
      return isValid;
    }

    public int getReasonRes() {
      return reasonRes;
    }
  }
}

こんな形になっていれば試験作るのも楽そうですね。Activity ですが、以下な定義になっています。こちらも簡潔。

public class AccountInputActivity extends BaseActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.container);
    replaceFragment(AccountInputFragment.newInstance(), R.id.container);
  }

}

replaceFragment は BaseActivity で定義されてます。

public abstract class BaseActivity extends AppCompatActivity {

  protected final void replaceFragment(@NonNull Fragment fragment, @IdRes int containerId) {
    final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    ft.replace(containerId, fragment, fragment.getClass().getSimpleName());
    ft.commit();
  }

}

これで AccountInput 関連は確認できているはずなので、もう一つの RepoList な画面を確認してみます。以下、なのかどうか。

  • presentation/activity/RepoListActivity.java
  • presentation/adapter/RepoListAdapter.java
  • presentation/contract/RepoListContract.java
  • presentation/fragment/RepoListFragment.java

RepoListActivity

viewmodel とか mapper というディレクトリに Repo ソレが見えますがとりあえずスルーで。おそらくこちらは色々やってるはずなので上記以外への広がりはあると思っています。今回は上から確認すすめます。まず RepoListActivity から以下に引用。

public class RepoListActivity extends BaseActivity {
  public enum BundleKey {
    ACCOUNT
  }

  @NonNull
  public static Intent newIntent(@NonNull Context context, @NonNull String account) {
    Intent intent = new Intent(context, RepoListActivity.class);
    intent.putExtra(BundleKey.ACCOUNT.name(), account);
    return intent;
  }

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

    String account = getIntent().getStringExtra(BundleKey.ACCOUNT.name());
    replaceFragment(RepoListFragment.newInstance(account), R.id.container);
  }

}

以下にポイント列挙します。

  • 次画面遷移時に使用されている newIntent メソッド
  • BundleKey という enum な属性の使い方も、へぇぇ、となってしまいました
  • onCreate では RepoListFragment 生成時にアカウント文字列を渡しているところが AccountInput との違い

RepoListFragment

Fragment も確認します。こちらは少々ボリューム大きめになっています。ので、少しづつ引用しつつで確認します。

public class RepoListFragment extends BaseFragment<RepoListContract.View, RepoListPresenter> implements RepoListContract.View {
  @BindView(R.id.progress)
  View progressBar;

  @BindView(R.id.list)
  RecyclerView recyclerView;

  @BindView(R.id.swipe_refresh)
  SwipeRefreshLayout swipeRefreshLayout;

  private String account;

  private RepoListAdapter adapter;
  • BaseFragment を継承
  • RepoListContrace.View を実装
  • 三点ほど injection する View が定義されています
  • アカウント文字列、リストアダプタを属性として保持

リストが絡んでくるとどんな実装になるのかな、と思いつつ続きを確認。

public static RepoListFragment newInstance(@NonNull String account) {
    Bundle args = new Bundle();
    args.putString(RepoListActivity.BundleKey.ACCOUNT.name(), account);
    RepoListFragment fragment = new RepoListFragment();
    fragment.setArguments(args);
    return fragment;
  }

  @NonNull
  @Override
  protected RepoListContract.View getViewContract() {
    return this;
  }

  @NonNull
  @Override
  protected RepoListPresenter getPresenter() {
    return new RepoListPresenter(GetRepositories.Factory.get(getContext()), RepoViewModelMapper.Factory.INSTANCE.get());
  }

  @Override
  protected int getLayoutRes() {
    return R.layout.fragment_repo_list;
  }

  @NonNull
  @Override
  protected Unbinder bindView(@NonNull View fragmentView) {
    return ButterKnife.bind(this, fragmentView);
  }

このあたりはおおよそこんなカンジ、なのかどうか。

  • newInstance ではアカウント文字列を Bundle に詰めて setArguments した上で Fragment オブジェクトを戻しています
  • getViewContract では RepoListContract.View を実装している this を戻しています
  • getPresenter でも RepoListPresenter オブジェクトを生成して戻しています
  • getLayoutRes はこの画面固有の id を返却
  • bindView では ButterKnife.bind の戻りを返却

次は onViewCreated のあたりの手続き定義です。

  @Override
  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    ofNullable(getArguments()).ifPresent(bundle -> {
      account = bundle.getString(RepoListActivity.BundleKey.ACCOUNT.name());
      Timber.d("account: %s", account);
      initRecycler();
      initSwipe();
    });
  }

  private void initRecycler() {
    ofNullable(getActivity()).ifPresent(activity -> {
      adapter = new RepoListAdapter(activity);
      recyclerView.setLayoutManager(new LinearLayoutManager(activity));
      recyclerView.setAdapter(adapter);
      presenter.getRepositoryList(account);
    });
  }

  private void initSwipe() {
    swipeRefreshLayout.setOnRefreshListener(() -> presenter.refreshRepositoryList(account));
  }

onViewCreated ではアカウント文字列を取得して初期化してますね。

  • Timber.d は別途確認
  • initRecycler はリスト初期化かな、これも別途確認します
  • initSwipe はリストのてっぺんからさらに上、な操作で、なのかどうか、これも別途確認

先に残った定義を全部確認します。

  @Override
  public void showRepositoryList(@NonNull List<RepoViewModel> repositoryList) {
    if (swipeRefreshLayout.isRefreshing()) {
      swipeRefreshLayout.setRefreshing(false);
    }
    adapter.setViewModels(repositoryList);
  }

  @Override
  public void showProgressBar() {
    progressBar.setVisibility(View.VISIBLE);
  }

  @Override
  public void hideProgressBar() {
    progressBar.setVisibility(View.GONE);
  }

  @Override
  public void showError(@NonNull String message) {
    message = isEmpty(message)
        ? getString(R.string.default_error_while_getting_repositories)
        : message;
    Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
  }

ここは RepoListContract.View で実装が強制されているメソッドの定義になります。いくつか別途確認必要なものを以下に列挙しておきます。

  • リストアダプタの実装確認必要
  • progressBar についてはレイアウト確認必要

ここまでで出てきた要確認事項を先に確認します。

Timber.d について

ログ用のユーティリティのようです。詳細以下。

initRecycler について

いくつかの wrapper 経由で以下な処理を行っています。

  • RepoListAdapter オブジェクト生成して adapter 属性に設定
  • RecyclerView#setLayoutManager 呼び出し (LinearLayoutManager オブジェクトを生成して渡している)
  • RecyclerView#setAdapter 呼び出し(adapter 属性を渡している)
  • RepoListPresenter#getRepositoryList 呼び出し

RepoListPresenter#getRepositoryList ですが

  • プログレスバー表示して
  • GetRepositories#enqueue 呼び出し(GetRepositories.Param.newInstance(account) の戻りを渡しています)

という処理を行っています。GetRepositories は domain/usecase 配下にて定義されているようです。このあたりは UnitTest な Hands-on で確認したあたりになります。Otto の Bus を使って通信結果の通知なども行っておりますが、詳細は別途とします。

initSwipe について

というか、SwipeRefreshLayout について。以下のドキュメントによれば、以前 PullToRefresh と呼ばれていた機能を、との記載あり。

ということは

  private void initSwipe() {
    swipeRefreshLayout.setOnRefreshListener(() -> presenter.refreshRepositoryList(account));
  }

という記述は Refresh な操作時には RepoListPresenter#refreshRepositoryList を呼び出すという理解で良いはず。処理の実装については別途確認

リストアダプタの実装確認必要

実装については別途確認するのでここでは一旦スルーします。

progressBar についてはレイアウト確認必要

レイアウト確認したら RelativeLayout でリストにかぶせる形になっています。以下な形です。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".presentation.view.activity.AccountInputActivity">

    <RelativeLayout
        android:id="@+id/progress"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone">

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"/>

    </RelativeLayout>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

Fragment の確認はこの程度で Presenter にすすめます。

RepoListPresenter.java

順に実装を確認します。先頭部分から。

public class RepoListPresenter extends BasePresenter<RepoListContract.View> implements RepoListContract.Interaction {
  @NonNull
  private final GetRepositories getRepositories;

  @NonNull
  private final RepoViewModelMapper mapper;

  @Nullable
  TaskTicket ticket;

  public RepoListPresenter(@NonNull GetRepositories getRepositories, @NonNull RepoViewModelMapper mapper) {
    this.getRepositories = getRepositories;
    this.mapper = mapper;
  }
  • BasePresenter を継承
  • RepoListContract.Interaction を実装
  • GetRepositories 型、RepoViewModelMapper 型の属性はコンストラクタで設定

以下は RepoListCOntrace.Interaction で実装を強制されているメソッドです。

  @Override
  public void getRepositoryList(@NonNull String account) {
    view.showProgressBar();
    ticket = getRepositories.enqueue(GetRepositories.Param.newInstance(account));
  }

  @Override
  public void refreshRepositoryList(@NonNull String account) {
    ticket = getRepositories.enqueue(GetRepositories.Param.newInstance(account, true));
  }

どちらも Getrepositories#enqueue にて問い合わせを行う処理です。以下は Fragment 破棄時に問い合わせをキャンセルする処理の記述で

  @Override
  public void onDestroyView() {
    ofNullable(ticket).ifPresent(ticket -> ticket.cancel(true));
  }

以降の記述は問い合わせ成功、失敗時に呼び出される処理と思われます(Otto の Bus がよしなに呼び出してくれる模様)。

  @Subscribe
  public void onSuccess(GetRepositories.OnSuccessGetRepositories success) {
    view.hideProgressBar();
    List<Repository> repositories = success.getItem();
    view.showRepositoryList(mapper.convertList(repositories));
  }

  @Subscribe
  public void onError(GetRepositories.OnFailureGetRepositories failure) {
    view.hideProgressBar();
    view.showError(failure.getItem().getMessage());
    finishActivity();
  }

annotation で紐がつく、というのは良いですね。あと、view は RepoListFragment なオブジェクトの参照なので

  @Override
  public void showRepositoryList(@NonNull List<RepoViewModel> repositoryList) {
    if (swipeRefreshLayout.isRefreshing()) {

を呼び出しているはずなのですが、List を mapper.convertList で List に変換しているのですね。これも別途実装確認必要ですね。

現時点で以下な実装未確認です。

  • RepoListContract.java
  • RepoListAdapter.java
  • RepoViewModelMapper.java
  • GetRepositories.java

Hugo-Octopress テーマ修正 石田流メモ

comments powered by Disqus