キミがGroupieを使うワケ。


# この記事は
なぜ生のRecyclerViewではなく、Groupieを使うのか。
Groupieを選択することによる利点を、具体的なアプリを用いて説明していきます！

# 例とするアプリ：ZOZOTOWN
注意：わたしはZOZOのエンジニアではありません！ただのファッション大好きエンジニアです！ :bow:


わたしが個人的にZOZOTOWNのUIを真似て画面構成の練習をしていることと、ZOZOTOWNで実際にGroupieが使われているかどうかは置いといてGroupieの利点の説明をするのに分かりやすい画面になっていると考えたので、例としてあげる事にしました。

今回はZOZOTOWNの中でも、特にホーム画面に着目していきましょう。

なんか著作権的なアレがアレかもなので、画面のスクショではなくわたしのラクガキを見てください :bow:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/8cdf8a91-1028-d4fe-1539-eb65f36ff473.jpeg" width=70%>

雑ですが、ZOZOTOWNのホーム画面はこんな感じの構成になっています。
画面お上から下へ、性別切り替えタブやプロモーションカードなど、様々な特徴を持つセルがリストのようになって配置されているのが分かるかと思います。

それでは、早速Groupieの良いところを紹介していきます。
# Groupieのメリット
- `ViewHolder`や`Adapter`の記述量が減る
- 複雑なリストが実装しやすい
  - 例えばLINEのトーク履歴画面のように同じ形のセルが上から下にずらずら並んでいるようなUIの場合は`RecyclerView`で簡単に実装できます。しかし、上の画像にあるようなZOZOTOWNのホーム画面のUIのように、様々なタイプのセルが上から下にリスト形式に配置されているような複雑なデザインの場合は`Groupie`の方が得意です。

- 差分更新が楽になる
    - `Groupie`の場合、めんどくさい差分更新は全部中でやってくれます。例えば`isSameAs()`とかを実装するとどのタイミングで更新されるか裏でゴニョゴニョしてくれます。

    - また、差分更新を行いたい部分を詳細に設定するのも楽です。例えば`RecyclerView`の場合だと一部のセルだけ更新したい時にインデックス(`position`)でしか指定できないので不便です。上のZOZOTOWNのホーム画面の一番上(`position=0`)に性別切り替えタブがありますよね？このタブって「男性」と「女性」と「子供」の3タイプありますが、横並びで同じセルに入っているので`position`でいうと全部`0`で同じになってしまうんです。 つまり、「男性」のみ選択された場合の更新を行うのに、`RecyclerView`だと`position`指定だから実装が複雑になってしまいます。細かい設定がやりにくいんです。この辺り、`Groupie`だと簡単です。
    
- フィルタリングが楽
  - フィルタリングっていうのは例えばUberEatsとかだと、「ファストフードだけ表示する」みたいなことです。特定のキーワードだけピックアップしてリスト表示する時なども`RecyclerView`より`Groupie`の方が便利らしい。
    

# Groupieの使い方
Groupieの使い方を簡単に説明してみます。
例として、ZOZOTOWNのホーム画面の一番上のセルである、①切り替えタブをGroupieを用いて実装してみようと思います。

:cat: 注意：実際のアプリで本当にこの通りに実装されているわけではありません！見よう見まねです！

## 全体像
まず全体的なイメージとしてはこんな感じです。
![スクリーンショット 2022-03-27 19.03.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/015d9a12-6189-4d96-f88c-58eb2f880979.png)

画像上でも軽く説明していますが、ここでも登場人物を一応紹介します。
画像にはないクラスもあったりしますが、ご了承ください。

- **person_switch_tab.xml**
これは切り替えタブのレイアウトxmlです。ImageViewを3つ横に並べています。
タブがタップされた時の変化を受け取ってレイアウトに反映するためにdataBindingの設定をする必要があります。

- **HomeAllFragment**
実際のアプリを見てもらったら分かるんですがホーム画面には「すべて」「コスメ」「シューズ」の三つのページがあるので、ホーム画面の「すべて」の画面という意味で`HomeAllFragment`という命名にしています。ここでは`RecyclerView`に`adapter`を設定したり、`viewmodel`を呼び出したりします。

- **HomeAllViewModel** 
タブがタップされた時の状態の変化を管理します。また、`MutableLiveData()`を使ってレンダリングしたいデータを監視可能な状態にします。

- **HomeAllAdapter**
`HomeAllFragment`のレイアウトである`fragment_home_all.xml`には`RecyclerView`が置かれている。このRecyclerViewと、ここ(`RecyclerView`のセル)に追加したいアイテムとを紐付けるためにあります。今回は`Groupie`を使うので、`GroupAdapter`を継承します。


- **SwitchTabItem**
`RecyclerView`のセルに入れるアイテムに関する実装を行うクラスです。`Groupie`を使うので、`BindableItem`を継承します。`databinding`で画面に表示したいデータなどの設定を行なったり、`isSameAs()`や`hasSameContentAs()`などを使ってどのタイミングで差分更新を行うかなどの設定をします。

- **Gender**
「男性」「女性」「子供」を管理するための`enum class`です。
定数のデータなので`enum`で管理します。


## コードを見てみる

- **person_switch_tab.xml**
```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">

    <data>
        <variable
            name="listener"
            type="com.nemo.androiduitraining.view.fragment.home.SwitchTabItem.OnClickListener" />
        <import type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
        <variable
            name="selectedGender"
            type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/mens_tab"
            android:paddingVertical="8dp"
            app:onClick="@{()->listener.onGenderClick(Gender.MAN)}"
            app:tintColor="@{selectedGender==Gender.MAN ? @color/person_image_blue : @color/text_gray}"
            app:isSelected="@{selectedGender==Gender.MAN}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/ic_mens_before"
            android:layout_weight="1" />

        <ImageView
            android:id="@+id/ladies_tab"
            android:paddingVertical="8dp"
            app:onClick="@{()->listener.onGenderClick(Gender.WOMAN)}"
            app:tintColor="@{selectedGender==Gender.WOMAN ? @color/person_image_pink : @color/text_gray}"
            app:isSelected="@{selectedGender==Gender.WOMAN}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/ic_ladies_before"
            android:layout_weight="1" />

        <ImageView
            android:id="@+id/kids_tab"
            android:paddingVertical="8dp"
            app:onClick="@{()->listener.onGenderClick(Gender.KIDS)}"
            app:tintColor="@{selectedGender==Gender.KIDS ? @color/person_image_yellow : @color/text_gray}"
            app:isSelected="@{selectedGender==Gender.KIDS}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_kids_before"
            android:layout_gravity="center"
            android:layout_weight="1" />

    </LinearLayout>
</layout>

```

- **HomeAllFragment**
```HomeAllFragment.kt
class HomeAllFragment : Fragment(R.layout.fragment_home_all) {
    companion object {
        fun newInstance() = HomeAllFragment()
    }

    private val homeAllAdapter = HomeAllAdapter()
    private val viewModel: HomeAllViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val binding = FragmentHomeAllBinding.bind(view)
        binding.allRecyclerView.adapter = homeAllAdapter
        viewModel.renderData.observe(viewLifecycleOwner) {
            homeAllAdapter.update(it, viewModel)
        }
    }
}
```

- **HomeAllViewModel** 
```HomeAllViewModel.kt
@HiltViewModel
class HomeAllViewModel @Inject constructor() : ViewModel(), SwitchTabItem.OnClickListener {
    val renderData = MutableLiveData<RenderData>(RenderData(Gender.MAN))
    data class RenderData(val selectedGender: Gender)

    override fun onGenderClick(gender: Gender) {
        renderData.value = renderData.value?.copy(selectedGender = gender)
    }
}
```

- **HomeAllAdapter**
```HomeAllAdapter.kt
class HomeAllAdapter : GroupAdapter<GroupieViewHolder>() {
    fun update(renderData: HomeAllViewModel.RenderData, listener: SwitchTabItem.OnClickListener) {
        val group = mutableListOf<BindableItem<out ViewBinding>>()
        group.add(SwitchTabItem(renderData.selectedGender, listener))

        updateAsync(group)
    }
}
```

- **SwitchTabItem**
```SwitchTabItem.kt
class SwitchTabItem(private val selectedGender: Gender, private val listener: OnClickListener) : BindableItem<PersonSwitchTabBinding>() {
    override fun bind(viewBinding: PersonSwitchTabBinding, position: Int) {
        viewBinding.listener = listener
        viewBinding.selectedGender = selectedGender
        viewBinding.executePendingBindings()
    }

    override fun getLayout(): Int = R.layout.person_switch_tab

    override fun initializeViewBinding(view: View): PersonSwitchTabBinding = PersonSwitchTabBinding.bind(view)
    override fun isSameAs(other: Item<*>): Boolean = other is SwitchTabItem
    override fun hasSameContentAs(other: Item<*>): Boolean = (other as? SwitchTabItem)?.selectedGender == selectedGender

    interface OnClickListener {
        fun onGenderClick(gender: Gender)
    }
}
```

- **Gender**
```Gender.kt
enum class Gender {
    MAN,
    WOMAN,
    KIDS
}
```


# Groupieに似てるやつ
- ListAdapter
  - 差分更新が楽ですけど、シンプルなUIにしか向いていない
- Epoxy
  - たぶんGroupieと遜色ないと思うのでどっちを選んでも良い気がする 

# 終わりに
終わりです。全部の説明を細かくできたわけではないので、いろいろ雑ですが、
Groupieの特徴や利点が伝わっていれば良いなと思います〜 :dog: