AndroidのMVP・MVVMの人気サンプルプロジェクト2個の実装方法を読む

  • 168
    Like
  • 0
    Comment
More than 1 year has passed since last update.

それぞれどんな実装方法をしているのか興味があったので、それぞれさらっと見てみたいと思います。

読みたくても時間がなくてなかなか読めないことが多いので、、
深くこれが良いなどはまずは考えずに行きたいと思います。
GitHubで「mvp android」で検索して★の多い順です。
ホントは5個ぐらい読みたかったのですが力尽きてしまいました。

pedrovgs/EffectiveAndroidUI

★ 2456
https://github.com/pedrovgs/EffectiveAndroidUI

MVPとMVVM(公式のDataBindingを利用しない)のサンプルアプリです。
両方の実装が混ざっているようです。
DaggerやButterKnifeを活用しています。

MVPでの実装

ViewがPresenterにイベントを渡して、Presenterが何をするのか判断して、Viewのメソッドを呼び出して反映します。

View(Fragment)

Daggerを利用してPresenterのインスタンスを作成して、Fragment#onViewCreatedのタイミングでTvShowCatalogPresenter#initialize()を呼び出します。
また外から呼び出せるようにshowLoading()などを用意しています。

Presenterで定義されているinterfaceであるTvShowCatalogPresenter.ViewをTvShowCatalogFragmentは実装しています。
https://github.com/pedrovgs/EffectiveAndroidUI/blob/25ca0774c7243d290b4f5c4fffa3119459c3218d/app/src/main/java/com/github/pedrovgs/effectiveandroidui/ui/fragment/TvShowCatalogFragment.java#L48-L48

public class TvShowCatalogFragment extends BaseFragment implements TvShowCatalogPresenter.View {
...
  @Inject TvShowCatalogPresenter tvShowCatalogPresenter;
...
  @InjectView(R.id.pb_loading) ProgressBar pb_loading;
...
  @Override public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    initializeGridView();
    tvShowCatalogPresenter.setView(this);
    tvShowCatalogPresenter.initialize();
  }

  @Override public void onAttach(Activity activity) {
    super.onAttach(activity);
  }

  @Override public void onResume() {
    super.onResume();
    tvShowCatalogPresenter.resume();
  }
...

  @Override public void hideLoading() {
    pb_loading.setVisibility(View.GONE);
  }

  @Override public void showLoading() {
    pb_loading.setVisibility(View.VISIBLE);
  }
...

Presenter

https://github.com/pedrovgs/EffectiveAndroidUI/blob/25ca0774c7243d290b4f5c4fffa3119459c3218d/app/src/main/java/com/github/pedrovgs/effectiveandroidui/ui/presenter/TvShowCatalogPresenter.java#L38-L38

TvShowCatalogPresenter#initialize()が呼び出されるとTvShowsを読みこんで、コールバックでview(Activity)に反映します。

@Singleton
public class TvShowCatalogPresenter extends Presenter {

  private GetTvShows getTvShowsInteractor;
  private Navigator navigator;

  private View view;
  private TvShowCollection currentTvShowCollection;

  @Inject
  public TvShowCatalogPresenter(GetTvShows getTvShowsInteractor, Navigator navigator) {
    this.getTvShowsInteractor = getTvShowsInteractor;
    this.navigator = navigator;
  }

  public void setView(View view) {
    if (view == null) {
      throw new IllegalArgumentException("You can't set a null view");
    }
    this.view = view;
  }

  @Override
  public void initialize() {
    checkViewAlreadySetted();
    loadTvShows();
  }

  /**
   * Use GetTvShows interactor to obtain a collection of videos and render it using the view
   * object setted previously. If the interactor returns an error the presenter will show an error
   * message and the empty case. In both cases, the progress bar visibility will be hidden.
   */
  private void loadTvShows() {
    if (view.isReady()) {
      view.showLoading();
    }
    getTvShowsInteractor.execute(new GetTvShows.Callback() {
      @Override public void onTvShowsLoaded(final Collection<TvShow> tvShows) {
        currentTvShowCollection = new TvShowCollection(tvShows);
        showTvShows(tvShows);
      }

      @Override public void onConnectionError() {
        notifyConnectionError();
      }
    });
  }
...
  private void notifyConnectionError() {
    if (view.isReady() && !view.isAlreadyLoaded()) {
      view.hideLoading();
      view.showConnectionErrorMessage();
      view.showEmptyCase();
      view.showDefaultTitle();
    }
  }

  private void showTvShows(Collection<TvShow> tvShows) {
    if (view.isReady()) {
      view.renderVideos(tvShows);
      view.hideLoading();
      view.updateTitleWithCountOfTvShows(tvShows.size());
    }
  }
....
  public void onTvShowThumbnailClicked(final TvShow tvShow) {
      navigator.openTvShowDetails(tvShow);
  }
....
  /**
   * View interface created to abstract the view
   * implementation used in this presenter.
   */
  public interface View {

    void hideLoading();

    void showLoading();

    void renderVideos(final Collection<TvShow> tvShows);

    void updateTitleWithCountOfTvShows(final int counter);

    void showConnectionErrorMessage();

    void showEmptyCase();

    void showDefaultTitle();

    void showTvShowTitleAsMessage(TvShow tvShow);

    boolean isReady();

    boolean isAlreadyLoaded();
  }
}

またTvShowCatalogPresenterは以下のインターフェースをデフォルトで実装するようです。

public abstract class Presenter {

  /**
   * Called when the presenter is initialized, this method represents the start of the presenter
   * lifecycle.
   */
  public abstract void initialize();

  /**
   * Called when the presenter is resumed. After the initialization and when the presenter comes
   * from a pause state.
   */
  public abstract void resume();

  /**
   * Called when the presenter is paused.
   */
  public abstract void pause();
}

getTvShowsInteractorを利用して非同期処理を行っているようです。

また以下のライブラリ(自作?)を利用して、ListViewのAdapterなどを管理しているようです。そこからのクリックで、PresenterにあるonTvShowThumbnailClicked()を呼ばせたりなどを行っています。
https://github.com/pedrovgs/Renderers

またNavigatorというクラスにActivityやフラグメントの移動などの処理を任せるようにしているようです。
https://github.com/pedrovgs/EffectiveAndroidUI/blob/e6f484f2de5d4e81e09f4e5e7e8049bd2a7f9e5f/app/src/main/java/com/github/pedrovgs/effectiveandroidui/ui/activity/Navigator.java#L42-L42

MVVMでの実装(Android公式DataBindingを利用しない)

基本的にはPresenterと同じような使い方をしているようです。
ただ違う部分があり、PresenterがShowTvShowOnBrowserActionCommandというクラスのインスタンスを保持していて、クリックした時に、それをViewが取り出して実行します。
また微妙にViewModelがPresenterのInterfaceのようなものを継承していないなどの違いがあります。

View (Fragment)

public class TvShowFragment extends BaseFragment
    implements TvShowViewModel.Listener {

  @Inject TvShowViewModel tvShowViewModel;
  @Inject ChapterViewModelRendererAdapterFactory chapterRendererAdapterFactory;
...
  @InjectView(R.id.iv_fan_art) ImageView iv_fan_art;
  @InjectView(R.id.lv_chapters) ListView lv_chapters;
  @InjectView(R.id.pb_loading) ProgressBar pb_loading;
  @InjectView(R.id.v_empty_case) View v_empty_case;
...
  @Override public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    initializeListView();
    bindViewModel();
  }

  @Override public void onAttach(Activity activity) {
    super.onAttach(activity);
    tvShowViewModel.setReady(true);
  }
...

  public void showTvShow(final String tvShowId) {
    if (isAdded()) {
      tvShowViewModel.loadTvShow(tvShowId);
    }
  }
...

  @OnClick(R.id.iv_fan_art) void onFanArtClicked() {
    ActionCommand fanArtClickActionCommand = tvShowViewModel.getTvShowClickedCommand();
    fanArtClickActionCommand.execute();
  }
...
  @Override public void onTvShowTitleLoaded(final String tvShowTitle) {
    String tvShowHeaderTitle = getString(R.string.tv_show_title, tvShowTitle);
    header_tv_show_chapters.setText(tvShowHeaderTitle);
  }

