Posted at

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


はじめに

この記事はロボP Advent Calendar 2018の15日目の記事です.


Koinとは

AndroidのKotlin向けDIライブラリらしいです。

すごい簡単に使えたので僕は好きです。

詳しくはこちら


やりたいこと

一般的な同じLayoutが並んでいるAdapterではなく、複数のLayoutが並んでるAdapterを作ります。

どうせならその要素一つ一つをDIで読み込んだらイケてそうだねって気がしたのでKoinの登場です。


実装する


土台づくり


準備

まず必要なライブラリを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>

1つ目の項目の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
}

それを使って1つ目の項目のクラスを作成します。

項目内で処理をしたい場合はここで設定します。


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が表示されたかと思います。


二つ目の項目を追加する


項目の作成

違うViewを表示させてみたいですよね。

土台は出来てるので、あとは2つ目の項目の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.ktMainActivity.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の項目が表示されたかと思います。


余談

例えば

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
}