12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

キミがGroupieを使うワケ。

Last updated at Posted at 2022-03-28

この記事は

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

例とするアプリ:ZOZOTOWN

注意:わたしはZOZOのエンジニアではありません!ただのファッション大好きエンジニアです! :bow:

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

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

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

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

それでは、早速Groupieの良いところを紹介していきます。

Groupieのメリット

  • ViewHolderAdapterの記述量が減る

  • 複雑なリストが実装しやすい

    • 例えば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

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

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

  • HomeAllFragment
    実際のアプリを見てもらったら分かるんですがホーム画面には「すべて」「コスメ」「シューズ」の三つのページがあるので、ホーム画面の「すべて」の画面という意味でHomeAllFragmentという命名にしています。ここではRecyclerViewadapterを設定したり、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
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:

12
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?