今更感がすごいが、DataBindingを使うことによってAndroidアプリケーションの実装でMVVMパターンな設計を考えやすくなったし、DroidKaigi 2017のアプリがMVVMで実装されていたりするので、自分なりに設計をまとめてみる。
全体図
他で実装されている記事を見るとDDDなりと混ぜ合わせた感じの設計がちらほら見えて、一番シンプル(かつ集合知的な知見が溜まっている)と感じたDroidKaigi/conference-app-2017のアーキテクチャを丸パクリする形になった。
何をしているかざっと書くと
- 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
- 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アプリを作ってみた。
先人の知識に頼りっぱなしで実装したが、いざ組んで見てわかること、記事にして思い違いだったと気づくことがたくさんあるのでとても重要だと思った(感想)