AdapterDelegates icon indicating copy to clipboard operation
AdapterDelegates copied to clipboard

Reuse AdapterDelegate<List<T>> in a domain specific way

Open vanniktech opened this issue 4 years ago • 3 comments

Use case:

I've got a few layouts which I can reuse on different screens. Think of an empty screen. Empty screen consists of an Emoji, text and also subtitle.

data class EmptyState(
  val emoji: String,
  val title: String,
  val subtitle: String
)

I want to reuse code for inflation + binding, so I've created:

object AdapterDelegateFactory {
  fun emptyState() = adapterDelegate<EmptyState, EmptyState>(R.layout.adapter_item_empty_state) {
    val emoji by lazy(NONE) { itemView.findViewById<TextView>(R.id.adapterItemEmptyStateEmoji) }
    val title by lazy(NONE) { itemView.findViewById<TextView>(R.id.adapterItemEmptyStateTitle) }
    val subtitle by lazy(NONE) { itemView.findViewById<TextView>(R.id.adapterItemEmptyStateSubtitle) }

    bind {
      emoji.text = item.emoji
      title.text = item.title
      subtitle.text = item.subtitle
    }
  }
}

Now on my feature screen A I have a RecyclerView that can either have Leaderboard entries or an empty state:

sealed class Entry {
  abstract val id: String

  data class LeaderBoardEntry(val leaderBoard: Leaderboard, override val id: String = leaderBoard.id.toString()) : Entry()
  data class EmptyStateEntry(val emptyState: EmptyState, override val id: String = emptyState.hashCode().toString()) : Entry()
}

Creating the AdapterDelegate for the leader board is straight forward:

private val adapterDelegateLeaderBoard: AdapterDelegate<List<Entry>> = adapterDelegate<LeaderBoardEntry, Entry>(R.layout.yatzy_adapter_item_leaderboard) {
  ...
}

Now, how can I reuse the empty state Adapter Delegate?

private val adapter by lazy(NONE) { AsyncListDifferDelegationAdapter(
  diffUtil { it.id.hashCode().toLong() },
  adapterDelegateLeaderBoard,
  AdapterDelegateFactory.emptyState())
}

This does not work since my factory function returns an AdapterDelegate<List<EmptyState>> and I need a AdapterDelegate<List<Entry>>.

This is what I came up with:

private val adapterDelegateEmptyState: AdapterDelegate<List<Entry>> =
  AdapterDelegateFactory.emptyState().wrapped<EmptyState, EmptyStateEntry, Entry> { it.emptyState }

Then I can do:

private val adapter by lazy(NONE) { AsyncListDifferDelegationAdapter(
  diffUtil { it.id.hashCode().toLong() },
  adapterDelegateLeaderBoard,
  adapterDelegateEmptyState
) }

Now as for the wrapped function:

fun <From, To : ToBase, ToBase> AdapterDelegate<List<From>>.wrapped(mapper: (To) -> From): AdapterDelegate<List<ToBase>> {
  val that = this
  @Suppress("UNCHECKED_CAST")
  return object : AdapterDelegate<List<To>>() {
    override fun onCreateViewHolder(parent: ViewGroup) = that.onCreateViewHolder(parent)

    override fun isForViewType(items: List<To>, position: Int): Boolean {
      return that.isForViewType(items.map { mapper(it) }, position)
    }

    override fun onBindViewHolder(items: List<To>, position: Int, holder: ViewHolder, payloads: MutableList<Any>) {
      that.onBindViewHolder(items.map { mapper(it) }, position, holder, payloads)
    }
  } as AdapterDelegate<List<ToBase>>
}

I feel like this functionality should be provided by the library? Maybe it even is and I'm missing it? Additionally, since the methods are protected my helper function currently lives in package com.hannesdorfmann.adapterdelegates4 😆

Do you have any better ideas?

vanniktech avatar Jun 13 '20 08:06 vanniktech

yea, I feel your pain.

I am considering removing (in AdapterDelegates 5) the need of specifying a base class, so essentially AdatperDelegate operate on datasource List<Any> and then indeed have some @Suppress("UNCHECKED_CAST") internally.

sockeqwe avatar Jun 20 '20 11:06 sockeqwe

That'd be cool too

vanniktech avatar Jun 20 '20 14:06 vanniktech

The above does not actually work if you have different item view types. In my first use case it either had X or Y. Having X & Y would crash. Updated version:

@Suppress("UNCHECKED_CAST", "PROTECTED_CALL_FROM_PUBLIC_INLINE")
inline fun <Old, reified I : T, T> AdapterDelegate<List<Old>>.wrap(noinline mapper: (I) -> Old): AdapterDelegate<List<T>> {
  val listItemAdapterDelegate = this as AbsListItemAdapterDelegate<Old, Old, AdapterDelegateViewHolder<Old>>
  return object : AbsListItemAdapterDelegate<I, T, RecyclerView.ViewHolder>() {
    override fun isForViewType(item: T, items: MutableList<T>, position: Int): Boolean = item is I

    override fun onBindViewHolder(item: I, holder: RecyclerView.ViewHolder, payloads: MutableList<Any>) {
      listItemAdapterDelegate.onBindViewHolder(mapper.invoke(item), holder as AdapterDelegateViewHolder<Old>, payloads)
    }

    override fun onCreateViewHolder(parent: ViewGroup) = listItemAdapterDelegate.onCreateViewHolder(parent)
  }
}

vanniktech avatar Jun 23 '20 09:06 vanniktech