...
  private void bindViewModel() {
    tvShowViewModel.setListener(this);
    tvShowViewModel.initialize();
  }
...

ViewModel

public class TvShowViewModel {

  private final GetTvShowById getTvShowById;
  private final ShowTvShowOnBrowserActionCommand showTvShowOnBrowserActionCommand;

  private Listener listener;
  private boolean isReady;

  @Inject
  public TvShowViewModel(GetTvShowById getTvShowById,
      ShowTvShowOnBrowserActionCommand showTvShowOnBrowserActionCommand) {
    this.getTvShowById = getTvShowById;
    this.showTvShowOnBrowserActionCommand = showTvShowOnBrowserActionCommand;
  }

  public void loadTvShow(final String tvShowId) {
    listener.onLoadVisibilityChanged(true);
    listener.onEmptyCaseVisibilityChanged(false);
    getTvShowById.execute(tvShowId, new GetTvShowById.Callback() {
      @Override public void onTvShowLoaded(TvShow tvShow) {
        notifyTvShowLoaded(tvShow);
      }

      @Override public void onTvShowNotFound() {
        notifyTvShowNotFound();
      }

      @Override public void onConnectionError() {
        notifyConnectionError();
      }
    });
  }

  public ActionCommand getTvShowClickedCommand() {
    return showTvShowOnBrowserActionCommand;
  }

  private void notifyConnectionError() {
    if (isReady) {
      listener.onLoadVisibilityChanged(false);
      listener.onVisibilityChanged(false);
      listener.onEmptyCaseVisibilityChanged(true);
      listener.onConnectionErrorMessageNotFound();
    }
  }
  private void notifyTvShowNotFound() {
    if (isReady) {
      listener.onLoadVisibilityChanged(false);
      listener.onVisibilityChanged(false);
      listener.onEmptyCaseVisibilityChanged(true);
      listener.onTvShowMessageNotFound();
    }
  }

  private void notifyTvShowLoaded(TvShow tvShow) {
    showTvShowOnBrowserActionCommand.setTvShowUrl(tvShow.getPoster());
    if (isReady) {
      listener.onFanArtLoaded(tvShow.getFanArt());
      listener.onTvShowTitleLoaded(tvShow.getTitle());
      listener.onChaptersLoaded(getChaptersViewModel(tvShow.getChapters()));
      listener.onVisibilityChanged(true);
      listener.onLoadVisibilityChanged(false);
      listener.onEmptyCaseVisibilityChanged(false);
    }
  }
...

  /**
   * Interface created to work as ViewModel listener.
   * Every change in the view model will be
   * notified to Listener implementation.
   */
  public interface Listener {

    void onFanArtLoaded(final String fanArt);

    void onTvShowTitleLoaded(final String tvShowTitle);

    void onChaptersLoaded(final List<ChapterViewModel> chapters);

    void onVisibilityChanged(final boolean visible);

    void onLoadVisibilityChanged(final boolean visible);

    void onEmptyCaseVisibilityChanged(final boolean visible);

    void onTvShowMessageNotFound();

    void onConnectionErrorMessageNotFound();
  }

Command

public class ShowTvShowOnBrowserActionCommand implements ActionCommand {

  private final Context context;

  private String tvShowUrl;

  @Inject
  public ShowTvShowOnBrowserActionCommand(@ActivityContext Context context) {
    this.context = context;
  }

  public void setTvShowUrl(String tvShowUrl) {
    this.tvShowUrl = tvShowUrl;
  }

  @Override public void execute() {
    if (tvShowUrl != null) {
      Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(tvShowUrl));
      context.startActivity(browserIntent);
    }
  }
}
public interface ActionCommand {
  void execute();
}

ivacf/archi

https://github.com/ivacf/archi
★ 1308
Standard Androidで実装するそうです。
ただModelの実装などで非同期で読み込むときはRxJavaを利用していました。
こちらはMVPでの実装とMVVMでの実装両方を用意しているので、それを見ていきましょう。

MVPでの実装

View(Activity)

基本的にはEffectiveAndroidUIと同じようにViewやPresenterを利用します。(Daggerを使わないという違いはありますが、、)

public class MainActivity extends AppCompatActivity implements MainMvpView {

