どくぴーの備忘録

真面目なことを書こうとするクソメガネのブログ。いつ投げ捨てられるのかは不明

DataBinding + RxJavaでMVVMパターンな設計を考える

今更感がすごいが、DataBindingを使うことによってAndroidアプリケーションの実装でMVVMパターンな設計を考えやすくなったし、DroidKaigi 2017のアプリがMVVMで実装されていたりするので、自分なりに設計をまとめてみる。

全体図

他で実装されている記事を見るとDDDなりと混ぜ合わせた感じの設計がちらほら見えて、一番シンプル(かつ集合知的な知見が溜まっている)と感じたDroidKaigi/conference-app-2017のアーキテクチャを丸パクリする形になった。

f:id:e10dokup:20170507160545g:plain

github.com

何をしているかざっと書くと

  • View
    • Activity/Fragment/Adapter ItemといったViewは1対1で対応するViewModelを持つ
    • 各Layout XMLには対応するViewModelをDataBindingでbindする
  • ViewModel
    • Viewの要素をクリックしたときの処理の定義やAPI/DBとのModelのやり取り、Viewへの反映を行う
    • ViewModelが取り扱うRepositoryはDagger2のDIを用いてインジェクトして利用する
  • Repository
    • ModelのCRUD操作を提供する
    • 各Modelのクラスに対応するRepositoryが存在する
    • RepositoryはDataSourceを持ち、DataSourceを利用してDBやAPIからデータを取得するが、ViewModel以下には隠蔽する。
  • Local/RemoteDataSource
    • 実際にModelのCRUD操作を行う
  • DataSourceからViewModelまでの処理はRxJavaでストリーミングに扱う

という感じ。

View

Viewは対応するViewModelをInjectして、Layout XMLにbindする。

public class MainActivity extends BaseActivity {

    @Inject
    MainActivityViewModel viewModel;

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getComponent().inject(this);
        bindViewModel(viewModel);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewModel(viewModel);
    }

    // 以下省略
}

ちなみにBaseActivityの中は次のような感じ。ViewModelにもライフサイクル系メソッドを用意して、ActivityやFragmentのライフサイクルと同期して呼び出すようにしている。

public abstract class BaseActivity extends AppCompatActivity {

    private ActivityComponent component;
    private ActivityViewModel viewModel;

    @NonNull
    public ActivityComponent getComponent() {
        if (component == null) {
            MyApplication application = (MyApplication) getApplication();
            component = application.getComponent().plus(new ActivityModule(this));
        }
        return component;
    }

    protected void bindViewModel(ActivityViewModel viewModel) {
        this.viewModel = viewModel;
    }

    @Override
    protected void onStart() {
        super.onStart();
        checkViewModel();
        viewModel.onStart(this);
    }
    
    // 以下省略
}

ViewModel

ViewModelの実装は次のようにする。画面遷移は、Navigatorという画面遷移を取り扱うクラスを用意して、それをViewModelにinjectして行うようにしてみた。今回は直面していないがContextが必要な処理があるときはEventBusを使ってActivityにイベントとして流したほうがいい気がする…。

public class MainActivityViewModel extends ActivityViewModel {

    private final Navigator navigator;
    private final TaskRepository taskRepository;

    private ObservableList<TaskViewModel> taskViewModels;


    @Inject
    public MainActivityViewModel(Navigator navigator, TaskRepository taskRepository) {
        this.navigator = navigator;
        this.taskRepository = taskRepository;

        this.taskViewModels = new ObservableArrayList<>();
    }

    @Override
    public void onStart() {

    }

    @Override
    public void onResume() {
        taskRepository.findAll()
                .map(tasks -> Stream.of(tasks)
                        .sorted((o1, o2) -> (int)(o1.deadlineEpoch - o2.deadlineEpoch))
                        .toList())
                .map(tasks -> convertToViewModel(tasks))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(taskViewModels1 -> {
                    this.taskViewModels.clear();
                    this.taskViewModels.addAll(taskViewModels1);
                });
    }

    // 以下省略
}

Navigatorの実装はこんな感じ。

@ActivityScope
public class Navigator {

    private final Activity activity;

    @Inject
    public Navigator(AppCompatActivity activity) {
        this.activity = activity;
    }

    public void navigateToCreateTask() {
        activity.startActivity(CreateTaskActivity.createIntent(activity));
    }

    public void navigateToTaskDetail(int taskId) {
        activity.startActivity(TaskDetailActivity.createIntent(activity, taskId));
    }

    public void closeActivity() {
        activity.finish();
    }
}

Repository

RepositoryにはLocal/RemoteDataSourceを持たせて、それらDataSourceにてCRUD操作を行う。例はローカルDBしかおいてないのでほぼ効果はないが、「ローカルDBにデータが有るときはローカルDBから、そうでないときはAPIから」という風に処理を分ける際にはここで分岐させる。

@Singleton
public class TaskRepository {

    private final TaskLocalDataSource taskLocalDataSource;

    @Inject
    public TaskRepository(TaskLocalDataSource taskLocalDataSource) {
        this.taskLocalDataSource = taskLocalDataSource;
    }

    public Single<List<Task>> findAll() {
        return taskLocalDataSource.findAll();
    }

    // 以下、省略
}

DataSource

DataSourceからの返り値はRxJavaのSingleで包んでストリーミングに流すようにする。例はLocalDataSourceだけだが、Retrofit等を用いてRemoteDataSourceを作るときはRetrofitのClientをinjectしてClientのアクセス結果をSingleで包む形になるはす。

public class TaskLocalDataSource {

    private final OrmaDatabase ormaDatabase;

    @Inject
    public TaskLocalDataSource(OrmaDatabase ormaDatabase) {
        this.ormaDatabase = ormaDatabase;
    }

    public Single<List<Task>> findAll() {
        return ormaDatabase.relationOfTask()
                .selector()
                .executeAsObservable()
                .toList()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
   
   // 以下省略
}

終わりに

とりあえずこれを踏襲してサンプルでToDoアプリを作ってみた。

github.com

先人の知識に頼りっぱなしで実装したが、いざ組んで見てわかること、記事にして思い違いだったと気づくことがたくさんあるのでとても重要だと思った(感想)