ZOZOのアプリのUIを再現しようの会 【お気に入りタブ編】その2

##前回の記事
https://qiita.com/nemo-855/items/3707e095c000f89ddb62

##はじめに
お気に入り画面のアイテムタブを作りました！そこ周りの実装について書いていきます！時系列的には先に新着タブを作っているのですが、先にこちらを記事にしました！

##今回の完成像

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">アディパンしか扱ってないZOZO <a href="https://t.co/2QhvDf1upm">pic.twitter.com/2QhvDf1upm</a></p>&mdash; ねも (@nemo_855) <a href="https://twitter.com/nemo_855/status/1449050123594465282?ref_src=twsrc%5Etfw">October 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##全体の構成
この画面全体の構成として、FavoriteFragmentという親FragmentがViewPager2とTabLayoutを持っています。その中で3つのFragmentを切り替えています。

このアイテムのタブのFragment(以下FavoriteItemFragment)は画面全体がRecyclerViewで作られてます。この画面のようにリストの要素が複数（今回はお気に入りのブランドの登録がありません、今人気のアイテム、一つ一つの商品のセルの３種類）ある場合にはViewHolderを分岐する方法が考えられます。ただViewHolderの分岐はRecyclerView.AdapterもしくはListAdapterのonCreateViewHolderの内部でViewTypeを用いて生成するViewHolderを分岐して、さらにonBindViewHolderでViewHolderごとにbindの処理を分岐するということをやらないといけないです。

