この記事は
なぜ生のRecyclerViewではなく、Groupieを使うのか。
Groupieを選択することによる利点を、具体的なアプリを用いて説明していきます!
例とするアプリ:ZOZOTOWN
注意:わたしはZOZOのエンジニアではありません!ただのファッション大好きエンジニアです!
わたしが個人的にZOZOTOWNのUIを真似て画面構成の練習をしていることと、ZOZOTOWNで実際にGroupieが使われているかどうかは置いといてGroupieの利点の説明をするのに分かりやすい画面になっていると考えたので、例としてあげる事にしました。
今回はZOZOTOWNの中でも、特にホーム画面に着目していきましょう。
なんか著作権的なアレがアレかもなので、画面のスクショではなくわたしのラクガキを見てください

雑ですが、ZOZOTOWNのホーム画面はこんな感じの構成になっています。
画面お上から下へ、性別切り替えタブやプロモーションカードなど、様々な特徴を持つセルがリストのようになって配置されているのが分かるかと思います。
それでは、早速Groupieの良いところを紹介していきます。
Groupieのメリット
-
ViewHolder
やAdapter
の記述量が減る -
複雑なリストが実装しやすい
- 例えばLINEのトーク履歴画面のように同じ形のセルが上から下にずらずら並んでいるようなUIの場合は
RecyclerView
で簡単に実装できます。しかし、上の画像にあるようなZOZOTOWNのホーム画面のUIのように、様々なタイプのセルが上から下にリスト形式に配置されているような複雑なデザインの場合はGroupie
の方が得意です。
- 例えばLINEのトーク履歴画面のように同じ形のセルが上から下にずらずら並んでいるようなUIの場合は
-
差分更新が楽になる
-
Groupie
の場合、めんどくさい差分更新は全部中でやってくれます。例えばisSameAs()
とかを実装するとどのタイミングで更新されるか裏でゴニョゴニョしてくれます。 -
また、差分更新を行いたい部分を詳細に設定するのも楽です。例えば
RecyclerView
の場合だと一部のセルだけ更新したい時にインデックス(position
)でしか指定できないので不便です。上のZOZOTOWNのホーム画面の一番上(position=0
)に性別切り替えタブがありますよね?このタブって「男性」と「女性」と「子供」の3タイプありますが、横並びで同じセルに入っているのでposition
でいうと全部0
で同じになってしまうんです。 つまり、「男性」のみ選択された場合の更新を行うのに、RecyclerView
だとposition
指定だから実装が複雑になってしまいます。細かい設定がやりにくいんです。この辺り、Groupie
だと簡単です。
-
-
フィルタリングが楽
- フィルタリングっていうのは例えばUberEatsとかだと、「ファストフードだけ表示する」みたいなことです。特定のキーワードだけピックアップしてリスト表示する時なども
RecyclerView
よりGroupie
の方が便利らしい。
- フィルタリングっていうのは例えばUberEatsとかだと、「ファストフードだけ表示する」みたいなことです。特定のキーワードだけピックアップしてリスト表示する時なども
Groupieの使い方
Groupieの使い方を簡単に説明してみます。
例として、ZOZOTOWNのホーム画面の一番上のセルである、①切り替えタブをGroupieを用いて実装してみようと思います。
注意:実際のアプリで本当にこの通りに実装されているわけではありません!見よう見まねです!
全体像
画像上でも軽く説明していますが、ここでも登場人物を一応紹介します。
画像にはないクラスもあったりしますが、ご了承ください。
-
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 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
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
@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
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
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
enum class Gender {
MAN,
WOMAN,
KIDS
}
Groupieに似てるやつ
- ListAdapter
- 差分更新が楽ですけど、シンプルなUIにしか向いていない
- Epoxy
- たぶんGroupieと遜色ないと思うのでどっちを選んでも良い気がする
終わりに
終わりです。全部の説明を細かくできたわけではないので、いろいろ雑ですが、
Groupieの特徴や利点が伝わっていれば良いなと思います〜