Android

Android開発でMVVMを採用してみて

More than 1 year has passed since last update.

※ この記事はエキサイトAdvent Calendar 2017 3日目の記事です。

新卒入社2年目ながら、ひとりで新規Androidアプリ案件を任せて頂き、
その際にMVVMパターンを採用しました。今回は採用してみてどうだったかについて紹介していきます!

解決したかったこと

新規案件で1から開発するにあたり、解決したかったことは以下でした。

  • マッチョなActivity / Fragmentを避ける
  • 神Managerクラスを避ける
  • 他のエンジニアがジョインしやすい
  • APIも同時並行開発だったのでモックしやすく
  • モダンなアーキテクチャを目指す

各所で設計について述べられているなか、技術力的に…等の言い訳をつけて有用な設計パターンや
よいアーキテクチャ・ライブラリを見送るのはもったいないし、使ってみたい!という独断と偏見から
最初からかっちりと設計を考えていきました。

MVVMパターン

MVVMに関する詳しい知見はたくさん転がっているので本記事では割愛、概要だけ。
素晴らしい記事がたくさんあってインターネット最高です:relaxed:

MVVM概要

MVVMを誤解を恐れず図解してみましょう。Wikiもこんな感じのが載っています。

これをAndroidに採用した場合、こんな感じになります。

M/V/VMそれぞれの役割は以下のスライドが非常に参考になりました。

Modelの問題

MVVMパターンを採用することでプレゼンテーションロジックとビジネスロジックを分けることができました。

次に問題となってくるのが、Model部分の実装です。
MVXパターンに総じて言えることとして、Model部分の実装にかんしては言及していません。

この部分はレイヤードアーキテクチャ(もどき)を採用し、
ドメインレイヤとデータレイヤに分け、データレイヤではレポジトリパターンを用いました。

目指したアーキテクチャ

記事やサンプルレポジトリをかき集めた結果、以下のようなアーキテクチャを目指しました。
(どこかでみたことあるような図なのは内緒です。)

採用技術

  • RxJava/RxAndroid/RxKotlin
  • Dagger2
  • Retrofit
  • Realm

Modelの変更通知

MVVMパターンで特に引っかかったのが、ViewModelへのModelの変更通知です。
ここはGitHubでいろんなリポジトリを漁った結果、RxJavaで対応する方向で落ち着きました。

RepositoryからRxJavaのSubjectを利用して通知を行います。
おもむろに簡単な実装例を載せてみます。

SampleRepository.kt
@Singleton
class SampleRepository @Inject constructor(private val api: Api, private val dao: Dao) {

    private val sampleSubject = PublishSubject.create<Sample>()
    val sample = sampleSubject.hide()!!
    private val errorSubject = PublishSubject.create<Throwable>()
    val error = errorSubject.hide()!!

    fun get() {
        if (needFetch) fetch() else find()
    }

    fun find() {
        dao.find().subscribe({ sampleSubject.onNext(it) }, { errorSubject.onNext(it)})
    }

    fun fetch() {
        api.getSample().subscribe({ sampleSubject.onNext(it) }, { errorSubject.onNext(it)})
    }
}

あとはこれをUseCase -> ViewModelへ流していく感じです。

SampleUseCase.kt
class SampleUseCase @Inject constructor(private val repo: SampleRepository) : UseCase() {

    val sample = repo.sample
    val error = repo.error

    fun getSample() {
        repo.get().addTo(disposables)
    }
}
SampleViewModel.kt
class SampleViewModel @Inject constructor(private val useCase: SampleUseCase) : viewModel() {

    val sample = ObservableField<Sample>()

    init {
        useCase.sample.subscribe { sample.set(it) }.addTo(disposables)
        useCase.error.subscribe { /* do something */ }.addTo(disposables)
    }

    fun getSample() {
        useCase.getSample()
    }
}

サンプルではUseCaseが不要に見えますが実際にはでビジネスロジックを挟んでいます。

MVVM + Layered Architecture(もどき)を採用してみて

解決できたこと

解決したかったことに合わせてどうだったかを振り返ってみます。

マッチョなActivity / Fragmentを避ける

Activity / FragmentはMVVMのViewにあたります。
ここでは基本的にAndroid依存の処理やViewのセットアップのみを記述する方針にしました。

クリック処理やViewの出し分けはViewModel(+ data binding)が担うことで
Activity / Fragmentはすっかりスリムな体型を保っています。

神Managerクラスを避ける

レイヤードアーキテクチャを採用したことで処理の責務がわかれたので神を崇めることなく
適材適所、それぞれの処理を記述することができました。

しかしなにかと○○Manger作ろうかな欲しいな…という気持ちになってしまいます。
そのときは初心に帰り、処理を整理し責務を分けそれぞれのレイヤーに実装することでことたります。

他のエンジニアがジョインしやすい

半年以上ひとりで開発していた案件ですが、2ヶ月程前から師匠にジョインしてもらいました。
ジョインしやすかったかヒアリングはできていませんが、責務を分けどこになにを書くかという決まりを
作ったことで全体的に統一感のある実装になっていて、成功だと感じています。

また、コードレビューにおいても、責務を分けたことで注力してレビューすべき箇所が
わかりやすくなったのも良いところではないでしょうか。

APIも同時並行開発だったのでモックしやすく

ここが1番のネックだったのですが、Repositoryパターンを取り入れたことで
APIがまだ未着手の部分はデータレイヤでモックの値を返すようにすることで
UseCaseより上の層はそこを意識することなく開発することができたのでとても良かったです。

モダンなアーキテクチャを目指す

概ね、モダンなアーキテクチャで実装できたかと思います。

しかし、初めてのAndroid案件ということもあり、舞い上がって「モダンなアーキテクチャを!」と思っていましたが、
やはり、プロジェクトの性質にあったアーキテクチャを採用するのが1番だよなと当たり前ながら思います。

開発中のアプリの性質上、ユーザのステータスが複雑なこともあり、MVVMを採用しましたがこれに関しても
非常にマッチしていて後悔はありません。ちゃんと設計しててよかったと自信を持って言えます!

まとめ

  • 最初に設計を固めておくの大事
  • アーキテクチャはアプリの性質に合うものを
  • 習うより慣れろ

今だと、いろんな画面で同じステータスを参照したいという場面が多々あるので
FluxやAACのViewModelの採用もありかなあなど思っています。設計考えるの楽しいですね。

実装の中身についてはまたどこかで触れられれば良いなと思います。

明日からのアドベントカレンダーも楽しみです!

参考