##前回の記事
https://qiita.com/nemo-855/items/3707e095c000f89ddb62
##はじめに
お気に入り画面のアイテムタブを作りました!そこ周りの実装について書いていきます!時系列的には先に新着タブを作っているのですが、先にこちらを記事にしました!
##今回の完成像
アディパンしか扱ってないZOZO pic.twitter.com/2QhvDf1upm
— ねも (@nemo_855) October 15, 2021
##全体の構成
この画面全体の構成として、FavoriteFragmentという親FragmentがViewPager2とTabLayoutを持っています。その中で3つのFragmentを切り替えています。
このアイテムのタブのFragment(以下FavoriteItemFragment)は画面全体がRecyclerViewで作られてます。この画面のようにリストの要素が複数(今回はお気に入りのブランドの登録がありません、今人気のアイテム、一つ一つの商品のセルの3種類)ある場合にはViewHolderを分岐する方法が考えられます。ただViewHolderの分岐はRecyclerView.AdapterもしくはListAdapterのonCreateViewHolderの内部でViewTypeを用いて生成するViewHolderを分岐して、さらにonBindViewHolderでViewHolderごとにbindの処理を分岐するということをやらないといけないです。
そのような処理をしているとコード量が多くなってしまいすぎると思ったので今回はGroupieを採用することにしました!GroupieはRecyclerView.ViewHolderを継承しているBindableItemというクラスを使ってそれをレイアウトの種類ごとに分岐して継承することで直接ViewHolderを扱わなくてもいい感じにRecyclerView内部のレイアウトを分岐してくれるというライブラリです!
また差分更新もListAdapterと同じで、DiffUtilという二つのリストの差分を計算してくれるクラスが管理してくれます!(DiffUtilの中でどのような処理がされているかまではわからないです🙇♂️勉強します!!)なのでGroupieを使う人は表示したいリストの管理だけしてそれをAdapterに渡すだけでよくなります。
##全体のコード
FavoriteItemFragment
@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を継承したクラスの実装はこんな感じです
お気に入りのアイテムがありませんのセル
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)
}
}
今人気のアイテムのセル
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)
}
}
一つ一つの商品のセル
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内部に定義している、商品一つ一つの表示データのモデルクラス
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
他2人