    private MainPresenter presenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Set up presenter
        presenter = new MainPresenter();
        presenter.attachView(this);
...
        // Set up search button
        searchButton = (ImageButton) findViewById(R.id.button_search);
        searchButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.loadRepositories(editTextUsername.getText().toString());
            }
        });
        //Set up username EditText
        editTextUsername = (EditText) findViewById(R.id.edit_text_username);
        editTextUsername.addTextChangedListener(mHideShowButtonTextWatcher);
        editTextUsername.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                    presenter.loadRepositories(editTextUsername.getText().toString());
                    return true;
                }
                return false;
            }
        });


    @Override
    public void showRepositories(List<Repository> repositories) {
        RepositoryAdapter adapter = (RepositoryAdapter) reposRecycleView.getAdapter();
        adapter.setRepositories(repositories);
        adapter.notifyDataSetChanged();
        reposRecycleView.requestFocus();
        hideSoftKeyboard();
        progressBar.setVisibility(View.INVISIBLE);
        infoTextView.setVisibility(View.INVISIBLE);
        reposRecycleView.setVisibility(View.VISIBLE);
    }

    @Override
    public void showMessage(int stringId) {
        progressBar.setVisibility(View.INVISIBLE);
        infoTextView.setVisibility(View.VISIBLE);
        reposRecycleView.setVisibility(View.INVISIBLE);
        infoTextView.setText(getString(stringId));
    }

    @Override
    public void showProgressIndicator() {
        progressBar.setVisibility(View.VISIBLE);
        infoTextView.setVisibility(View.INVISIBLE);
        reposRecycleView.setVisibility(View.INVISIBLE);
    }

インターフェース MainMvpView

public interface MainMvpView extends MvpView {

    void showRepositories(List<Repository> repositories);

    void showMessage(int stringId);

    void showProgressIndicator();
}

Presenter

RxJavaを利用してModelからデータを取り出すようです。そして結果をView(Activity)に返すようです。

public class MainPresenter implements Presenter<MainMvpView> {

    public static String TAG = "MainPresenter";

    private MainMvpView mainMvpView;
    private Subscription subscription;
    private List<Repository> repositories;

    @Override
    public void attachView(MainMvpView view) {
        this.mainMvpView = view;
    }

    @Override
    public void detachView() {
        this.mainMvpView = null;
        if (subscription != null) subscription.unsubscribe();
    }

    public void loadRepositories(String usernameEntered) {
        String username = usernameEntered.trim();
        if (username.isEmpty()) return;

        mainMvpView.showProgressIndicator();
        if (subscription != null) subscription.unsubscribe();
        ArchiApplication application = ArchiApplication.get(mainMvpView.getContext());
        GithubService githubService = application.getGithubService();
        subscription = githubService.publicRepositories(username)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(application.defaultSubscribeScheduler())
                .subscribe(new Subscriber<List<Repository>>() {
                    @Override
                    public void onCompleted() {
                        Log.i(TAG, "Repos loaded " + repositories);
                        if (!repositories.isEmpty()) {
                            mainMvpView.showRepositories(repositories);
                        } else {
                            mainMvpView.showMessage(R.string.text_empty_repos);
                        }
                    }

                    @Override
                    public void onError(Throwable error) {
                        Log.e(TAG, "Error loading GitHub repos ", error);
                        if (isHttp404(error)) {
                            mainMvpView.showMessage(R.string.error_username_not_found);
                        } else {
                            mainMvpView.showMessage(R.string.error_loading_repos);
                        }
                    }

                    @Override
                    public void onNext(List<Repository> repositories) {
                        MainPresenter.this.repositories = repositories;
                    }
                });
    }

    private static boolean isHttp404(Throwable error) {
        return error instanceof HttpException && ((HttpException) error).code() == 404;
    }

}

またMainPresenterは以下の様なインターフェースを実装しています。


public interface Presenter<V> {

    void attachView(V view);

    void detachView();

}

ちなみにAPIの通信などにはRetrofitを利用している感じでした。

MVVMでの実装

AndroidのDataBindingを利用します。

レイアウト

レイアウトxmlで指定できる変数にMainViewModelを指定しています。
このレイアウトで様々なViewのvisibilityや、onClickイベントtextなどをBindしています。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="uk.ivanc.archimvvm.viewmodel.MainViewModel"/>
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/very_light_grey"
        tools:context=".MainActivity">

        <ImageView
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:src="@drawable/octocat"/>

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            android:minHeight="?attr/actionBarSize"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>

        <RelativeLayout
            android:id="@+id/layout_search"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/toolbar"
            android:animateLayoutChanges="true"
            android:background="?attr/colorPrimary"
            android:paddingBottom="20dp"
            android:paddingLeft="@dimen/vertical_margin"
            android:paddingRight="@dimen/vertical_margin"
            android:paddingTop="10dp">

            <ImageButton
                android:id="@+id/button_search"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_marginLeft="5dp"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:onClick="@{viewModel.onClickSearch}"
                android:src="@drawable/ic_search_white_36dp"
                app:visibility="@{viewModel.searchButtonVisibility}"
                tools:visibility="visible"/>

            <EditText
                android:id="@+id/edit_text_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_toLeftOf="@id/button_search"
                android:hint="@string/hit_username"
                android:imeOptions="actionSearch"
                android:inputType="text"
                android:onEditorAction="@{viewModel.onSearchAction}"
                android:textColor="@color/white"
                android:theme="@style/LightEditText"
                app:addTextChangedListener="@{viewModel.usernameEditTextWatcher}"/>

        </RelativeLayout>

        <ProgressBar
            android:id="@+id/progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/layout_search"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="20dp"
            app:visibility="@{viewModel.progressVisibility}"/>

        <TextView
            android:id="@+id/text_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/layout_search"
            android:layout_centerHorizontal="true"
            android:layout_marginLeft="@dimen/vertical_margin"
            android:layout_marginRight="@dimen/vertical_margin"
            android:layout_marginTop="20dp"
            android:gravity="center"
            android:text="@{viewModel.infoMessage}"
            android:textColor="@color/secondary_text"
            android:textSize="18sp"
            app:visibility="@{viewModel.infoMessageVisibility}"/>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/repos_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/layout_search"
            android:clipToPadding="false"
            android:paddingBottom="@dimen/vertical_margin_half"
            android:paddingTop="@dimen/vertical_margin_half"
            android:scrollbars="vertical"
            app:visibility="@{viewModel.recyclerViewVisibility}"
            tools:listitem="@layout/item_repo"/>

    </RelativeLayout>

</layout>

View(Activity)

先ほどのレイアウトをDataBindingUtilで指定して作成して、MainViewModelをインスタンス化して、binding.setViewModelでViewModelの変数の変更をレイアウトに伝えられるようにします。

public class MainActivity extends AppCompatActivity implements MainViewModel.DataListener {

    private MainActivityBinding binding;
    private MainViewModel mainViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
        mainViewModel = new MainViewModel(this, this);
        binding.setViewModel(mainViewModel);
        setSupportActionBar(binding.toolbar);
        setupRecyclerView(binding.reposRecyclerView);
    }

    @Override
    public void onRepositoriesChanged(List<Repository> repositories) {
        RepositoryAdapter adapter =
                (RepositoryAdapter) binding.reposRecyclerView.getAdapter();
        adapter.setRepositories(repositories);
        adapter.notifyDataSetChanged();
        hideSoftKeyboard();
    }

    private void setupRecyclerView(RecyclerView recyclerView) {
        RepositoryAdapter adapter = new RepositoryAdapter();
        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
    }

ViewModel

RxJavaでModelからデータを取り出すのは同じですが、AndroidのDataBindingをうまく活用しています。
ObservableIntでVisibilityが変わった時にレイアウトのVisibilityも変わるようにしていたり、文字列が変わるようです。
getUsernameEditTextWatcher()などは、レイアウトファイルでapp:addTextChangedListener="@{viewModel.usernameEditTextWatcher}で指定していたので、呼びだされてTextChangedListenerとして利用されるようです。

public class MainViewModel implements ViewModel {

    private static final String TAG = "MainViewModel";

