Reputation: 2724
Since I decided to migrate from setting items and notifyDataSetChanged()
to using AsyncListDiffer SubmitList
method I am facing problems with implementing native ads to RecyclerView in my current adapter class
I used this method in the old question and it worked perfectly fine with my old structure of the app, but when I tried the AsyncListDiffer it did not work
**the problem
When I use this logic in getItemViewType
if (shouldLoadNativeAds && position != 0 && position % 10 == 0) VIEW_TYPE_AD_CARD_LAYOUT else VIEW_TYPE_CONTENT
it replaces the content item with AD item e.g. if the item is 20 the recyclerView shows 18 and 2 for ads as for I used the following code (it's duplicating items when scrolling)**
video showing the duplicating problem
the old structure of PostAdapter
class PostAdapter(
private val titleAndGridLayout: TitleAndGridLayout
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items = arrayListOf<Item>()
private var context: Context? = null
val VIEW_TYPE_CONTENT = 1
val VIEW_TYPE_AD_CARD_LAYOUT = 2
val VIEW_TYPE_AD_GRID_LAYOUT = 3
var isDestroyed = false
private var adsCnt = 3
var viewType = 0
set(value) {
field = value
notifyDataSetChanged()
}
fun submitList(items: List<Item>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clearList() {
this.items.clear()
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
val androidVersionCode: Int = Build.VERSION.SDK_INT
val shouldLoadNativeAds: Boolean = (context?.let { Utils.hasInternetConnection(it) } == true
&& androidVersionCode >= Build.VERSION_CODES.N)
return when (viewType) {
CARD, CARD_MAGAZINE -> {
if (shouldLoadNativeAds && position > 0 && position % LIST_AD_DELTA == 0)
VIEW_TYPE_AD_CARD_LAYOUT else VIEW_TYPE_CONTENT
}
TITLE -> {
if (shouldLoadNativeAds && position > 0 && position % LIST_AD_DELTA == 0) VIEW_TYPE_AD_GRID_LAYOUT else VIEW_TYPE_CONTENT
}
GRID -> {
if (shouldLoadNativeAds && (position + 1) % 10 == 0 && (position + 1) != 1) VIEW_TYPE_AD_GRID_LAYOUT else VIEW_TYPE_CONTENT
}
else -> VIEW_TYPE_CONTENT
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
[email protected] = parent.context
when (viewType) {
VIEW_TYPE_CONTENT -> {
when (this.viewType) {
CARD -> {
val cardLayoutBinding: CardLayoutBinding =
CardLayoutBinding.inflate(inflater, parent, false)
return CardViewHolder(cardLayoutBinding)
}
CARD_MAGAZINE -> {
val cardMagazineBinding: CardMagazineBinding =
CardMagazineBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return CardMagazineViewHolder(cardMagazineBinding)
}
TITLE -> {
val titleLayoutBinding: TitleLayoutBinding =
TitleLayoutBinding.inflate(inflater, parent, false)
return TitleViewHolder(titleLayoutBinding)
}
else -> {
val gridLayoutBinding: GridLayoutBinding =
GridLayoutBinding.inflate(inflater, parent, false)
return GridViewHolder(gridLayoutBinding)
}
}
}
VIEW_TYPE_AD_CARD_LAYOUT -> {
val nativeAdRowBinding = AdUnifiedBinding.inflate(inflater, parent, false)
return AdViewHolder(nativeAdRowBinding)
}
else -> {
val nativeAdRowTitleGridBinding =
NativeAdRowTitleGridBinding.inflate(inflater, parent, false)
return AdViewHolderGrid(nativeAdRowTitleGridBinding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item: Item = items[getRealPosition(position)]
val intent = Intent(holder.itemView.context, DetailsActivity::class.java)
if (getItemViewType(position) == VIEW_TYPE_CONTENT) {
when (this.viewType) {
CARD -> if (holder is CardViewHolder) {
holder.bind(item)
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
CARD_MAGAZINE -> if (holder is CardMagazineViewHolder) {
holder.bind(item)
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
TITLE -> if (holder is TitleViewHolder) {
holder.bind(item)
if (position == itemCount - 1)
titleAndGridLayout.tellFragmentToGetItems()
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
GRID -> if (holder is GridViewHolder) {
holder.bind(item)
if (position == itemCount - 1)
titleAndGridLayout.tellFragmentToGetItems()
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
}
} else if (getItemViewType(position) == VIEW_TYPE_AD_CARD_LAYOUT) {
if (holder is AdViewHolder) {
holder.bindAdData()
}
} else {
if (holder is AdViewHolderGrid) {
holder.bindAdData()
if (getItemViewType(position) == VIEW_TYPE_AD_GRID_LAYOUT) {
if (position == itemCount - 1)
titleAndGridLayout.tellFragmentToGetItems()
}
}
}
}
private fun getRealPosition(position: Int): Int {
return if (LIST_AD_DELTA == 0) {
position
} else {
position - position / LIST_AD_DELTA
}
}
override fun getItemCount(): Int {
var additionalContent: Int = 0
if (items.size > 0 && LIST_AD_DELTA > 0 && items.size > LIST_AD_DELTA) {
additionalContent = (items.size + (items.size / LIST_AD_DELTA)) / LIST_AD_DELTA
}
return items.size + additionalContent;
}
override fun getItemId(position: Int): Long {
return getRealPosition(position).toLong()
}
}
companion object {
private const val CARD = 0
private const val CARD_MAGAZINE = 1
private const val TITLE = 2
private const val GRID = 3
private const val TAG = "POST_ADAPTER"
private const val LIST_AD_DELTA = 10
}
init {
setHasStableIds(true)
}
}
The new one with AsyncListDiffer
class PostAdapter(
private val titleAndGridLayout: TitleAndGridLayout
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var context:Context?=null
val VIEW_TYPE_CONTENT = 1
val VIEW_TYPE_AD_CARD_LAYOUT = 2
val VIEW_TYPE_AD_GRID_LAYOUT = 3
var isDestroyed = false
private var adsCnt = 3
var viewType = 0
set(value) {
field = value
notifyDataSetChanged()
}
private val differCallback = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return (oldItem.id == newItem.id)
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return (oldItem == newItem)
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun getItemViewType(position: Int): Int {
val androidVersionCode: Int = Build.VERSION.SDK_INT
val shouldLoadNativeAds: Boolean = (context?.let { Utils.hasInternetConnection(it) } == true
&& androidVersionCode >= Build.VERSION_CODES.N)
return when (viewType) {
CARD, CARD_MAGAZINE -> {
if (shouldLoadNativeAds && position > 0 && position % LIST_AD_DELTA == 0)
VIEW_TYPE_AD_CARD_LAYOUT else VIEW_TYPE_CONTENT
}
TITLE -> {
if (shouldLoadNativeAds && position > 0 && position % LIST_AD_DELTA == 0) VIEW_TYPE_AD_GRID_LAYOUT else VIEW_TYPE_CONTENT
}
else -> VIEW_TYPE_CONTENT
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
[email protected] = parent.context
val nativeAdRowBinding = AdUnifiedBinding.inflate(inflater, parent, false)
val nativeAdRowTitleGridBinding =
NativeAdRowTitleGridBinding.inflate(inflater, parent, false)
when (viewType) {
VIEW_TYPE_CONTENT -> {
when (this.viewType) {
CARD -> {
val cardLayoutBinding: CardLayoutBinding =
CardLayoutBinding.inflate(inflater, parent, false)
return CardViewHolder(cardLayoutBinding)
}
CARD_MAGAZINE -> {
val cardMagazineBinding: CardMagazineBinding =
CardMagazineBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return CardMagazineViewHolder(cardMagazineBinding)
}
TITLE -> {
val titleLayoutBinding: TitleLayoutBinding =
TitleLayoutBinding.inflate(inflater, parent, false)
return TitleViewHolder(titleLayoutBinding)
}
else -> {
val gridLayoutBinding: GridLayoutBinding =
GridLayoutBinding.inflate(inflater, parent, false)
return GridViewHolder(gridLayoutBinding)
}
}
}
VIEW_TYPE_AD_CARD_LAYOUT -> {
return AdViewHolder(nativeAdRowBinding)
}
else -> {
return AdViewHolderGrid(nativeAdRowTitleGridBinding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item: Item = differ.currentList[getRealPosition(position)]
val intent = Intent(holder.itemView.context, DetailsActivity::class.java)
if (getItemViewType(getRealPosition(position)) == VIEW_TYPE_CONTENT) {
when (this.viewType) {
CARD -> if (holder is CardViewHolder) {
holder.bind(item)
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
CARD_MAGAZINE -> if (holder is CardMagazineViewHolder) {
holder.bind(item)
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
TITLE -> if (holder is TitleViewHolder) {
holder.bind(item)
if (position == itemCount - 1)
titleAndGridLayout.tellFragmentToGetItems("titleLayout")
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
GRID -> if (holder is GridViewHolder) {
holder.bind(item)
if (position == itemCount - 1) {
titleAndGridLayout.tellFragmentToGetItems("gridLayout")
}
holder.itemView.setOnClickListener { view: View ->
intent.putExtra("postItem", item)
view.context.startActivity(intent)
}
}
}
} else if (getItemViewType(getRealPosition(position)) == VIEW_TYPE_AD_CARD_LAYOUT) {
if (holder is AdViewHolder) {
holder.bindAdData()
}
} else {
holder as AdViewHolderGrid
holder.bindAdData()
holder.setIsRecyclable(false)
if (position == itemCount - 1) {
titleAndGridLayout.tellFragmentToGetItems("gridLayout")
}
}
}
private fun getRealPosition(position: Int): Int {
return if (LIST_AD_DELTA == 0) {
position
} else {
position - position / LIST_AD_DELTA
}
}
override fun getItemCount(): Int {
var additionalContent: Int = 0
val itemsSize = differ.currentList.size
if (itemsSize > 0 && LIST_AD_DELTA > 0 && itemsSize > LIST_AD_DELTA) {
additionalContent = (itemsSize + (itemsSize / LIST_AD_DELTA)) / LIST_AD_DELTA
}
return itemsSize + additionalContent;
}
override fun getItemId(position: Int): Long {
return getRealPosition(position).toLong()
}
companion object {
private const val CARD = 0
private const val CARD_MAGAZINE = 1
private const val TITLE = 2
private const val GRID = 3
private const val TAG = "POST_ADAPTER"
private const val LIST_AD_DELTA = 10
}
init {
setHasStableIds(true)
}
inner class AdViewHolder(private val binding: AdUnifiedBinding) :
RecyclerView.ViewHolder(binding.root) {
private val videoOptions = VideoOptions.Builder()
.setStartMuted(false)
.build()
private var adOptions = NativeAdOptions.Builder()
.setVideoOptions(videoOptions)
.build()
fun bindAdData() {
val builder =
AdLoader.Builder(binding.root.context, "ca-app-pub-3940256099942544/2247696110")
builder.forNativeAd { nativeAd ->
if(isDestroyed){
Toast.makeText([email protected], "$isDestroyed", Toast.LENGTH_SHORT).show()
Log.e(TAG, "bindAdData: $isDestroyed", )
nativeAd.destroy()
Log.e(TAG, "bindAdData: ${nativeAd.body.toString()}", )
}
populateNativeAdView(nativeAd, binding)
}
builder.withNativeAdOptions(adOptions)
val adLoader = builder
.withAdListener(
object : AdListener() {
override fun onAdFailedToLoad(loadAdError: LoadAdError) {
if (adsCnt > 0) {
bindAdData()
} else {
adsCnt -= 1
}
val error =
"""
domain: ${loadAdError.domain}, code: ${loadAdError.code}, message: ${loadAdError.message}
""""
Toast.makeText(
binding.root.context,
"Failed to load native ad with error $error",
Toast.LENGTH_SHORT
)
.show()
}
}
)
.build()
adLoader.loadAds(AdRequest.Builder().build(), 5)
}
private fun populateNativeAdView(nativeAd: NativeAd, unifiedAdBinding: AdUnifiedBinding) {
val nativeAdView = unifiedAdBinding.root
nativeAdView.mediaView = unifiedAdBinding.adMedia
nativeAdView.headlineView = unifiedAdBinding.adHeadline
nativeAdView.bodyView = unifiedAdBinding.adBody
nativeAdView.callToActionView = unifiedAdBinding.adCallToAction
nativeAdView.iconView = unifiedAdBinding.adAppIcon
nativeAdView.priceView = unifiedAdBinding.adPrice
nativeAdView.starRatingView = unifiedAdBinding.adStars
nativeAdView.storeView = unifiedAdBinding.adStore
nativeAdView.advertiserView = unifiedAdBinding.adAdvertiser
unifiedAdBinding.adHeadline.text = nativeAd.headline
nativeAd.mediaContent?.let { unifiedAdBinding.adMedia.setMediaContent(it) }
if (nativeAd.body == null) {
unifiedAdBinding.adBody.visibility = INVISIBLE
} else {
unifiedAdBinding.adBody.visibility = View.VISIBLE
unifiedAdBinding.adBody.text = nativeAd.body
}
if (nativeAd.callToAction == null) {
unifiedAdBinding.adCallToAction.visibility = INVISIBLE
} else {
unifiedAdBinding.adCallToAction.visibility = View.VISIBLE
unifiedAdBinding.adCallToAction.text = nativeAd.callToAction
}
if (nativeAd.icon == null) {
unifiedAdBinding.adAppIcon.visibility = View.GONE
} else {
unifiedAdBinding.adAppIcon.setImageDrawable(nativeAd.icon?.drawable)
unifiedAdBinding.adAppIcon.visibility = View.VISIBLE
}
if (nativeAd.price == null) {
unifiedAdBinding.adPrice.visibility = INVISIBLE
} else {
unifiedAdBinding.adPrice.visibility = View.VISIBLE
unifiedAdBinding.adPrice.text = nativeAd.price
}
if (nativeAd.store == null) {
unifiedAdBinding.adStore.visibility = INVISIBLE
} else {
unifiedAdBinding.adStore.visibility = View.VISIBLE
unifiedAdBinding.adStore.text = nativeAd.store
}
if (nativeAd.starRating == null) {
unifiedAdBinding.adStars.visibility = INVISIBLE
} else {
unifiedAdBinding.adStars.rating = nativeAd.starRating!!.toFloat()
unifiedAdBinding.adStars.visibility = View.VISIBLE
}
if (nativeAd.advertiser == null) {
unifiedAdBinding.adAdvertiser.visibility = INVISIBLE
} else {
unifiedAdBinding.adAdvertiser.text = nativeAd.advertiser
unifiedAdBinding.adAdvertiser.visibility = View.VISIBLE
}
nativeAdView.setNativeAd(nativeAd)
}
}
inner class AdViewHolderGrid(private val binding: NativeAdRowTitleGridBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindAdData() {
val builder =
AdLoader.Builder(binding.root.context, "ca-app-pub-3940256099942544/2247696110")
builder.forNativeAd { nativeAd: NativeAd ->
if (isDestroyed) {
nativeAd.destroy()
}
val styles =
NativeTemplateStyle.Builder().withMainBackgroundColor(
ColorDrawable(
ContextCompat.getColor(
binding.root.context,
R.color.backgroundColor
)
)
).build()
val template: TemplateView = binding.myTemplate
Log.d(TAG, "bindAdData: ${nativeAd.body}")
template.setStyles(styles)
template.setNativeAd(nativeAd)
}
val adLoader = builder
.withAdListener(
object : AdListener() {
override fun onAdFailedToLoad(loadAdError: LoadAdError) {
if (adsCnt > 0) {
bindAdData()
} else {
adsCnt -= 1
}
val error =
"""
domain: ${loadAdError.domain}, code: ${loadAdError.code}, message: ${loadAdError.message}
""""
Toast.makeText(
binding.root.context,
"Failed to load native ad with error $error",
Toast.LENGTH_SHORT
)
.show()
}
}
)
.build()
adLoader.loadAds(AdRequest.Builder().build(), 5)
}
}
}
Upvotes: 1
Views: 637
Reputation: 1724
I highly recommend starting using AdapterDelegates when you need to display different item types in the RecyclerView, especially if you have conditions on how to display them. The AdapterDelegates is supporting the AsyncListDiffer as well. You will forever forget about the headache of managing the types and keep the logic out of your adapter. As result, it will be flexible, easy to change, and support.
In this case, you can manage your items just like items of the list.
For example:
You will need to have classes for your types like CardItem
, CardMagazineItem
, TitleItem
, AdItem
, etc.
Then you will need to create the adapter delegates for each type:
fun cardItemDelegate(): AdapterDelegate<List<Any>> {
return adapterDelegate<CardItem>(
layout = ...,
on = { item, _, _ -> item is CardItem }
) {
bind {
... // here code of binding like you have in ViewHolders
}
}
}
fun cardMagazineItemDelegate(): AdapterDelegate<List<Any>> {
...
}
val postAdapter = object : AsyncListDifferDelegationAdapter<Any>(
differCallback,
cardItemDelegate(),
... // your rest delegates for other types
)
val items: MutableList<Any> // your main items
val iterator = items.listIterator()
for (i in iterator.withIndex()) {
if (i.index != 0 && i.index % 10 == 0) { // add the AdItem after every 10th item
iterator.add(AdItem())
}
}
postAdapter.items = items
Upvotes: 0