Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
vanniktech opened this issue Jun 13, 2020 · 3 comments
Open

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

vanniktech opened this issue Jun 13, 2020 · 3 comments

Comments

@vanniktech
Copy link
Contributor

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?

@sockeqwe
Copy link
Owner

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.

@vanniktech
Copy link
Contributor Author

That'd be cool too

@vanniktech
Copy link
Contributor Author

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)
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants