複数項目を表示するAdapterをKoinでDIしながら実装する

# はじめに
この記事は[ロボP Advent Calendar 2018](https://qiita.com/advent-calendar/2018/robop)の15日目の記事です．

# Koinとは
AndroidのKotlin向けDIライブラリらしいです。
すごい簡単に使えたので僕は好きです。
詳しくは[こちら](https://github.com/InsertKoinIO/koin)

# やりたいこと
一般的な同じLayoutが並んでいるAdapterではなく、複数のLayoutが並んでるAdapterを作ります。
どうせならその要素一つ一つをDIで読み込んだらイケてそうだねって気がしたのでKoinの登場です。

<img width="399" src="https://qiita-image-store.s3.amazonaws.com/0/126115/43e8c6b4-d3ad-96d3-4ac5-9245450a8483.png">

# 実装する

## 土台づくり

### 準備

まず必要なライブラリをsyncします。
今回は`Koin`とList表示に`RecyclerView`を使いました。

``` build.gradle
//RecyclerView
implementation 'com.android.support:recyclerview-v7:28.0.0'
//koin
implementation 'org.koin:koin-android:0.9.3'
```

### Layout

ActivityのLayout

``` activity_main.xml
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:orientation="vertical"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"/>

    </android.support.constraint.ConstraintLayout>

</layout>
```

Listの要素を保持する空のLayout

```layout_container.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/container"
        android:orientation="vertical">
</LinearLayout>
```

１つ目の項目のlayout

```layout_normal.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:text="normal view !"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

</android.support.constraint.ConstraintLayout>
```

### Koin

まずModule化するクラスのInterfaceを定義します。

```ItemContract.kt

interface ItemContract {
    val isVisible: Boolean
    fun getView(context: Context): View
}
```

それを使って１つ目の項目のクラスを作成します。
項目内で処理をしたい場合はここで設定します。

```NormalItem.kt

class NormalItem(override val isVisible: Boolean) : ItemContract {
    override fun getView(context: Context): View {
        return LayoutInflater.from(context).inflate(R.layout.layout_normal, null)
    }
}
```

DIをアプリ起動時に行うので、Applicationを継承したカスタムApplicationクラスを作ります。

```App.kt

class App : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin(
            this, listOf(
                this.itemModule
            )
        )
    }

    private val itemModule: Module = applicationContext {
        factory("normal") { NormalItem(isVisible = true) as ItemContract }
    }
}
```

作成した`App.kt`をManifestファイルに設定します。

```AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          package="com.example.s04341.ditest">

    <application
            android:name=".App"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme"
            tools:ignore="GoogleAppIndexingWarning">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>
```

### Adapter

複数項目表示するAdapterを作成します。
空のLinearLayoutのみをItemのLayoutとして持つAdapterに、
先程作成したInterfaceを実装するItemから読み込ませるViewを変えることで複数項目を出しています。
なので保持するItemsは`List<ItemContract>`になっています。

```MultiAdapter.kt

class MultiAdapter(
    val context: Context,
    val items: ArrayList<ItemContract>
) : RecyclerView.Adapter<MultiAdapter.ViewHolder>() {

    val layoutInflater = LayoutInflater.from(context)

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(layoutInflater.inflate(R.layout.layout_container, viewGroup, false))
    }

    override fun getItemCount(): Int {
        return items.size
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, pos: Int) {
        viewHolder.container.removeAllViews()
        viewHolder.container.addView(items[pos].getView(context).rootView)
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val container: LinearLayout by lazy {
            itemView.findViewById(R.id.container) as LinearLayout
        }
    }
}
```

### Activity

ActivityでKoinでDIされた項目を`Inject()`して受け取り、それをList化してAdapterにわたすことで反映しています。

```MainActivity.kt

class MainActivity : AppCompatActivity() {

    private val normalItem: ItemContract by inject("normal")

    private val recyclerView: RecyclerView by lazy {
        findViewById<RecyclerView>(R.id.recycler_view)
    }

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

        val items = ArrayList<ItemContract>()

        items.add(normalItem)

        recyclerView.setHasFixedSize(true)

        val multiAdapter = MultiAdapter(this, items)
        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        recyclerView.adapter = multiAdapter
    }
}
```

### 実行！

`NormalItem.kt`で設定したLayoutが表示されたかと思います。

<img src="https://qiita-image-store.s3.amazonaws.com/0/126115/11d07b61-8c05-3fc3-fdd3-0a8771f9cbac.png" width=50%>

## 二つ目の項目を追加する

### 項目の作成

違うViewを表示させてみたいですよね。
土台は出来てるので、あとは２つ目の項目のItemを作成するだけです。

```SpecialItem.kt
class SpecialItem(override val isVisible: Boolean) : ItemContract {
    override fun getView(context: Context): View {
        return LayoutInflater.from(context).inflate(R.layout.layout_special, null)
    }
}
```

```layout_special.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:textColor="#ff0000"
            android:textStyle="bold"
            android:text="special view !"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

</android.support.constraint.ConstraintLayout>
```

### 実装

これらを`App.kt`と`MainActivity.kt`に追加します。

```App.kt
class App : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin(
            this, listOf(
                this.itemModule
            )
        )
    }

    private val itemModule: Module = applicationContext {
        factory("normal") { NormalItem(isVisible = true) as ItemContract }
        factory("sp") { SpecialItem(isVisible = true) as ItemContract }
    }
}
```

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val normalItem: ItemContract by inject("normal")
    private val spItem: ItemContract by inject("sp")

    private val recyclerView: RecyclerView by lazy {
        findViewById<RecyclerView>(R.id.recycler_view)
    }

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

        val items = ArrayList<ItemContract>()

        items.add(normalItem)
        items.add(spItem)

        recyclerView.setHasFixedSize(true)

        val multiAdapter = MultiAdapter(this, items)
        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        recyclerView.adapter = multiAdapter
    }
}
```

### 実行！

別のLayoutの項目が表示されたかと思います。
<img src="https://qiita-image-store.s3.amazonaws.com/0/126115/4aae9e96-5329-e389-5d77-36db258aaedc.png" width=50%>

## 余談

例えば
`12月のみSpecialLayoutを表示したい`
などの場合は`App.kt`で条件を書いて別インスタンスを作成すればいけます。

```App.kt
...

    private val itemModule: Module = applicationContext {
        factory("normal") { NormalItem(isVisible = true) as ItemContract }

        val df = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
        val date = Date(System.currentTimeMillis())
        if (date.month + 1 == 12) {
            factory("sp") { SpecialItem(isVisible = true) as ItemContract }
        } else {
            factory("sp") { SpecialItem(isVisible = false) as ItemContract }
        }
    }
```

```MainActivity.kt
...

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

        val items = ArrayList<ItemContract>()

        items.add(normalItem)

        if (spItem.isVisible) {
            items.add(spItem)
        }

        recyclerView.setHasFixedSize(true)

        val multiAdapter = MultiAdapter(this, items)
        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        recyclerView.adapter = multiAdapter
    }
```