ConstraintLayout FlowでViewを動的に追加してみる

こんにちは。株式会社ZOZOでAndroidエンジニアをしております@zzt-osamuhanzawaです。[ZOZO Advent Calendar 2021](https://qiita.com/advent-calendar/2021/zozo)のその3の15日目の担当をさせていただきます。今回は実務で触った`ConstraintLayout Flow`（以下、Flowと略）について少しだけ書きたいと思います。
## ConstraintLayout Flowとは
まず、なんじゃそれ？という方がいるかもしれないので簡単に説明しますと、例えば以下のQiitaハッシュタグのような連続したUIを実装したいときに利用されるヘルパーウィジェットでConstraintLayout2.0から追加されました。こういったUIは今まではFlexboxLayoutやGridLayoutなどで実装しておりましたが、Flowが追加されたことでシンプルに実装することができるらしい、とのことです。らしいというのは実現したいことを実装しようとすると、そんなシンプルにいかなかったからです。
<img width="300" alt="2021-12-13 18.36.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218011/125d642d-dec5-10ff-6bd3-459b36464859.png">

## やりたいこと
Attributeなどに関しては[公式](https://developer.android.com/reference/androidx/constraintlayout/helper/widget/Flow)、または、説明をしてくれている記事はそれなりにあるため割愛します。結論から何がしたかったかと言うとFlowにViewを動的に追加することです。弊社ではWEARというサービスを展開しておりますが、そのAndroidアプリで以下のようなハッシュタグをタイル表示をしているUIが多数あり、これらをFlowに置き換えてみることにしました。また、これらのタグはAPIレスポンスで返却されるため、静的ではなく動的に追加する必要があり、少し実装に悩みました。
<img width="300" alt="2021-12-13 19.55.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218011/bd882679-c8cc-f567-caa5-1e1e26844aa5.png">

## 動的に追加する
早速ですがFlowでViewを動的に追加する方法を説明していきたいと思います。
なお、実装に関しては`DataBinding`の利用を想定しております。

### Layout
例えばこのような`layout.xml`があったとします。

```xml
        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/flowContainer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            >
            <androidx.constraintlayout.helper.widget.Flow
                android:id="@+id/flow"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                app:flow_horizontalBias="0"
                app:flow_horizontalGap="6dp"
                app:flow_horizontalStyle="packed"
                app:flow_verticalGap="6dp"
                app:flow_verticalStyle="packed"
                app:flow_wrapMode="chain"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                />
        </androidx.constraintlayout.widget.ConstraintLayout>
```
この`@+id/flow`に対する要素のレイアウトがこんな感じでファイルが`flow_row.xml`とします

```xml
<androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        >
        <TextView
            android:id="@+id/tag"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:lines="1"
            android:textColor="@color/gray"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            />
    </androidx.constraintlayout.widget.ConstraintLayout>
```
### 実装
実装に関しては単純に`layout.xml`の`@+id/flow`に要素を突っ込んでいく感じで、親のViewとその子であるFlowに`addView()`をしていきます。

```kotlin
    private fun setUpTags(parent: ViewGroup, data: List<String>) {
        val flow = binding.flow
        data.forEach { str ->
            val view = DataBindingUtil.inflate<FrowRowBinding>(
                requireActivity().layoutInflater,
                R.layout.flow_row,
                parent,
                false
            ).apply {
                root.id = View.generateViewId()
            }

            view.tag.text = str

            parent.addView(view.root)
            flow.addView(view.root)
        }
    }
```
なお、Flowの内部をみてみると`addView()`で以下のような処理を行なっており、IDが設定されている必要があります。

``` java
        if (view.getId() == -1) {
            Log.e("ConstraintHelper", "Views added to a ConstraintHelper need to have an id");
            return;
        }
```

Flowへの動的なView追加はこれでひとまず実現できました。

### Viewのremoveについて
画面を引っ張ってデータを更新するPullToRefreshやonResumeのタイミングといったデータの更新が走る際に上記の`setUpTags()`のメソッドが必ず呼ばれる想定をすると、この実装は不完全です。なぜかというとこのメソッドが呼ばれる度に要素のViewが増殖する実装となります。そのためViewを追加する前に要素のViewがあった場合、Viewをremoveする必要があります。そしてFlowは`ViewGroup`ではないので`removeAllViews()`がありません。動的に追加するというタイトルでしたが少し悩んだというのは、こちらの方かもしれません。

そして、これを実現するために以下のフィールドを用意しました。

```kotlin
var referenceIds = mutableListOf<Int>()
```

このフィールドは何かというとFlowの要素を`inflate`する際に設定したIDを追加していくためのリストとなります。

```kotlin
            ).apply {
                root.id = View.generateViewId()
                referenceIds.add(root.id)
            }
```
このリストを`Flow#setReferencedIds()`というメソッドがあるので、こちらにセットします。なお、Flowの内部で`addView()`する際にreferenceIdをnullクリアしている処理があるため、この処理はaddView後に実行させる必要があります。

```kotlin
        flow.referencedIds = referenceIds.toIntArray()
```


そして、要素のViewを追加していくコードの前に以下のようなViewをクリアする処理を加えます。以下の実装が`removeAllViews()`の代わりとなります。`findViewById`で設定したIDをキーにして削除したいViewを指定するためにフィールドでIDのリストを保持するような実装になってます。

```kotlin
        referenceIds.forEach { referenceId ->
            parent.removeView(parent.findViewById(referenceId))
        }
        referenceIds.clear()
```
Flowに追加したViewの削除に関してはこの実装でうまくいきました。

## まとめ
ConstraintLayoutのヘルパーであるFlowにViewを動的に追加する方法や削除について簡単ですが以上です。この記事で同じようなことで悩む人の時間が少しでも減れば嬉しいです。