    public ObservableInt infoMessageVisibility;
    public ObservableInt progressVisibility;
    public ObservableInt recyclerViewVisibility;
    public ObservableInt searchButtonVisibility;
    public ObservableField<String> infoMessage;

    private Context context;
    private Subscription subscription;
    private List<Repository> repositories;
    private DataListener dataListener;
    private String editTextUsernameValue;

    public MainViewModel(Context context, DataListener dataListener) {
        this.context = context;
        this.dataListener = dataListener;
        infoMessageVisibility = new ObservableInt(View.VISIBLE);
        progressVisibility = new ObservableInt(View.INVISIBLE);
        recyclerViewVisibility = new ObservableInt(View.INVISIBLE);
        searchButtonVisibility = new ObservableInt(View.GONE);
        infoMessage = new ObservableField<>(context.getString(R.string.default_info_message));
    }

    public void setDataListener(DataListener dataListener) {
        this.dataListener = dataListener;
    }

    @Override
    public void destroy() {
        if (subscription != null && !subscription.isUnsubscribed()) subscription.unsubscribe();
        subscription = null;
        context = null;
        dataListener = null;
    }

    public boolean onSearchAction(TextView view, int actionId, KeyEvent event) {
        if (actionId == EditorInfo.IME_ACTION_SEARCH) {
            String username = view.getText().toString();
            if (username.length() > 0) loadGithubRepos(username);
            return true;
        }
        return false;
    }

    public void onClickSearch(View view) {
        loadGithubRepos(editTextUsernameValue);
    }

    public TextWatcher getUsernameEditTextWatcher() {
        return new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
                editTextUsernameValue = charSequence.toString();
                searchButtonVisibility.set(charSequence.length() > 0 ? View.VISIBLE : View.GONE);
            }

            @Override
            public void afterTextChanged(Editable editable) {

            }
        };
    }

    private void loadGithubRepos(String username) {
        progressVisibility.set(View.VISIBLE);
        recyclerViewVisibility.set(View.INVISIBLE);
        infoMessageVisibility.set(View.INVISIBLE);
        if (subscription != null && !subscription.isUnsubscribed()) subscription.unsubscribe();
        ArchiApplication application = ArchiApplication.get(context);
        GithubService githubService = application.getGithubService();
        subscription = githubService.publicRepositories(username)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(application.defaultSubscribeScheduler())
                .subscribe(new Subscriber<List<Repository>>() {
                    @Override
                    public void onCompleted() {
                        if (dataListener != null) dataListener.onRepositoriesChanged(repositories);
                        progressVisibility.set(View.INVISIBLE);
                        if (!repositories.isEmpty()) {
                            recyclerViewVisibility.set(View.VISIBLE);
                        } else {
                            infoMessage.set(context.getString(R.string.text_empty_repos));
                            infoMessageVisibility.set(View.VISIBLE);
                        }
                    }

                    @Override
                    public void onError(Throwable error) {
                        Log.e(TAG, "Error loading GitHub repos ", error);
                        progressVisibility.set(View.INVISIBLE);
                        if (isHttp404(error)) {
                            infoMessage.set(context.getString(R.string.error_username_not_found));
                        } else {
                            infoMessage.set(context.getString(R.string.error_loading_repos));
                        }
                        infoMessageVisibility.set(View.VISIBLE);
                    }

                    @Override
                    public void onNext(List<Repository> repositories) {
                        Log.i(TAG, "Repos loaded " + repositories);
                        MainViewModel.this.repositories = repositories;
                    }
                });
    }

    private static boolean isHttp404(Throwable error) {
        return error instanceof HttpException && ((HttpException) error).code() == 404;
    }

    public interface DataListener {
        void onRepositoriesChanged(List<Repository> repositories);
    }
}

まとめ

同じアーキテクチャだけあって、結構この2つは似ている部分が多かったです。
しかし、Daggerを使っていたり、RxJavaを使っていたりなど利用ライブラリに違いがあったり、してどういうアプローチを取るかの違いはあってそれが面白かったです。
ただ実装だけではCommandの実装など意図がくみきれない部分があるので調べていけたら良いなと思いました。
個人的にはarchiのMVVMの実装はAndroidのDataBindingを活用できていて、楽になってそうな感じがあったので、試してみたいと思いました。