おじゃまぷよ系エンジニアメモ

主にスマホネイティブ関連Tips。たまにWebも書きたい。お腹はぷよぷよ

AndroidでMVPするときのPresenterの責務

Androidのクラス設計でMVPやMVVMが流行っています。
自分もMVPのクラス設計が導入しやすく好きなのですが、実際にやろうとすると何をPresenterに書き何をView(ActivityやFragment)に書くべきなのか指針がなくて
なんとなくMVPになることが多いです。そこで個人的にどういう風にMVPに分けるべきかメモしておきます
個人的に注意するポイントは以下の三点です

  • PresenterにViewの具象クラスを引き数として与えない
  • PresenterにAndroid特有のクラスを引き数として与えない
  • Presenterはテスタブルな状態にする

PresenterにViewの具象クラスを引き数として与えない

個人的にNGなコードは以下のようなPresenterが出てきたときです

class MainViewPresenter(val mainView: MainActivity) {
    fun doSomething() {

    }
}

PresenterのコンストラクタでActivityやFragmentをそのまま受け取るようなパターンです。
PresenterがガッツリViewと依存してしまいますし、AcitivityやFragmentを受け取ることでテストコードでmockにしにくいし、これをやってしまうとView側でできることとPresenter側で出来ることの境目がなくなってしまいます FragmentからfindViewByIdすればレイアウトは操作できてしまうし、Contextを取り出せばやりたいほうだいです。すべてのロジックをPresenterに書けてしまいまうためチームメンバーによって何を書くかバラバラになってしまいます。
ViewとPresenterはInterfaceを通して余計なことができないように縛りましょう

interface MainViewable{
    fun bindUserData(user:User)
}
//MainViewableのInterfaceを渡すことで余計なことをさせない
class MainViewPresenter(val mainView:MainViewable) {
    fun doSomething() {
           
    }
}
class MainActivity : AppCompatActivity(),MainViewable{
  val presenter:MainViewPresenter by lazy { 
        MainViewPresenter(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        presenter.doSomething()
    }

    override fun bindUserData(user:User) {
          //Viewにデータを反映させる
    }
}

PresenterにAndroid特有のクラスを引き数として与えない

例えばこういうPresenterです。

class MainViewPresenter(val context: Context,val pref:SharedPreferences) {
    fun doSomething() {

    }
}

ContextやSharedPreferencesなどAndroid特有のクラスを渡してしまうものです。
これをやってしまうとActivityやFragmentを引き数として渡すのと同じでUnitテスト時にmockを差し込みにくくなります。 またContextを渡してしまうと上記で書いた通りPresenter側でほぼなんでも出来るようになってしまうのでViewとPresenterの責務が曖昧になりがちです。 SharedPreferencesなどはそれをラップしたクラスを作ってそれを渡すといいでしょう。Contextはそもそも渡さないようにしましょう。
Contextが必要なクラスが多くあると思いますが、それは上記のSharedPreferenceServiceのようにContextを扱えるView側でクラスを生成しそれをPresenterへ引き数として渡すようにしましょう。

class SharedPreferencesService(private val context: Context) {
    val pref: SharedPreferences by lazy {
        context.getSharedPreferences("sample", Context.MODE_PRIVATE)
    }

    fun apply(key: String, value: String) {
        pref.edit().putString(key, value).apply()
    }
}
//ラップしたクラスにすることで簡単にmockにかえJUnitを回しやすくなる
class MainViewPresenter(val context: Context,val pref:SharedPreferencesService) {
    fun doSomething() {
    }
}

Presenterはテスタブルな状態にする

基本的にPresenterにAndroid特有の関心事を持ち込まなければJUnitによるテストが実行可能になっているはずです。
Presenterは高速にテスト回せる状態にし、ActivityやFragmentはAndroidUnitTestによる少し重めのテストという風に住み分けします。
PresenterがJUnitによるテストコードを回せなくなったとき、一度立ち止まって本当にPresenterでやるべき処理か立ち止まるいいきっかけになります。
ソースレビューするときもレビュー観点としておくことで、メンバー間によるプログラムの差異を減らせると思います。

最後に

そもそもMVPってAndroidのコードがFragmentやActivityにビジネスロジックとViewの操作が集中してマッチョになってしまうのでビジネスロジック部分を分けたいという欲求から生まれたものなはずなのに
そのビジネスロジック部分にFragmentやContextが出てきてマッチョになっていくのは何か違う気がすると思います。なんのためのPresenterなのか?それを一度考えましょう
ちなみにadapterの操作をどこで行うか?というのが悩みどころですが、あれもAndroid特有のクラスなので自分は「View側で操作する」でいいと思います。
sharedPreference同様にapdaterをラップしたクラスを用意しそこで操作するようにすればPresenterに渡してもいいかもしれません。
ものすごく雑にまとめたので実際のプロダクトでやるともしかしたらうまくいかない部分が出てくるかもしれませんが、基本方針としてPresenterをJUnitでテスタブルな状態にするということを守っていれば
クラス間の責務は適切にわけられているのではないでしょうか?
その他にMVPの責務の分け方の考えなど意見がある方は是非教えてください。自分もこれが最適とは思っていないので……
Androidのクラス設計ってなんでこんなに難しいんでしょうかね。