Android + AnkoのAnkoComponent周りで自分がよく使うパターンをまとめてみる。

自分が実際にアプリ作成時、Ankoを使う際に使うパターンをまとめてみます。


## コンポーネント

まずは、画面のコンポーネントを表すインターフェースを定義します

```Component.kt
import android.view.View

interface Component<R : View> {
    val root: R
}

fun <R : View, T : View> Component<R>.bindView(id: Int) = lazy {
    root.findViewById<T>(id)
}

val Component<*>.rootId: Int
    get() {
        if (root.id == View.NO_ID) {
            root.id = View.generateViewId()
        }
        return root.id
    }
```

コンポーネントは必ず`root`となるViewを持つようにして、拡張関数として`bindView`を、拡張プロパティとして`rootId`を提供します。


## ViewComponent

次に、複数の`Activity`で用いるような共通の`Component`を表す`ViewComponent`(名前が微妙)を定義します。

```ViewComponent.kt
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.ViewManager
import org.jetbrains.anko.AnkoContext
import org.jetbrains.anko.AnkoException
import kotlin.properties.Delegates


interface ViewComponent<R : View> : Component<R> {
    //Contextを元にViewを生成
    fun createView(ctx: Context): R
}

private val DEFAULT_INIT: View.() -> Unit = {}

//ViewComponentからViewを生成してViewManagerに追加
//主にAnkoのDSL上で使用する
fun <R : View> ViewManager.component(component: ViewComponent<R>,
                                     init: R.() -> Unit = DEFAULT_INIT): R = when (this) {
    is ViewGroup -> component.createView(context).apply(init).also(::addView)
    is AnkoContext<*> -> component.createView(ctx).apply(init).also { addView(it, null) }
    else -> throw AnkoException("$this is the wrong parent")
}

//ViewComponentからViewを生成してViewManagerに追加
//主にAnkoのDSL外で使用する
fun <R : View> ViewManager.addComponent(component: ViewComponent<R>,
                                        init: R.() -> Unit = DEFAULT_INIT): R = component(component, init)

//主に長過ぎるDSLを切り分けるために使う
fun <R : View> component(create: Context.() -> R): ViewComponent<R> = object : ViewComponent<R> {
    override var root: R by Delegates.notNull()
        private set

    override fun createView(ctx: Context) = ctx.create().also { root = it }
}
```

## UI
最後にActivityのUIを表すUIを定義します。

```UI.kt
import android.app.Activity
import android.view.View
import org.jetbrains.anko.AnkoComponent

interface UI<A : Activity, R : View> : Component<R>, AnkoComponent<A>
```

## 実際に使う
使う際はこんな感じ

```AppbarComponent.kt
interface IAppbarComponent : ViewComponent<AppBarLayout> {

    val toolbar: Toolbar
}

class AppbarComponent : IAppbarComponent {
    override var root: AppBarLayout by Delegates.notNull()
        private set

    private val toolbarId = View.generateViewId()
    override val toolbar: Toolbar by bindView(toolbarId)

    override fun createView(ctx: Context) = with(ctx) {
        appbarLayout {
            root = this
            toolbar {
                id = toolbarId
            }
        }
    }
}
```

```MainUI.kt
interface IMainUI : UI<MainActivity, RelativeLayout> {
    val textView: TextView
    val appbarComponent: IAppbarComponent
}

class MainUI : IMainUI {
    override lateinit var root: RelativeLayout
        private set

    private val textViewId = View.generateViewId()
    override val textView: TextView by bindView(textViewId)

    override val appbarComponent: IAppbarComponent = AppbarComponent()

    @SuppressLint("SetTextI18n")
    override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
        relativeLayout {
            root = this

            component(appbarComponent) {
                //Appbarをなんか適当に初期化してみたり
            }.lparams(matchParent, wrapContent)

            this.textView {
                id = textViewId
                text = "Hello World"
            }.lparams(wrapContent, wrapContent) {
                centerInParent()
            }
        }
    }
}
```

```MainActivity.kt
class MainActivity : AppCompatActivity(), IMainUI by MainUI() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(this)
        setSupportActionBar(appbarComponent.toolbar)
    }
}
```

こんな感じでボイラープレート的に頻繁にでてくる`AppbarLayout`と`Toolbar`の入れ子等を`ViewComponent`として切り分けて見たり出来ます。

あとは`RecyclerView.ViewHolder`に使う`View`を`ViewComponent`として定義してみたりとか。

ちなみに[AndroidX](https://developer.android.com/topic/libraries/support-library/refactor)を使っているので`appbarLayout`と`toolbar`はこんな感じで定義してます。

```kotlin
import android.content.Context
import android.view.View
import android.view.ViewManager
import androidx.appcompat.widget.Toolbar
import com.google.android.material.appbar.AppBarLayout
import org.jetbrains.anko.custom.customView

private val DEFAULT_INIT: View.() -> Unit = {}

fun ViewManager.toolbar(theme: Int = 0, init: Toolbar.() -> Unit = DEFAULT_INIT): Toolbar {
    return customView(theme, init)
}

fun Context.toolbar(theme: Int = 0, init: Toolbar.() -> Unit = DEFAULT_INIT): Toolbar {
    return customView(theme, init)
}

fun ViewManager.appbarLayout(theme: Int = 0, init: AppBarLayout.() -> Unit = DEFAULT_INIT): AppBarLayout {
    return customView(theme, init)
}

fun Context.appbarLayout(theme: Int = 0, init: AppBarLayout.() -> Unit = DEFAULT_INIT): AppBarLayout {
    return customView(theme, init)
}
```

## まとめ

`Component`には`View`としての振る舞いも持たせていいのかもなーとか思います。
例えば、`TwitterCardComponent`とかあったりしたらそれに、`setText`とか`setUserName`を定義するみたいな。