How to insert different item e.g. native ads on each 10 items of recyclerView using diffutil "AsyncListDiffer"

551 Views Asked by At

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

}
1

There are 1 best solutions below

6
On

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:

  1. You will need to have classes for your types like CardItem, CardMagazineItem, TitleItem, AdItem, etc.

  2. 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>> {
  ...
}
  1. Add your differCallback and delegates to the adapter:
val postAdapter = object : AsyncListDifferDelegationAdapter<Any>(
    differCallback,
    cardItemDelegate(),
    ... // your rest delegates for other types
)
  1. And now you can modify the list of your main items for your needs:
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