I successfully added the AdMob native ads in my app, every 10 items it loads and show one ad, the problem is when I scroll out from it it's destroyed and the second list (10 items) load the second ad, and so on..., to understand issue see this video
this makes some delay in loading ads each time it depends of the internet speed of the user, so I wanna make the ad unit "space" in reclcerview not recycled when the user scrolled, I tried some methods like onViewRecycled
and holder.setIsRecyclable(false)
on bindViewHolder but neither of it is working
here's my Adapter class
class PostAdapter(
items: List<Item>, private val titleAndGridLayout: TitleAndGridLayout
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val items: List<Item>
private var nativeAd: NativeAd? = null
private val VIEW_TYPE_CONTENT = 1
private val VIEW_TYPE_AD_CARD_LAYOUT = 2
internal val VIEW_TYPE_AD_GRID_LAYOUT = 3
private var adsCnt = 3
var viewType = 0
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
return when (viewType) {
CARD, CARD_MAGAZINE -> {
if (position != 0 && position % 10 == 0) VIEW_TYPE_AD_CARD_LAYOUT else VIEW_TYPE_CONTENT
}
TITLE -> {
if ((position + 1) % 9 == 0 && (position + 1) != 1) VIEW_TYPE_AD_GRID_LAYOUT else VIEW_TYPE_CONTENT
}
GRID -> {
if ((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)
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 -> {
// view = inflater.inflate(R.layout.ad_unified, false)
return AdViewHolder(nativeAdRowBinding)
}
else -> {
// view = inflater.inflate(R.layout.ad_unified, false)
return AdViewHolderGrid(nativeAdRowTitleGridBinding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item: Item = items[position]
// val document = Jsoup.parse(item.content)
// val elements = document.select("img")
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("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(position) == VIEW_TYPE_AD_CARD_LAYOUT) {
if (holder is AdViewHolder) {
holder.bindAdData()
holder.setIsRecyclable(false)
}
} else {
holder as AdViewHolderGrid
holder.bindAdData()
holder.setIsRecyclable(false)
if (position == itemCount - 1) {
titleAndGridLayout.tellFragmentToGetItems("gridLayout")
}
}
}
// override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
// super.onViewRecycled(holder)
// if(holder is AdViewHolder){
// getItemViewType(holder.bindingAdapterPosition)
//
// }
// }
override fun getItemCount(): Int {
return items.size
}
// override fun getItemId(position: Int): Long {
// return position.toLong()
// }
class CardViewHolder(private val cardLayoutBinding: CardLayoutBinding) :
RecyclerView.ViewHolder(cardLayoutBinding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
// Log.e("IMAGE", document.getAllElements().select("img").get(0).attr("src"));
var date: Date? = Date()
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
cardLayoutBinding.postTitle.text = item.title
try {
// Log.e("IMAGE", elements[0].attr("src"))
Glide.with(cardLayoutBinding.root).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(cardLayoutBinding.postImage)
} catch (e: IndexOutOfBoundsException) {
cardLayoutBinding.postImage.setImageResource(R.drawable.no_image)
// Log.e(TAG, e.toString())
}
cardLayoutBinding.postDescription.text = document.text()
try {
date = format.parse(item.published)
} catch (e: ParseException) {
e.printStackTrace()
}
val prettyTime = PrettyTime()
cardLayoutBinding.postDate.text = prettyTime.format(date)
}
}
class CardMagazineViewHolder(private val cardMagazineBinding: CardMagazineBinding) :
RecyclerView.ViewHolder(cardMagazineBinding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
var date: Date? = Date()
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
// Log.e("IMAGE", document.getAllElements().select("img").get(0).attr("src"));
cardMagazineBinding.postTitle.text = item.title
try {
// Log.e("IMAGE", elements[0].attr("src"))
Glide.with(itemView.context).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(cardMagazineBinding.postImage)
} catch (e: IndexOutOfBoundsException) {
cardMagazineBinding.postImage.setImageResource(R.drawable.no_image)
// Log.e(TAG, e.toString())
}
try {
date = format.parse(item.published)
} catch (e: ParseException) {
e.printStackTrace()
}
val prettyTime = PrettyTime()
cardMagazineBinding.postDate.text = prettyTime.format(date)
}
}
inner class TitleViewHolder(private val binding: TitleLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
// Log.e("IMAGE", document.getAllElements().select("img").get(0).attr("src"));
binding.postTitle.text = item.title
try {
// Log.e("IMAGE", elements[0].attr("src"))
Glide.with(itemView.context).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(binding.postImage)
} catch (e: IndexOutOfBoundsException) {
binding.postImage.setImageResource(R.drawable.no_image)
// Log.e(TAG, e.toString())
}
}
}
inner class GridViewHolder constructor(private val binding: GridLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
val document = Jsoup.parse(item.content)
val elements = document.select("img")
// Log.e("IMAGE", document.getAllElements().select("img").get(0).attr("src"));
binding.postTitle.text = item.title
try {
// Log.e("IMAGE", elements[0].attr("src"))
Glide.with(itemView.context).load(elements[0].attr("src"))
.transition(DrawableTransitionOptions.withCrossFade(600))
.placeholder(R.drawable.loading_animation)
.error(R.drawable.no_image)
.into(binding.postImage)
} catch (e: IndexOutOfBoundsException) {
binding.postImage.setImageResource(R.drawable.no_image)
// Log.e(TAG, e.toString())
}
}
}
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"
}
init {
this.items = items
// this.fragment = fragment
// this.postViewModel = postViewModel
}
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 ->
// OnUnifiedNativeAdLoadedListener implementation.
// If this callback occurs after the activity is destroyed, you must call
// destroy and return or you may get a memory leak.
this@PostAdapter.nativeAd = nativeAd
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.loadAd(AdRequest.Builder().build())
}
private fun populateNativeAdView(nativeAd: NativeAd, unifiedAdBinding: AdUnifiedBinding) {
val nativeAdView = unifiedAdBinding.root
// Set the media view.
nativeAdView.mediaView = unifiedAdBinding.adMedia
// Set other ad assets.
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
// The headline and media content are guaranteed to be in every UnifiedNativeAd.
unifiedAdBinding.adHeadline.text = nativeAd.headline
nativeAd.mediaContent?.let { unifiedAdBinding.adMedia.setMediaContent(it) }
// These assets aren't guaranteed to be in every UnifiedNativeAd, so it's important to
// check before trying to display them.
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
}
// This method tells the Google Mobile Ads SDK that you have finished populating your
// native ad view with this native ad.
nativeAdView.setNativeAd(nativeAd)
}
}
inner class AdViewHolderGrid(private val nativeAdRowTitleGridBinding: NativeAdRowTitleGridBinding) :
RecyclerView.ViewHolder(nativeAdRowTitleGridBinding.root) {
fun bindAdData() {
val adLoader =
AdLoader.Builder(
nativeAdRowTitleGridBinding.root.context,
"ca-app-pub-3940256099942544/2247696110"
)
.forNativeAd { nativeAd: NativeAd ->
this@PostAdapter.nativeAd = nativeAd
// populateNativeADView(nativeAd)
val styles =
NativeTemplateStyle.Builder().withMainBackgroundColor(
ColorDrawable(
ContextCompat.getColor(
nativeAdRowTitleGridBinding.root.context,
R.color.backgroundColor
)
)
).build()
val template: TemplateView = nativeAdRowTitleGridBinding.myTemplate
Log.d(TAG, "bindAdData: ${nativeAd.body}")
template.setStyles(styles)
template.setNativeAd(nativeAd)
}
.withAdListener(object : AdListener() {
override fun onAdClicked() {
super.onAdClicked()
Log.d(TAG, "onAdClicked: ")
}
override fun onAdClosed() {
super.onAdClosed()
Log.d(TAG, "onAdClosed: ")
}
override fun onAdLoaded() {
super.onAdLoaded()
Log.d(TAG, "onAdLoaded: ")
}
override fun onAdOpened() {
super.onAdOpened()
Log.d(TAG, "onAdOpened: ")
}
override fun onAdFailedToLoad(adError: LoadAdError) {
// Handle the failure by logging, altering the UI, and so on.
Toast.makeText(
nativeAdRowTitleGridBinding.root.context,
adError.message,
Toast.LENGTH_SHORT
).show()
Log.e(TAG, "onAdFailedToLoad: ${adError.cause.toString()}")
}
})
.withNativeAdOptions(
NativeAdOptions.Builder()
// Methods in the NativeAdOptions.Builder class can be
// used here to specify individual options settings.
.build()
).build()
adLoader.loadAd(AdRequest.Builder().build())
}
}
fun destroyNativeAd() {
nativeAd?.destroy()
Log.d(TAG, "destroyNativeAd: ${nativeAd?.body}")
}
}