読者です 読者をやめる 読者になる 読者になる

そーす

福岡在住のプログラマ

基本的にCustomViewを使うメリット(全体設計編)

基本的にCustomViewってどういうことだよ、って話ですが…

例えば, Activityだと

class MainActivity: AppCompatActivity(){
  private view by lazy { MainView(this) }
  
  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(view)
  }
}

class MainView: FrameLayout {
   constructor(context: Context?) : super(context)
   constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
   constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

   init {
      LayoutInflater.from(context).inflate(R.layout.main, this)
   }
}

こうしようって話です。

なぜ?

Viewに関するロジックと操作を分離したいからです。 Androidにかぎらずアプリ開発ではModel部分のロジックよりもView側のロジックの方が複雑だったり大量にコード書く必要があったりしますよね。 setContentView(R.layout.main)だと、ActivityにView側のロジックやViewの参照を持たせなければならずActivityが肥大化し、 ActivityはActivityでしか受け取れないイベントなどを処理する必要があるのでActivityの責務が大きくなりすぎます。 なので、CustomViewにしてView側の処理はすべてCustomViewに隠蔽してしまおうという話です。

実装例

わかりやすいかどうかわかりませんが、 例としてユーザ情報(画像、名前、メールアドレス)を表示して名前とメールアドレスを編集してサーバに送る機能を実装するとしましょう。

Activityに直接書く例

class MainActivity : AppCompatActivity() {
    private val iconView by lazy { findViewById(R.id.iconView) as ImageView }
    private val nameForm by lazy { findViewById(R.id.nameForm) as EditText }
    private val emailForm by lazy { findViewById(R.id.emailForm) as EditText }
    private val sendButton by lazy { findViewById(R.id.sendButton) as Button }

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

        val user = User.load()
        Picasso.with(this).load(user.iconUrl).into(iconView)
        nameForm.setText(user.name)
        emailForm.setText(user.email)
        sendButton.setOnClickListener { send() }
    }

    private fun send() {
        val name = nameForm.text.toString()
        val email = emailForm.text.toString()

        //送信処理…
        //成功したら
        //...
        //失敗したら
        //...
    }
}

これをCustomViewで書き直してみます。 ちなみに、KotlinAndroidExtensionを用いています。

//Activity
class MainActivity : AppCompatActivity(), MainView.EventDelegate, MainModel.EventDelegate {
    private val view by lazy { MainView(this) }
    private val model by lazy { MainModel(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(view)

        view.set(user = User.load())
    }

    override fun onClickedSendButton(name: String, email: String) {
        model.send(name = name, email = email)
    }

    override fun onSucceededSending() {
    }

    override fun onFailedSending() {
    }
}

//View
class MainView : FrameLayout {
    val delegate: EventDelegate?

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    init {
        LayoutInflater.from(context).inflate(R.layout.activity_main, this)
        delegate = context as? EventDelegate
        sendButton.setOnClickListener { delegate?.onClickedSendButton(name = nameForm.text.toString(), email = emailForm.text.toString()) }
    }

    fun set(user: User) {
        Picasso.with(context).load(user.iconUrl).into(iconView)
        nameForm.setText(user.name)
        emailForm.setText(user.email)
    }

    interface EventDelegate {
        fun onClickedSendButton(name: String, email: String)
    }
}

//Model
class MainModel(delegate: EventDelegate?) {
    val delegate = delegate

    fun send(name: String, email: String) {
        //Apiとか叩く処理…
        //成功したら
        delegate?.onSucceededSending()
        //失敗したら
        delegate?.onFailedSending()
    }

    interface EventDelegate {
        fun onSucceededSending()
        fun onFailedSending()
    }
}

機能がシンプルすぎるのでコード量はCustomViewの方が増えましたね(笑)

しかし、責務を上手く分割できたかと思います。

設計としてはModelViewPresenterになるのでしょうか。Activity自体がイベントを直接受け取る場合もあるのでCなのかPなのか私はよくわかりません。とりあえずPとします。

Model -> MainModel

View -> MainView

Presenter -> MainActivity

って感じですかね。

Model

Modelは描画に依存しないロジック部分を担当します。大体がサーバやローカルキャッシュとのやり取りですね。 ModelはViewとPresenterのことは全く知りません。 Modelはメソッドを公開していますが、誰が叩いているかは気にしません。メソッドの処理の結果に応じて適切なdelgateインスタントのメソッドを呼ぶだけです。

View

Viewは特定のデータを加工したりして表示したり、Viewで受け取ったイベントを処理します。 ViewもModelとPresenterのことは全く知りません。 CustomViewを使わない方ではActivity自体が何をどう表示するかを知っている必要がありましたが、CustomViewにすることによってActivityはUserクラスのインスタントを渡すことだけを行えばよいので表示に関する責務がなくなりました。 また、ViewはViewで起こるイベントをdelegateインスタントに移譲します。

Presenter

PresenterだけがModelとViewの存在を知っておりViewから流れてくるイベントに対して適切にModelのメソッドを読んだりModelからのコールバックイベントをViewに反映したりします。 上記のView, Modelの設計にすることでActivityはイベントを受け取って適切にModelとViewにイベントを流すだけの存在と成り果てました。

設計に正解はない

です。すべてにおいてこの設計が良いわけではないです。自分も未だに試行錯誤してます。ただ、AndroidCleanArchitectureのように細けるのは個人的にはどうなのかなーと思います。 結構おおざっぱな設計だとは思いますが、ModelとViewが疎結合であることは大きなメリットだと思います。 多人数で実装を並行に行っても問題起きないですし。 あと今回のCustomViewを使った設計の良いところはActivityからViewの個々のオブジェクトを参照する必要がなく、 CustomViewのパブリックなプロパティ、メソッドとEventDelegateを見ればViewの責務が理解できることだと思います。 Activityから直接参照すると何でもできてしまうのでそのViewの責務が曖昧になってしまい、運用面での不安が残ります。

なんか意外とアプリの設計に関する資料って出てこないんですよね。人の設計見たい。 まだ書き足りない部分ありますが、疲れました。

とりあえずKotlinいいよ、Kotlin.