AndroidでリアクティブなRecyclerViewを作る

# リアクティブなRecyclerView
![reactive_sample.gif](https://qiita-image-store.s3.amazonaws.com/0/309112/1d7d264a-a7d8-62fa-4f96-da726d02ee59.gif)

(リアクティブ感を出すために、FireStoreに直接インサートしたデモです)

## 本記事のポイント
- FireStoreのデータをRxKotlinを使ってViewに反映
- ViewModelからViewへの単方向DataBinding

## さらっと出てくるけど説明しないもの
- Dagger
- RecyclerViewの基本的な使い方

## Utility
RxkotlinとLiveDataの接続を容易にするためにextentionを定義します。

```kotlin
import androidx.lifecycle.LiveData
import androidx.lifecycle.LiveDataReactiveStreams
import org.reactivestreams.Publisher

fun <T> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this) as LiveData<T>

```

## 画面のレイアウト

```xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".presentation.main.TodoListFragment">

    <data>

        <variable
            name="fragment"
            type="sample.presentation.main.TodoListFragment" />

        <variable
            name="viewModel"
            type="sample.presentation.main.TodoListViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/todo_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffcccccc"
            android:clipToPadding="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/addButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="32dp"
            android:layout_marginBottom="32dp"
            android:clickable="true"
            android:focusable="true"
            android:tint="@android:color/holo_red_dark"
            android:onClick="@{() -> fragment.addButtonClicked()}"
            app:backgroundTint="@android:color/holo_red_light"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:srcCompat="@android:drawable/ic_input_add" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
```

### dataBindingで状態の変化に強いviewを作る

viewModelで公開しているLiveDataをxmlからdatabindingすることで、LiveDataのデータが変わると自動的にレイアウトも変わるようになる。
RxKotlin -> LiveData -> layoutとstreamが繋がり幸せ度が高い。

ボタンのクリックイベントはdatabindingでbinding先のメソッドを呼び出すようにしている。かつて、クリックイベントを直接メソッドに紐づける場合は、[butterknife](https://github.com/JakeWharton/butterknife)等でやられていた、今は公式のフレームワークで可能。

Activityのlayoutは何も書いていないので省略。

## activity

```kotlin
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import sample.R
import sample.databinding.ActivityMainBinding
import sample.presentation.NavigationController
import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject


class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var navigationController: NavigationController

    private val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding
        navigationController.navigateToTodoList()
    }
}

```

databindingを利用する場合はDataBindingUtilを利用する。ただ、bindingオブジェクトはclassの至る所で触るのでlazyで定義すると使いやすい。

## fragment

```kotlin
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import sample.databinding.FragmentTodoListBinding
import sample.di.ViewModelFactory
import sample.presentation.NavigationController
import dagger.android.support.DaggerFragment
import javax.inject.Inject


class TodoListFragment : DaggerFragment() {

    companion object {
        fun newInstance() = TodoListFragment()
    }

    @Inject lateinit var factory: ViewModelFactory
    @Inject lateinit var navigationController: NavigationController

    private val binding: FragmentTodoListBinding by lazy {
        FragmentTodoListBinding.inflate(requireActivity().layoutInflater).apply {
            viewModel = fragmentViewModel
            fragment = this@TodoListFragment
        }
    }
    private val fragmentViewModel: TodoListViewModel by lazy {
        ViewModelProviders.of(this, this.factory).get(TodoListViewModel::class.java)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return this.binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        this.binding.todoList.setHasFixedSize(true)
        this.binding.todoList.adapter = TodoRecyclerViewAdapter(this.viewLifecycleOwner, this.fragmentViewModel)
        this.binding.todoList.layoutManager = LinearLayoutManager(context)
    }

    /**
     * フロートボタンのイベントハンドラ。
     * layoutのxmlからイベントバインディングしている。
     */
    fun addButtonClicked() {
        val fragment = InputDialogFragment.newInstance()
        fragment.show(requireFragmentManager(), "tag")
    }
}

```

floatingActionButtonのイベントハンドラ先については、今回は関係ないので省略。

## viewModel

```kotlin
import androidx.lifecycle.ViewModel
import sample.repository.TodoRepository
import sample.util.ext.toLiveData
import io.github.droidkaigi.confsched2018.util.rx.SchedulerProvider
import javax.inject.Inject

class TodoListViewModel @Inject constructor(
        private val todoRepository: TodoRepository,
        schedulerProvider: SchedulerProvider
): ViewModel() {

    val todoData = this.todoRepository.getTodoList().subscribeOn(schedulerProvider.io()).toLiveData()

}

```

RepositoryのTodoListから受け取ったものをLiveDataに変換して外部に公開している。
これをlayoutでdatabindingしている。

## Repository

```kotlin
import sample.dataStore.TodoDataStore
import sample.model.Todo
import io.reactivex.Flowable
import io.reactivex.Single
import javax.inject.Inject

class TodoRepositoryImpl @Inject constructor(private val todoDataStore: TodoDataStore): TodoRepository {
    override fun getTodoList(): Flowable<List<Todo>> {
        return this.todoDataStore.getTodoDataList().map { it.sortedBy { it.registerDate } }
    }

    override fun addTodo(todo: Todo): Single<Unit> {
        return this.todoDataStore.addTodoData(todo)
    }
}
```

dataStoreから上がってきたものをソートして上に返しているだけ。

## dataStore

```kotlin
import com.google.firebase.Timestamp
import com.google.firebase.firestore.FirebaseFirestore
import sample.model.Todo
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.Observable
import io.reactivex.Single
import timber.log.Timber
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneId

class TodoDataStoreImpl: TodoDataStore {
    private val collectionPath = "todo"
    data class TodoForFireStore(val title: String,
                                val message: String,
                                val done: Boolean,
                                val registerDate: Timestamp)

    override fun addTodoData(todo: Todo): Single<Unit> {
        return Single.create<Unit> { observer ->
            val todoForFireStore = TodoForFireStore(
                    title = todo.title,
                    message = todo.message,
                    done = todo.done,
                    registerDate = Timestamp(todo.registerDate.toEpochSecond(), todo.registerDate.nano)
            )
            val db = FirebaseFirestore.getInstance()
            db.collection(this.collectionPath).add(todoForFireStore).addOnSuccessListener {reference ->
                Timber.d(reference.toString())
                observer.onSuccess(Unit)
            }.addOnFailureListener {
                Timber.d(it.toString())
                observer.onError(it)
            }
        }
    }

    override fun getTodoDataList(): Flowable<List<Todo>> {
        return Observable.create<List<Todo>> { observer ->
            val db = FirebaseFirestore.getInstance()
            db.collection(this.collectionPath).addSnapshotListener { snapshot, exception ->
                // もしエラーが起きてたら
                if (exception != null) {
                    Timber.d(exception.toString())
                    observer.onError(exception)
                    return@addSnapshotListener
                }

                if (snapshot == null || snapshot.isEmpty) {
                    Timber.d("data is empty")
                    observer.onNext(listOf())
                    return@addSnapshotListener
                }

                observer.onNext(snapshot.map{
                    val title = it.get("title") as String
                    val message = it.get("message") as String
                    val done = it.get("done") as Boolean
                    val registeredTimestamp = it.get("registerDate") as Timestamp
                    val registerDate = OffsetDateTime.ofInstant(
                            Instant.ofEpochMilli(registeredTimestamp.seconds * 1000),
                            ZoneId.systemDefault()
                    )
                    Todo(
                            title = title,
                            message = message,
                            done = done,
                            registerDate = registerDate
                    )
                })
            }
            return@create
        }.toFlowable(BackpressureStrategy.ERROR)
    }
}
```

FireStoreを扱うdataStore。裏側の実装がfireStoreであることは、ここで完全に隠蔽し、ここから上のレイヤ(RepositoryやviewModel)には、裏側の実装を意識させない。

## recyclerViewAdapter

```kotlin
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import sample.databinding.ItemBinding
import sample.model.Todo

class TodoRecyclerViewAdapter(lifeCycleOwner: LifecycleOwner, private val viewModel: TodoListViewModel):
        RecyclerView.Adapter<TodoRecyclerViewAdapter.BindingHolder>() {

    // Adapterが知っている一番最後のlist
    private var oldList: List<Todo> = listOf()

    // 初期化時にviewModelを購読して、変更があった場合、RecyclerViewに通知する
    init {
        this.viewModel.todoData.observe(lifeCycleOwner, Observer {
            this.updateList(it)
        })
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoRecyclerViewAdapter.BindingHolder {
        val binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return BindingHolder(binding)
    }

    override fun getItemCount(): Int = this.viewModel.todoData.value?.size ?: 0

    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        holder.binding.todo = this.oldList[position]
    }

    private fun updateList(newList: List<Todo>) {
        val diffResult = DiffUtil.calculateDiff(TodoRecyclerViewAdapter.TodoDiffUtilImpl(this.oldList, newList))
        diffResult.dispatchUpdatesTo(this)
        this.oldList = newList
    }

    data class BindingHolder(val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root)

    class TodoDiffUtilImpl(private val oldList: List<Todo>, private val newList: List<Todo>): DiffUtil.Callback() {
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
                = oldList[oldItemPosition].registerDate == newList[newItemPosition].registerDate

        override fun getOldListSize(): Int = oldList.size

        override fun getNewListSize(): Int = newList.size

        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
            return super.getChangePayload(oldItemPosition, newItemPosition)
        }

        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
                = oldList[oldItemPosition] == newList[newItemPosition]
    }
}

```

AdapterではtodoListに更新があった場合、変更を検知してリストを更新しなければならない。そのために、DiffUtilなるものが公式から提供されている。
二つのリストに違いがあった場合に、RecyclerView用の変更イベントを投げてくれる。
DiffUtilを継承したクラスで、リストのDiffとは何かを定義する。

# 最後に
RecyclerViewAdapterの構成を非常に悩んだ。
今でも`oldList`をAdapterで保持するのが正解かどうかわからない・・・
viewModelに持たせた方が良いような・・・。
でも`oldList`が必要なのはAdapterが原因な訳で、viewModelが他のクラスに影響されるのもどうなのかなーとか。

(ほとんど処理が無いアプリなので当たり前ですが)かなりスッキリかけたので、とりあえずこれで良いかな・・・

# 参考
- [droidkaigi2018](https://github.com/DroidKaigi/conference-app-2018)
- [DiffUtil + DataBindingでお手軽リスト表示](https://dev.classmethod.jp/smartphone/android/android_diffutil_mvvm/)