そのような処理をしているとコード量が多くなってしまいすぎると思ったので今回は[Groupie](https://github.com/lisawray/groupie)を採用することにしました！GroupieはRecyclerView.ViewHolderを継承しているBindableItemというクラスを使ってそれをレイアウトの種類ごとに分岐して継承することで直接ViewHolderを扱わなくてもいい感じにRecyclerView内部のレイアウトを分岐してくれるというライブラリです！

また差分更新もListAdapterと同じで、DiffUtilという二つのリストの差分を計算してくれるクラスが管理してくれます！（DiffUtilの中でどのような処理がされているかまではわからないです🙇‍♂️勉強します！！）なのでGroupieを使う人は表示したいリストの管理だけしてそれをAdapterに渡すだけでよくなります。
##全体のコード

FavoriteItemFragment

```kotlin
@AndroidEntryPoint
class FavoriteItemFragment : Fragment(R.layout.fragment_favorite_item) {
    companion object {
        fun newInstance() = FavoriteItemFragment()
    }

    private val viewModel: FavoriteItemViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val binding = FragmentFavoriteItemBinding.bind(view)

        val adapter = CustomGroupieAdapter()
        fetchClothsAndUpdateList(adapter)
        setupRecycler(binding, adapter)
    }

    private fun fetchClothsAndUpdateList(adapter: CustomGroupieAdapter) {
        viewModel.clothsLD.observe(viewLifecycleOwner) { cloths ->
            adapter.updateList(cloths)
        }
        viewModel.fetchDisplayClothsList()
    }

    private fun setupRecycler(
        binding: FragmentFavoriteItemBinding,
        adapter: CustomGroupieAdapter
    ) {
        val spanSize = DisplayItemKind.values().maxOf { it.spanSize }
        val gridLayoutManager = GridLayoutManager(requireContext(), spanSize).also {
            it.spanSizeLookup = CustomGridSpanSizeLookup(adapter, resources)
        }
        binding.favoriteItemRecycler.layoutManager = gridLayoutManager
        binding.favoriteItemRecycler.adapter = adapter
    }

    private class CustomGroupieAdapter : GroupieAdapter() {
        private var _itemList: List<BindableItem<out ViewBinding>> = makeDefaultList()
        val itemList: List<BindableItem<out ViewBinding>>
            get() = _itemList

        private fun makeDefaultList() = listOf(
            FavoriteNoItemRegistered(),
            FavoriteNowPopularItem()
        )

        fun updateList(newItemList: List<FavoriteItemViewModel.DisplayClothsData>) {
            _itemList = makeDefaultList() + newItemList.map {
                FavoriteItemDescription(displayData = it)
            }
            update(_itemList)
        }
    }

    private class CustomGridSpanSizeLookup(
        private val adapter: CustomGroupieAdapter,
        private val res: Resources
    ) : GridLayoutManager.SpanSizeLookup() {
        override fun getSpanSize(position: Int): Int {
            return when (adapter.itemList[position]) {
                is FavoriteNoItemRegistered -> DisplayItemKind.NO_ITEM.spanSize
                is FavoriteNowPopularItem -> DisplayItemKind.POPULAR.spanSize
                is FavoriteItemDescription -> DisplayItemKind.DESCRIPTION.spanSize
                else -> throw IllegalArgumentException(res.getString(R.string.illegal_class))
            }
        }
    }

    private enum class DisplayItemKind(val spanSize: Int) {
        NO_ITEM(3), POPULAR(3), DESCRIPTION(1)
    }
}
```

またそれぞれのBindableItemを継承したクラスの実装はこんな感じです

お気に入りのアイテムがありませんのセル

```kotlin
class FavoriteNoItemRegistered : BindableItem<FavoriteItemNoItemRegisteredBinding>() {
    override fun bind(viewBinding: FavoriteItemNoItemRegisteredBinding, position: Int) {

    }

    override fun getLayout() = R.layout.favorite_item_no_item_registered

    override fun initializeViewBinding(view: View): FavoriteItemNoItemRegisteredBinding {
        return FavoriteItemNoItemRegisteredBinding.bind(view)
    }
}
```

今人気のアイテムのセル

```kotlin
class FavoriteNowPopularItem : BindableItem<FavoriteItemNowPopularBinding>() {
    override fun bind(viewBinding: FavoriteItemNowPopularBinding, position: Int) {

    }

    override fun getLayout() = R.layout.favorite_item_now_popular

    override fun initializeViewBinding(view: View): FavoriteItemNowPopularBinding {
        return FavoriteItemNowPopularBinding.bind(view)
    }
}
```
一つ一つの商品のセル

```kotlin
class FavoriteItemDescription(
    private val displayData: FavoriteItemViewModel.DisplayClothsData
) : BindableItem<FavoriteItemDescriptionBinding>() {
    override fun bind(viewBinding: FavoriteItemDescriptionBinding, position: Int) {
        viewBinding.clothsNameTv.text = displayData.itemName
        viewBinding.clothsGenreTv.text = displayData.itemGenre
        viewBinding.clothsPriceTv.text = displayData.itemPrice.toString()

        viewBinding.mainImage.load(displayData.itemImage) {
            this.error(R.drawable.ic_android_black_24dp)
        }

        when (val percent = displayData.discountPercent) {
            null -> viewBinding.discountPercentTv.visibility = View.GONE
            else -> {
                viewBinding.discountPercentTv.visibility = View.VISIBLE
                viewBinding.discountPercentTv.text = percent.toString()
            }
        }

        when (val price = displayData.couponPrice) {
            null -> viewBinding.couponPriceTv.visibility = View.GONE
            else -> {
                viewBinding.couponPriceTv.visibility = View.VISIBLE
                viewBinding.couponPriceTv.text = price.toString()
            }
        }
    }

    override fun getLayout() = R.layout.favorite_item_description

    override fun initializeViewBinding(view: View): FavoriteItemDescriptionBinding {
        return FavoriteItemDescriptionBinding.bind(view)
    }
}
```
このFragmentのViewModel内部に定義している、商品一つ一つの表示データのモデルクラス

```kotlin
data class DisplayClothsData(
        val itemImage: String,
        val discountPercent: Int?,
        val couponPrice: Int?,
        val itemName: String,
        val itemGenre: String,
        val itemPrice: Int
    )
```

##終わりに
次は一番詰まったCoordinatorLayoutについて記事にしようと思います！！まだ正直理解が曖昧なので頑張ります💪


##ハッカソンのメンバー
レビュアー: どすこいさん
https://twitter.com/dosukoi_android

ホームタブ担当(&主催): みっちゃんさん
https://twitter.com/mimimi_engineer

探すタブ担当: らべさん
https://twitter.com/wawatantanwawa

他２人

##リポジトリ
https://github.com/iroha-168/AndroidUITrain

##参考文献
https://github.com/lisawray/groupie