Kotlin Coroutines Flow + LiveData + Two-way Data Binding

## Kotlin Coroutines Flow

Kotlin Coroutines 1.2.0から`Flow`というCold Streamを実現する仕組みが導入されました。これまでは`Channel`というHot Streamの仕組みがあったのでRxにだいぶ近づいてきた感じがあります。
この記事では、FlowとLiveData、双方向データバインディングを使っていい感じにUIのイベントを処理してみます。

## LiveData as Flow

まずは、LiveDataにセットされた値をFlowとして扱うための拡張関数を作ります。`Channel`を作るときは`channel`や`produce`といったChannel Builderで作っていましたが、`Flow`では`flow`や`flowViaChannel`というFlow Builderが用意されています。
`flow`では`Flow`に値を流す処理`emit`がsuspend関数になっており、`Observer`の`onChanged`から直接実行できないため`flowViaChannel`の`offer`を使っています。

```kotlin:LiveDataExtensions.kt
fun <T> LiveData<T>.asFlow() = flowViaChannel<T?> {
    it.offer(value)
    val observer = Observer<T> { t -> it.offer(t) }
    observeForever(observer)
    it.invokeOnClose {
        removeObserver(observer)
    }
}
```

## LiveData + Two-way Data Binding

これは従来のものとなにも変わりません。LiveDataを双方向データバインディングでビューと結びつけます。今回は2つのEditTextのテキストに設定しています。

```kotlin:MainViewModel
class MainViewModel : ViewModel() {
    val org = MutableLiveData<String>()
    val repository = MutableLiveData<String>()
}
```
```xml:activity_main.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"
>
    <data>
        <variable
            name="viewModel"
            type="com.chibatching.flowreactiveuisample.MainViewModel"
        />
    </data>
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
    >
        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
        >
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
            >
                <EditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:hint="org"
                    android:text="@={viewModel.org}"
                />
                <EditText
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:hint="repository"
                    android:text="@={viewModel.repository}"
                />
            </LinearLayout>
        </com.google.android.material.appbar.AppBarLayout>
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            tools:listitem="@layout/list_item_result"
        />
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
```

## Flow + LiveData = Reactive UI

今回は2つのEditTextのうち1つにUser/Organization、もう1つにリポジトリ名を入力してGitHubのリポジトリをインクリメンタルに検索するものを作ります。
検索結果は`RecyclerView`でEditTextの下に表示します。

```kotlin:MainActivity
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by lazy {
        ViewModelProviders.of(this).get(MainViewModel::class.java)
    }

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

    private val adapter = RepoListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this
        binding.recyclerView.adapter = adapter
        viewModel.repos.observe(this, Observer { repos ->
            repos?.let { adapter.repos = it }
            adapter.notifyDataSetChanged()
        })
    }
}
```

reposという名前の`LiveData`を`observe`して変更が来たら`RecyclerView`を更新するシンプルな作りです。
先程の`MainViewModel`にreposを実装します。

```kotlin:MainViewModel
class MainViewModel : ViewModel() {
    val org = MutableLiveData<String>()
    val repository = MutableLiveData<String>()

    val repos = object : MutableLiveData<List<Repo>>() {
        override fun onActive() {
            value?.let { return }

            viewModelScope.launch {
                var job: Deferred<Unit>? = null
                org.asFlow()
                    .combineLatest(repository.asFlow()) { org, repo ->
                        Pair(org, repo) // 2つのEditTextの最新の値を合成する
                    }
                    .collect { // ここから値が流れてきたときの処理
                        job?.cancel() // 検索中に値が入力されたときは検索をキャンセルする
                        job = async(Dispatchers.Main) {
                            value = searchRepository(it.first, it.second)
                        }
                    }
            }
        }
    }

    private suspend fun searchRepository(org: String?, repo: String?): List<Repo> {
        // 省略
    }
}
```

これだけでEditTextの値が編集される度に検索を行い、結果をリストに表示することができました。

## もうちょっといい感じに

1文字入力される度に毎回検索が走るのは、変換途中だったりするためあまり使い勝手が良くない＆通信が激増するのでRxJavaでよくやるような一定時間ウェイトを入れて入力が無ければ処理を実行する(≒debounce)を実装してみます。
`Flow`にはすでにいくつかのオペーレーターが実装されているのですが、残念ながらこのdebounceに相当するようなオペーレーターはまだありません。
ただ、自分自身でオペーレーターを実装することもKotlin Coroutinesの世界でできるため難しくありません。debounceの処理はこんな感じに実装できました。

<b>追記：</b> Kotlin Coroutines 1.2.1で`debounce`オペレーターが実装され自分で作る必要がなくなりました。:tada:

```kotlin:FlowExtensions.kt
fun <T> Flow<T>.debounce(waitMillis: Long) = flow {
    coroutineScope {
        val context = coroutineContext
        var delayPost: Deferred<Unit>? = null
        collect {
            delayPost?.cancel()
            delayPost = async(Dispatchers.Default) {
                delay(waitMillis)
                withContext(context) {
                    // emitはContextの変更を許していないので元のContextで実行されるようにする
                    emit(it)
                }
            }
        }
    }
}
```

これを`MainViewModel`に追加します。

```kotlin:MainViewModel
class MainViewModel : ViewModel() {
    val org = MutableLiveData<String>()
    val repository = MutableLiveData<String>()

    val repos = object : MutableLiveData<List<Repo>>() {
        override fun onActive() {
            value?.let { return }

            viewModelScope.launch {
                var job: Deferred<Unit>? = null
                org.asFlow()
                    .combineLatest(repository.asFlow()) { org, repo ->
                        Pair(org, repo) // 2つのEditTextの最新の値を合成する
                    }
                    .debounce(500) // 入力を500ms待つ
                    .distinctUntilChanged() // 待った結果、値に変更がないときは無視する(標準で用意されているオペーレーター)
                    .collect { // ここから値が流れてきたときの処理
                        job?.cancel() // 検索中に値が入力されたときは検索をキャンセルする
                        job = async(Dispatchers.Main) {
                            value = searchRepository(it.first, it.second)
                        }
                    }
            }
        }
    }

    private suspend fun searchRepository(org: String?, repo: String?): List<Repo> {
        // 省略
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53706/aa829149-fc35-3590-5ec1-0674d388bcd8.gif" width=360 />

## おわり

ということで、`Flow`と`LiveData`, `DataBinding`を使って簡単にEditTextの変更を扱うことができました。
RxJavaを使えばできるけどKotlin Coroutinesだと大変だなーというところがまた一つ減った感じですね〜。

全体のコードはこちらです↓
[chibatching/FlowReactiveUiSample](https://github.com/chibatching/FlowReactiveUiSample)