Skip to content
:docs

Native

Learn how to add and configure native placements and implement additional features through the SDK interface.

Key features

The Buzzvil SDK's native inventory displays ads customized for your app that blend naturally into its screens. After a user engages with an ad, the next ad appears automatically, continuously serving ads in a single placement to accelerate app monetization. It also provides dynamic features that encourage users to engage with more ads.

Integration

Follow these steps to integrate native placements into your app.

Step 1. Prepare

  • Complete Getting started.
  • Obtain a Unit ID for the native placement (referred to below as YOUR_NATIVE_UNIT_ID).

Step 2. Configure the ad layout

See the illustration and table below for the components and UI of a native ad layout.

ComponentDescription
Ad title (required)The ad's title. We recommend no more than 10 characters. You can replace part of the text with an ellipsis as needed, regardless of character count.
Ad creative (required)
  • The image, video or other creative. Preserve its aspect ratio.
  • Display it using com.buzzvil.buzzad.benefit.presentation.media.MediaView.
  • Size: 1,200 × 627 px.
  • You may add padding. Contact your Buzzvil representative for details.
Ad description (required)A detailed description of the ad. We recommend no more than 40 characters. You can replace part of the text with an ellipsis as needed, regardless of character count.
Advertiser icon (required)
  • The advertiser's icon image. You must preserve its aspect ratio.
  • Size: 80 x 80 px
CTA button (required)
  • Encourages ad participation. A maximum of 7 characters is recommended; truncate with an ellipsis if needed.
  • Display it using com.buzzvil.buzzad.benefit.presentation.media.DefaultCtaView.
  • See Primary color for colors and Reward icon for icons. For other changes, see Implement a custom CTA button.
BenefitHub entry point (NativeToFeedLayout) (required)A UI (NativeToFeedLayout) that opens BenefitHub when tapped. Add it to the NativeAd2View layout.
Ad disclosure (recommended)Text identifying the content as an ad (for example, 광고, ad, 스폰서, or Sponsored).

Build the native ad layout in an activity or fragment according to the structure below. Implement a layout (your_native_ad.xml) that meets the specifications of NativeAd2View.

The following is an example layout for NativeAd2View.

XML
<com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2View
    android:id="@+id/nativeAd2View">
    
      
    <com.buzzvil.buzzad.benefit.presentation.media.MediaView
        android:id="@+id/mediaView"
        ...생략... />
    <TextView
        android:id="@+id/textTitle"
        ...생략... />
    <TextView
        android:id="@+id/textDescription"
        ...생략... />
    <ImageView
        android:id="@+id/imageIcon"
        ...생략... />
    <com.buzzvil.buzzad.benefit.presentation.media.DefaultCtaView
        android:id="@+id/ctaView"
        ...생략... />
    <com.buzzvil.buzzad.benefit.presentation.feed.navigation.NativeToFeedLayout
        android:id="@+id/your_native_to_feed_layout">
        
        ...생략...
    </com.buzzvil.buzzad.benefit.presentation.feed.navigation.NativeToFeedLayout>
</com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2View>

Set the Native Unit ID on NativeToFeedLayout in your code.

Kotlin
val nativeToFeedLayout = findViewById(R.id.your_native_to_feed_layout)
nativeToFeedLayout.setNativeUnitId(YOUR_NATIVE_UNIT_ID)

Display the base reward amount

Display the base reward (BaseReward) earned simply by entering BenefitHub in NativeToFeedLayout to encourage clicks.

The following example displays base reward points in NativeToFeedLayout.

Kotlin
import com.buzzvil.buzzad.benefit.BaseRewardManager
import com.buzzvil.buzzad.benefit.BuzzAdBenefit

BuzzAdBenefit.getBaseRewardManager()?.getAvailableFeedBaseReward(YOUR_FEED_ID, object : BaseRewardManager.BaseRewardListener {
    override fun onBaseRewardLoaded(reward: Int) {
        if (reward < 1) {
            nativeToFeedText.text = "더 많은 참여 기회 보기"
        } else {
            nativeToFeedText.text = "$reward 포인트 받고 더 많은 참여 기회 보기"
        }
    }
})

Step 3. Display ads

Follow these steps and the sample code to display ads in the ad layout.

  1. Use NativeAd2ViewBinder.Builder to set all required views, including nativeAd2View and MediaView, then call build() to create a NativeAd2ViewBinder object.
  2. Optionally register NativeAd2StateChangedListener to implement UI for each ad request state.
  3. Call bind() on the new NativeAd2ViewBinder object to allocate and display an ad automatically.
    • If allocation fails, no ad is displayed. Use NativeAd2StateChangedListener to define failure behavior.
    • When the user completes participation (the CTA view shows the completion label 참여 완료 (Participation complete)) and returns to the native placement, it automatically refreshes to the next ad.
Kotlin
import com.buzzvil.buzzad.benefit.core.ad.AdError
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2StateChangedListener
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2ViewBinder

val nativeAd2View = findViewById(R.id.nativeAd2View)
val mediaView = findViewById(R.id.mediaView)
val titleTextView = findViewById(R.id.textTitle)
val descriptionTextView = findViewById(R.id.textDescription)
val iconImageView = findViewById(R.id.imageIcon)
val ctaView = findViewById(R.id.ctaView)

// 광고 레이아웃을 설정합니다.
val binder: NativeAd2ViewBinder = NativeAd2ViewBinder.Builder()
    .nativeAd2View(nativeAd2View)
    .mediaView(mediaView)
    .titleTextView(titleTextView)
    .descriptionTextView(descriptionTextView)
    .iconImageView(iconImageView)
    .ctaView(ctaView)
    .build(Constant.YOUR_NATIVE_ID)

// (Optional) 광고 요청 상태에 따른 UI를 구현합니다.
binder.addNativeAd2StateChangedListener(object : NativeAd2StateChangedListener {
    override fun onRequested() {
        // 광고 할당을 요청한 상태입니다.
        // 이후에는 onNext(), onComplete(), onError() 중 하나가 호출됩니다.
        // 광고 자동 갱신을 시도할 때마다 반복적으로 호출됩니다.
        // 로딩 화면 등을 구현할 수 있습니다.
        Toast.makeText(this@MainActivity, "onRequested", Toast.LENGTH_SHORT).show()
    }

    override fun onNext(nativeAd2: NativeAd2) {
        // 광고 할당에 성공하면 호출됩니다.
        // 이후에 광고 갱신 시 onRequested()가 다시 호출됩니다.
        // 광고 자동 갱신을 성공할 때마다 반복적으로 호출됩니다.
        // 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
        Toast.makeText(this@MainActivity, "onNext", Toast.LENGTH_SHORT).show()
    }

    override fun onComplete() {
        // 더 이상 갱신할 수 있는 광고가 없을 때 호출됩니다.
        // 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
        Toast.makeText(this@MainActivity, "onComplete", Toast.LENGTH_SHORT).show()
    }

    override fun onError(adError: AdError) {
        // 최초 광고 할당에 실패하면 호출됩니다.
        // 로딩 화면 등을 구현한 경우, 여기에서 로딩을 종료합니다.
        Toast.makeText(this@MainActivity, "onError: ${adError.adErrorType.name}", Toast.LENGTH_SHORT).show()
    }
})

// 광고 할당 및 표시를 자동으로 수행합니다.
binder.bind()

Step 4. Test the basic integration

After completing the basic integration, verify that ads are allocated and displayed correctly. Also check that NativeToFeedLayout (the layout in the native placement that opens BenefitHub) works and opens BenefitHub when clicked.

Ad operation policy (mandatory requirements)

1. Do not refresh ads

  • Make the initial ad request only once when the app screen opens.
  • Subsequent ad requests (reallocations) are allowed only after a clear user action, such as a screen transition.
  • Automatically refreshing ads at fixed intervals or continuously requesting new ads is prohibited.

Excessive ad requests can harm the user experience, ad performance, and revenue.

2. Do not selectively use only impression-based ads

  • Buzzvil combines different ad types to provide an optimized ad experience.
  • API integrations must not selectively use only a specific ad type, such as impression-based ads.
  • SDK integrations must not request ads from another platform when impression-based ads are unavailable.
  • Operate all ad types in balance according to Buzzvil's ad logic.

Restricting or selectively using ad types negatively affects Buzzvil's ad operation strategy and revenue optimization.

Additional features

This section explains how to implement additional native placement features.

Use a native overlay

Native overlays require no separate integration or implementation. When a user returns to your app after participating in a native ad, an overlay appears over the completed ad to encourage entry into BenefitHub. The overlay contains text and an icon, covering the entire native area (NativeAdView) to encourage a click. It disappears automatically when the user clicks Close, or clicks the overlay and then returns.

Overlay UIs adapt to the native placement's size. To increase BenefitHub entry rates, the overlay shows different ad or content product UIs based on the user's past participation and preferences. Each UI type opens the following landing page.

  • Daily Reward: BenefitHub's Daily Reward bottom sheet
    • This does not appear unless Daily Reward is enabled in BenefitHub.
    • See here for details about Daily Reward.
  • Quiz rewards: BenefitHub's content rewards filter
  • Fortune-telling rewards: BenefitHub's content rewards filter
  • Fortune Cookie: BenefitHub's content rewards filter
  • Base reward: BenefitHub
  • This does not appear unless base rewards are enabled in BenefitHub.
  • Total available rewards: BenefitHub
  • Default UI: BenefitHub
    • Appears when a server communication error occurs.

Register an ad event listener

Receive ad callbacks through NativeAd2EventListener. Register the listener before calling NativeAd2ViewBinder.bind().

The following example registers an ad event listener on NativeAd2ViewBinder.

Kotlin
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2EventListener
import com.buzzvil.buzzad.benefit.presentation.reward.RewardResult

// (Optional) 광고 이벤트 리스너를 등록합니다.
// 로그 기록, 단순 알림 외에 다른 동작을 추가하는 것을 권장하지 않습니다. 직접 구현한 동작이 네이티브에서 제공하는 기능(광고 자동 갱신 등)과 충돌할 수 있습니다.
binder.addNativeAd2EventListener(object : NativeAd2EventListener {
    override fun onImpressed(nativeAd2: NativeAd2) {
        // Native 광고가 유저에게 노출되었을 때 호출됩니다.
    }

    override fun onClicked(nativeAd2: NativeAd2) {
        // 유저가 Native 광고를 클릭했을 때 호출됩니다.
    }

    override fun onRewardRequested(nativeAd2: NativeAd2) {
        // 리워드 적립을 요청했을 때 호출됩니다.
    }

    override fun onParticipated(nativeAd2: NativeAd2) {
        // 유저가 광고 참여를 완료하였을 때 호출됩니다.
    }

    override fun onRewarded(nativeAd2: NativeAd2, rewardResult: RewardResult) {
        // 리워드가 적립되었을 때 호출됩니다.
    }
})

binder.bind()

Reward result types (RewardResult)

RewardResultResultDescription
SUCCESSSuccessThe reward was credited successfully.
ALREADY_PARTICIPATEDFailureThe reward for this ad has already been credited.
MISSING_REWARDFailureThe ad has no reward.
TOO_SHORT_TO_PARTICIPATEFailureThe user returned from the ad landing page too quickly.
NETWORK_TIMEOUTFailureA network timeout occurred.
NETWORK_TIMEOUT_IN_POWER_SAVE_MODEFailureA network timeout occurred because of the device's power-saving mode (절전모드).
BROWSER_NOT_LAUNCHEDFailureThe browser did not launch to display the ad landing page.
REQUEST_ERRORFailureThe request is invalid.
UNKNOWN_NETWORK_ERRORFailureAn unknown network error occurred.
UNKNOWN_SERVER_ERRORFailureAn unknown server error occurred.
UNKNOWN_CLIENT_ERRORFailureAn unknown client error occurred.
UNKNOWN_ERRORFailureAn unknown error occurred.
  • When a user enables power-saving mode (절전모드) on their device, background execution restrictions make rewards for click-based ads (CPC, CPM) likely to fail. Use RewardResult.NETWORK_TIMEOUT_IN_POWER_SAVE_MODE to explain the failure to users who have enabled power-saving mode (절전모드).

Add clickable areas

In addition to the default clickable MediaView and CtaView, add more clickable views using addClickableView() of NativeAd2ViewBinder.Builder, as shown below.

Kotlin
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2ViewBinder

val viewBinder = NativeAd2ViewBinder.Builder()
    .nativeAd2View(customNativeAd2View)
    .mediaView(mediaView)
    .titleTextView(textTitle)
    .descriptionTextView(textDescription)
    .iconImageView(imageIcon)
    .ctaView(ctaView)
    // 클릭이 되도록 하고 싶은 뷰를 추가합니다.
    .addClickableView(textTitle)
    // addClickableView를 여러 번 호출하여 여러 개의 클릭 가능 영역을 추가할 수 있습니다.
    .addClickableView(discountPercentageText)
    .addClickableView(priceText)
    .addClickableView(originalPriceText)
    .build()

Implement a carousel

A carousel is a UI that displays multiple ads through horizontal scrolling. Use the Buzzvil SDK to create a carousel in a native placement and implement its required features. Optional features can further improve the user experience.

Implement basic carousel features

Step 1. Allocate ads

To allocate multiple ads in a native placement, use NativeAd2Pool, which controls duplicate ad allocation.

Call NativeAd2Pool.init() to check how many ads can currently be allocated.

Kotlin
import com.buzzvil.buzzad.benefit.core.ad.AdError
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2PoolInitListener

private fun initCarouselPool() {
    // 광고 중복 할당을 막기 위해 하나의 캐러셀에 하나의 NativeAd2Pool 인스턴스를 생성하여 사용합니다.
    val carouselPool = NativeAd2Pool(Constant.YOUR_NATIVE_ID)
    val REQUEST_AD_COUNT = 5

    // 현재 할당 받을 수 있는 광고의 갯수를 확인합니다.
    carouselPool.init(REQUEST_AD_COUNT, object : NativeAd2PoolInitListener {
        override fun onLoaded(adCount: Int) {
            // TODO 할당 받은 광고 갯수(adCount)만큼 아이템 리스트를 만들어 RecyclerView 어댑터를 초기화 합니다.
        }

        override fun onError(adError: AdError) {
            // TODO 광고 레이아웃을 숨김 처리 하는 등 적절한 에러 처리를 할 수 있습니다.
        }
    })
}
Step 2. Create a RecyclerView

Use RecyclerView to lay out item views horizontally and an adapter to manage them, creating a carousel in a native placement with the Buzzvil SDK. Follow these steps to create RecyclerView.

  1. Add RecyclerView where you want the carousel.

    XML
        
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/carouselRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            tools:listitem="@layout/your_native_ad_view" />
    
  2. Define a layout for each item of RecyclerView in the view layout file (your_native_ad_view.xml).

Code
```xml
<?xml version="1.0" encoding="utf-8"?>

<com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2View xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nativeAd2View"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    ...
</com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2View>
```

3. Declare a NativeAd2CarouselItem class for each item in RecyclerView.

Code
> ✏️ &nbsp;**Note**<br />`RecyclerView` can also contain BenefitHub entry points. For extensibility, declare `NativeAd2CarouselItem` as a base class and derive each item type from it. See [BenefitHub entry slide](#베네핏허브-진입-슬라이드) below.

The following example declares the base class `NativeAd2CarouselItem` and extends it with `NativeAd2Item`.
 
```kotlin
sealed class NativeAd2CarouselItem {
    // 리사이클러 뷰에서 해당 아이템이 네이티브 2.0인지 타입을 구분하기 위한 클래스입니다.
    data object NativeAd2Item : NativeAd2CarouselItem()

    // TODO 베네핏허브 진입 슬라이드를 위한 클래스 추가
}
```
Step 3. Implement the RecyclerView adapter

Follow these steps to implement the RecyclerView adapter and ViewHolder.

  1. Implement NativeAd2CarouselViewHolder to bind NativeAd2View.

    Kotlin
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2ViewBinder
    import com.buzzvil.sample.buzzvilsdksample.databinding.CarouselItemBinding
    
    class NativeAd2CarouselViewHolder(
        binding: CarouselItemBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        private val nativeAd2ViewBinder: NativeAd2ViewBinder
    
        init {
            val nativeAd2View = binding.nativeAd2View
            val mediaView = binding.adMediaView
            val titleTextView = binding.adTitleText
            val descriptionTextView = binding.adDescriptionText
            val iconImageView = binding.adIconImage
            val ctaView = binding.adCtaView
    
            // NativeAd2View와 하위 컴포넌트를 바인드합니다.
            nativeAd2ViewBinder = NativeAd2ViewBinder.Builder()
                .nativeAd2View(nativeAd2View)
                .mediaView(mediaView)
                .titleTextView(titleTextView)
                .descriptionTextView(descriptionTextView)
                .iconImageView(iconImageView)
                .ctaView(ctaView)
                .build(Constant.YOUR_NATIVE_ID)
        }
    
        fun setPool(carouselPool: NativeAd2Pool, adKey: Int) {
            // 해당 position(adKey)에 해당하는 NativeAd2ViewBinder가 carouselPool을 사용하도록 합니다.
            nativeAd2ViewBinder.setPool(adKey, carouselPool)
        }
    
        fun bind(position: Int) {
            // NativeAd2ViewBinder의 bind()를 호출하면 광고 할당 및 갱신이 자동으로 수행됩니다.
            nativeAd2ViewBinder.bind(position)
        }
    
        fun unbind(position: Int) {
            // unbind를 반드시 호출하여 뷰를 재사용할 때 문제가 발생하지 않게 합니다.
            nativeAd2ViewBinder.unbind(position)
        }
    }
    
  2. Implement an adapter (NativeAd2CarouselAdapter) to manage the carousel.

Code
```kotlin
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
import com.buzzvil.sample.buzzvilsdksample.databinding.CarouselItemBinding
import com.buzzvil.sample.buzzvilsdksample.nativead.carousel.NativeAd2CarouselItem
import com.buzzvil.sample.buzzvilsdksample.nativead.carousel.NativeAd2CarouselViewHolder

class NativeAd2CarouselAdapter(
    private val list: List<NativeAd2CarouselItem>,
    private val carouselPool: NativeAd2Pool
) : RecyclerView.Adapter<NativeAd2CarouselViewHolder?>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NativeAd2CarouselViewHolder {
        val inflater = parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val binding = CarouselItemBinding.inflate(inflater, parent, false)
        return NativeAd2CarouselViewHolder(binding)
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onBindViewHolder(holder: NativeAd2CarouselViewHolder, position: Int) {
        // 해당 position에 해당하는 NativeAd2ViewBinder가 carouselPool을 사용하도록 합니다.
        holder.setPool(carouselPool, position)
        holder.bind(position)
    }

    override fun onViewRecycled(holder: NativeAd2CarouselViewHolder) {
        super.onViewRecycled(holder)

        // unbind를 반드시 호출하여 뷰를 재사용할 때 문제가 발생하지 않게 합니다.
        holder.unbind(holder.adapterPosition)
    }
}
```
Step 4. Bind to an activity or Fragment

The following example creates the NativeAd2CarouselAdapter implemented above in an activity (NativeAd2CarouselActivity).

  1. Implement buildCarouselItems() to create an item list whose size equals the number of allocated ads (adCount).
  2. In NativeAd2PoolInitListener.onLoaded(), use buildCarouselItems() to create NativeAd2CarouselAdapter and set it on the RecyclerView.

The following is an example implementation of NativeAd2CarouselActivity.

Kotlin
import com.buzzvil.buzzad.benefit.core.ad.AdError
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2PoolInitListener

private fun initCarouselPool() {
    // 광고 중복 할당을 막기 위해 하나의 캐러셀에 하나의 NativeAd2Pool 인스턴스를 생성하여 사용합니다.
    val carouselPool = NativeAd2Pool(Constant.YOUR_NATIVE_ID)
    val REQUEST_AD_COUNT = 5

    // 현재 할당 받을 수 있는 광고의 갯수를 확인합니다.
    carouselPool.init(REQUEST_AD_COUNT, object : NativeAd2PoolInitListener {
        override fun onLoaded(adCount: Int) {
            // 어댑터를 초기화합니다.
            initCarouselAdapter(adCount, carouselPool)
        }

        override fun onError(adError: AdError) {
            // TODO 광고 레이아웃을 숨김 처리 하는 등 적절한 에러 처리를 할 수 있습니다.
        }
    })
}

private fun initCarouselAdapter(adCount: Int, carouselPool: NativeAd2Pool) {
    // 할당 받은 광고 갯수(adCount) 크기의 아이템 리스트를 만들어 NativeAd2CarouselAdapter를 생성합니다.
    val list = buildCarouselItems(adCount)
    val adapter = NativeAd2CarouselAdapter(list, carouselPool)

    // RecyclerView에 어댑터를 설정합니다.
    binding.carouselRecyclerView.adapter = adapter
}

private fun buildCarouselItems(adCount: Int): List<NativeAd2CarouselItem> {
    // adCount 크기의 배열을 생성합니다.
    val carouselItems = arrayOfNulls<NativeAd2CarouselItem>(adCount)

    // 배열을 NativeAd2Item으로 채웁니다.
    carouselItems.fill(NativeAd2Item)

    return carouselItems.toList().filterNotNull()
}

Run the app to see the carousel in action. Then complete the integration by following Implement required carousel features below.

Implement required carousel features

The Buzzvil SDK provides a slide page and entry point into BenefitHub, where users can find a variety of ads and filter them to suit their preferences.

BenefitHub entry slide

Add a Carousel To Feed Slide anywhere in the carousel, with no limit on the number of slides. These provide entry points between ads to BenefitHub, a placement optimized for monetization. The SDK provides the following classes.

CodeDescription
FeedPromotionHolds the image and text resources displayed on a BenefitHub entry slide.
FeedPromotionFactoryCreates FeedPromotion objects.
  • buildForCarousel(): Allocates carousel resources and creates a FeedPromotion object.
FeedPromotionViewBinderConnects FeedPromotion to NativeAd2View for display.
  • bind(): Binds FeedPromotion and NativeAd2View.
  • unbind(): Unbinds FeedPromotion and NativeAd2View.
  • Builder: Builder for creating FeedPromotionViewBinder.

Follow these steps to implement a BenefitHub entry slide.

  1. Add a class declaration in NativeAd2CarouselItem for the BenefitHub entry slide.

    The following example adds CarouselToFeedSlideItem to the existing NativeAd2CarouselItem implementation.

    Kotlin
    import com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedPromotion
    
    sealed class NativeAd2CarouselItem {
        // RecyclerView에서 해당 아이템이 네이티브 2.0인지 타입을 구분하기 위한 클래스입니다.
        data object NativeAd2Item : NativeAd2CarouselItem()
    
        // RecyclerView에서 해당 아이템이 베네핏허브 진입 슬라이드인지 타입을 구분하기 위한 클래스입니다.
        data class CarouselToFeedSlideItem(val feedPromotion: FeedPromotion) : NativeAd2CarouselItem()
    }
    
  2. Implement the adapter (NativeAd2CarouselAdapter) and ViewHolder (NativeAd2CarouselViewHolder) to handle CarouselToFeedSlideItem.

    i. In NativeAd2CarouselViewHolder, select FeedPromotionViewBinder according to the item type.

    Kotlin
    import com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedPromotion
    import com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedPromotionViewBinder
    
    class NativeAd2CarouselViewHolder(
        binding: CarouselItemBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        ...
        private val carouselToFeedViewBinder: FeedPromotionViewBinder
    
        init {
            ...
    
            // CarouselToFeedSlideItem을 위한 바인더를 빌드합니다.
            carouselToFeedViewBinder = FeedPromotionViewBinder.Builder(nativeAd2View, mediaView)
                .titleTextView(titleTextView)
                .descriptionTextView(descriptionTextView)
                .iconImageView(iconImageView)
                .ctaView(ctaView)
                .build()
        }
    
        ...
    
        fun bind(position: Int, feedPromotion: FeedPromotion) {
            // FeedPromotion 객체를 뷰에 바인드합니다.
            carouselToFeedViewBinder.bind(feedPromotion)
        }
    
        fun unbind(position: Int) {
            // unbind를 반드시 호출하여 뷰를 재사용할 때 문제가 발생하지 않게 합니다.
            ...
            carouselToFeedViewBinder.unbind()
        }
    }
    

    The following example shows the complete NativeAd2CarouselViewHolder implementation.

    Kotlin
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2ViewBinder
    import com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedPromotion
    import com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedPromotionViewBinder
    
    class NativeAd2CarouselViewHolder(
        binding: CarouselItemBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        private val nativeAd2ViewBinder: NativeAd2ViewBinder
        private val carouselToFeedViewBinder: FeedPromotionViewBinder
    
        init {
            val nativeAd2View = binding.nativeAd2View
            val mediaView = binding.adMediaView
            val titleTextView = binding.adTitleText
            val descriptionTextView = binding.adDescriptionText
            val iconImageView = binding.adIconImage
            val ctaView = binding.adCtaView
    
            // NativeAd2View와 하위 컴포넌트를 바인드합니다.
            nativeAd2ViewBinder = NativeAd2ViewBinder.Builder()
                .nativeAd2View(nativeAd2View)
                .mediaView(mediaView)
                .titleTextView(titleTextView)
                .descriptionTextView(descriptionTextView)
                .iconImageView(iconImageView)
                .ctaView(ctaView)
                .build(Constant.YOUR_NATIVE_ID)
    
            // CarouselToFeedSlideItem을 위한 바인더를 빌드합니다.
            carouselToFeedViewBinder = FeedPromotionViewBinder.Builder(nativeAd2View, mediaView)
                .titleTextView(titleTextView)
                .descriptionTextView(descriptionTextView)
                .iconImageView(iconImageView)
                .ctaView(ctaView)
                .build()
        }
    
        fun setPool(carouselPool: NativeAd2Pool, adKey: Int) {
            // 해당 position(adKey)에 해당하는 NativeAd2ViewBinder가 carouselPool을 사용하도록 합니다.
            nativeAd2ViewBinder.setPool(adKey, carouselPool)
            carouselToFeedViewBinder.setPool(carouselPool)
        }
    
        fun bind(position: Int) {
            // NativeAd2ViewBinder의 bind()를 호출하면 광고 할당 및 갱신이 자동으로 수행됩니다.
            nativeAd2ViewBinder.bind(position)
        }
    
        fun bind(position: Int, feedPromotion: FeedPromotion) {
            // FeedPromotion 객체를 뷰에 바인드합니다.
            carouselToFeedViewBinder.bind(feedPromotion)
        }
    
        fun unbind(position: Int) {
            // unbind를 반드시 호출하여 뷰를 재사용할 때 문제가 발생하지 않게 합니다.
            nativeAd2ViewBinder.unbind(position)
            carouselToFeedViewBinder.unbind()
        }
    }
    

    ii. In onBindeViewHolder of NativeAd2CarouselAdapter, connect the bind function to call for CarouselToFeedSlideItem.

    Kotlin
    class NativeAd2CarouselAdapter(
        ...
    ) : RecyclerView.Adapter<NativeAd2CarouselViewHolder?>() {
        ...
    
        override fun onBindViewHolder(holder: NativeAd2CarouselViewHolder, position: Int) {
            ...
            // 아이템의 타입을 구분하여 적절한 bind() 함수를 호출합니다.
            val item = list[position]
    
            if (item is NativeAd2Item) {
                holder.bind(position)
            } else if (item is CarouselToFeedSlideItem) {
                holder.bind(position, item.feedPromotion)
            }
        }
        
        ...
    }
    

    The following example shows the complete NativeAd2CarouselAdapter implementation.

    Kotlin
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
    
    class NativeAd2CarouselAdapter(
        private val list: List<NativeAd2CarouselItem>,
        private val carouselPool: NativeAd2Pool
    ) : RecyclerView.Adapter<NativeAd2CarouselViewHolder?>() {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NativeAd2CarouselViewHolder {
            val inflater = parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            val binding = CarouselItemBinding.inflate(inflater, parent, false)
            return NativeAd2CarouselViewHolder(binding)
        }
    
        override fun getItemCount(): Int {
            return list.size
        }
    
        override fun onBindViewHolder(holder: NativeAd2CarouselViewHolder, position: Int) {
            // 해당 position에 해당하는 NativeAd2ViewBinder가 carouselPool을 사용하도록 합니다.
            holder.setPool(carouselPool, position)
    
            // 아이템의 타입을 구분하여 적절한 bind() 함수를 호출합니다.
            val item = list[position]
    
            if (item is NativeAd2Item) {
                holder.bind(position)
            } else if (item is CarouselToFeedSlideItem) {
                holder.bind(position, item.feedPromotion)
            }
        }
    
        override fun onViewRecycled(holder: NativeAd2CarouselViewHolder) {
            super.onViewRecycled(holder)
    
            // unbind를 반드시 호출하여 뷰를 재사용할 때 문제가 발생하지 않게 합니다.
            holder.unbind(holder.adapterPosition)
        }
    }
    
  3. In buildCarouselItems(), which builds the item list for the adapter, insert FeedPromotion at the desired position.

    The following example places the BenefitHub entry slide at the end of the carousel.

    Kotlin
    import com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedPromotionFactory
    
    private fun buildCarouselItems(adCount: Int): List<NativeAd2CarouselItem> {
        // 마지막 페이지에 CarouselToFeedSlideItem을 추가하기 위해 1개 더 큰 사이즈로 배열을 생성합니다.
        val carouselItems = arrayOfNulls<NativeAd2CarouselItem>(adCount + 1)
    
        // 배열을 NativeAd2Item으로 채웁니다.
        carouselItems.fill(NativeAd2Item)
    
        // 마지막 아이템은 CarouselToFeedSlideItem으로 설정합니다.
        val carouselToFeedSlideItem = CarouselToFeedSlideItem(FeedPromotionFactory(Constant.YOUR_NATIVE_ID).buildForCarousel())
        carouselItems[carouselItems.size - 1] = carouselToFeedSlideItem
    
        return carouselItems.toList().filterNotNull()
    }
    
BenefitHub entry point

Add a Carousel To Feed Link below the carousel so users can open BenefitHub.

Follow these steps to implement the BenefitHub entry point.

  1. Add FeedEntryView below the RecyclerView used for the carousel.

    Find the complete activity_native_ad2_carousel.xml code here.

    XML
    
    
    
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/carouselRecyclerView"
        ... />
    
    
    
    <com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedEntryView
        android:id="@+id/toFeedLink"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="36dp"
        app:buzzvilFeedEntryViewName="carousel_to_feed_link">
    
        
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
    
            <TextView
                style="@style/NativeCarouselToFeedLink"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="포인트 더 받으러 가기" />
    
            <ImageView
                android:layout_width="16dp"
                android:layout_height="16dp"
                android:scaleType="centerInside"
                android:src="@drawable/ic_chevron_right" />
        </LinearLayout>
    </com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedEntryView>
    
  2. Set the visibility of the BenefitHub entry point according to the ad allocation response.

    • Show the carousel and BenefitHub entry point when there are ads to display.
    • Hide the carousel and BenefitHub entry point while ads have not yet loaded, if an error occurs, or if there are no ads.

    The following example changes the visibility of RecyclerView (the carousel) and FeedEntryView in NativeAd2CarouselActivity according to whether the allocation request succeeded.

    Kotlin
    import com.buzzvil.buzzad.benefit.core.ad.AdError
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2PoolInitListener
    
    private fun initCarouselPool() {
        // 광고 중복 할당을 막기 위해 하나의 캐러셀에 하나의 NativeAd2Pool 인스턴스를 생성하여 사용합니다.
        val carouselPool = NativeAd2Pool(Constant.YOUR_NATIVE_ID)
        val REQUEST_AD_COUNT = 5
    
        // 현재 할당 받을 수 있는 광고의 갯수를 확인합니다.
        carouselPool.init(REQUEST_AD_COUNT, object : NativeAd2PoolInitListener {
            override fun onLoaded(adCount: Int) {
                // 어댑터를 초기화합니다.
                initCarouselAdapter(adCount, carouselPool)
    
                binding.carouselRecyclerView.visibility = View.VISIBLE
                binding.toFeedLink.visibility = View.VISIBLE
            }
    
            override fun onError(adError: AdError) {
                // TODO 광고 레이아웃을 숨김 처리 하는 등 적절한 에러 처리를 할 수 있습니다.
                binding.carouselRecyclerView.visibility = View.GONE
                binding.toFeedLink.visibility = View.GONE
            }
        })
    }
    
  3. Fix the position of the BenefitHub entry point.

    Different lengths of ad description text (textDescription) can produce different line counts, making the entry point shift between slides. Set a fixed lines value on the description view to keep it in a consistent position.

    The following example sets the description view's lines value to 2.

    XML
    <TextView
        android:id="@+id/textDescription"
        android:ellipsize="end"
        android:lines="2" />
    

Run the app to see the BenefitHub entry slide on the last carousel page and the entry point below the carousel. Then see Implement optional carousel features below to provide a user-friendly UI and UX.

Implement optional carousel features

This section presents some of the ways you can add features to a carousel. Adapt the code to your needs.

Implement infinite looping

You can implement an infinite loop that lets users scroll continuously through a finite set of carousel items. There are many approaches; this guide uses a simple method in which the adapter's getItemCount() returns a very large value.

Follow these steps to implement an infinite loop.

  1. Modify the adapter (NativeAd2CarouselAdapter) to accept the isInfiniteLoopEnabled parameter.

    Kotlin
    class NativeAd2CarouselAdapter(
        ...
        private val isInfiniteLoopEnabled: Boolean,
    ) : RecyclerView.Adapter<NativeAd2CarouselViewHolder>() {
        ...
    }
    
  2. Change the implementation of getItemCount() in NativeAd2CarouselAdapter. When using position, take the remainder after division by the list size to obtain the actual index.

  • getItemCount(): To make the RecyclerView appear to have infinite data, return Integer.MAX_VALUE when isInfiniteLoopEnabled is true and there are items to display.

  • getItemPosition(): Use the remainder of position divided by the list size to return the actual index.

    Kotlin
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
    
    class NativeAd2CarouselAdapter(
        private val list: List<NativeAd2CarouselItem>,
        private val carouselPool: NativeAd2Pool,
        private val isInfiniteLoopEnabled: Boolean,
    ) : RecyclerView.Adapter<NativeAd2CarouselViewHolder>() {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NativeAd2CarouselViewHolder {
            val inflater = parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            val binding = CarouselItemBinding.inflate(inflater, parent, false)
            return NativeAd2CarouselViewHolder(binding)
        }
    
        override fun getItemCount(): Int {
            val actualItemCount = list.size
            return if (isInfiniteLoopEnabled && actualItemCount > 0) {
                // 무한 루프를 쉽게 구현하는 방법으로 매우 큰 수를 여기서 반환합니다.
                Int.MAX_VALUE
            } else actualItemCount
        }
    
        override fun onBindViewHolder(holder: NativeAd2CarouselViewHolder, position: Int) {
            // position 대신 itemPosition을 사용합니다.
            val itemPosition = getItemPosition(position)
    
            // 해당 position에 해당하는 NativeAd2ViewBinder가 carouselPool을 사용하도록 합니다.
            holder.setPool(carouselPool, itemPosition)
    
            // 아이템의 타입을 구분하여 적절한 bind() 함수를 호출합니다.
            val item = list[itemPosition]
    
            if (item is NativeAd2Item) {
                holder.bind(itemPosition)
            } else if (item is CarouselToFeedSlideItem) {
                holder.bind(itemPosition, item.feedPromotion)
            }
        }
    
        override fun onViewRecycled(holder: NativeAd2CarouselViewHolder) {
            super.onViewRecycled(holder)
    
            // position 대신 itemPosition을 사용합니다.
            val itemPosition = getItemPosition(holder.adapterPosition)
    
            // unbind를 반드시 호출하여 뷰를 재사용할 때 문제가 발생하지 않게 합니다.
            holder.unbind(itemPosition)
        }
    
        private fun getItemPosition(position: Int): Int {
            return if (isInfiniteLoopEnabled) {
                position % list.size
            } else {
                position
            }
        }
    }
    
  1. Add NativeAd2CarouselAdapter(isInfiniteLoopEnabled) to the activity (NativeAd2CarouselActivity).

    Kotlin
    import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool
    
    private fun initCarouselAdapter(adCount: Int, carouselPool: NativeAd2Pool) {
        // 무한 스크롤을 적용합니다.
        val isInfiniteLoopEnabled = true
    
        // 할당 받은 광고 갯수(adCount) 크기의 아이템 리스트를 만들어 NativeAd2CarouselAdapter를 생성합니다.
        val list = buildCarouselItems(adCount)
        val adapter = NativeAd2CarouselAdapter(list, carouselPool, isInfiniteLoopEnabled)
    
        // RecyclerView에 어댑터를 설정합니다.
        binding.carouselRecyclerView.adapter = adapter
    
        if (isInfiniteLoopEnabled) {
            // 적당히 큰 수의 position으로 이동하여 무한 스크롤 효과를 구현합니다.
            binding.carouselRecyclerView.layoutManager?.scrollToPosition(list.size * 10000)
        }
    }
    
Implement a loading screen

Use NativeAd2StateChangedListener to show a loading screen during refreshes after ad participation, visually indicating network request delays.

Kotlin
import com.buzzvil.buzzad.benefit.core.ad.AdError
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2StateChangedListener
import com.buzzvil.buzzad.benefit.presentation.feed.entrypoint.FeedPromotion

class NativeAd2CarouselViewHolder(
    private val binding: CarouselItemBinding
) : RecyclerView.ViewHolder(binding.root) {
    ...

    init {
        ...

        // 광고 로딩 상태에 따라 로딩 뷰를 보여주거나 숨깁니다.
        nativeAd2ViewBinder.addNativeAd2StateChangedListener(object : NativeAd2StateChangedListener {
            override fun onComplete() {
                setLoadingView(false)
            }

            override fun onError(adError: AdError) {
                setLoadingView(false)
            }

            override fun onNext(nativeAd2: NativeAd2) {
                setLoadingView(false)
            }

            override fun onRequested() {
                setLoadingView(true)
            }
        })

        ...
    }

    ...

    fun bind(position: Int, feedPromotion: FeedPromotion) {
        ...

        // 베네핏허브 진입 슬라이드는 로딩 뷰가 필요 없기 때문에 false로 설정합니다.
        setLoadingView(false)
    }

    ...

    private fun setLoadingView(isLoading: Boolean) {
        if (isLoading) {
            binding.loadingView.visibility = View.VISIBLE
        } else {
            binding.loadingView.visibility = View.GONE
        }
    }
}
Register an ad event listener

Record when carousel ad events occur, such as impressions and clicks, to understand user behavior.

Kotlin
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2EventListener
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2ViewBinder
import com.buzzvil.buzzad.benefit.presentation.reward.RewardResult

class NativeAd2CarouselViewHolder(
    private val binding: CarouselItemBinding
) : RecyclerView.ViewHolder(binding.root) {
    private val nativeAd2ViewBinder: NativeAd2ViewBinder

    init {
        ...

        // NativeAd2View와 하위 컴포넌트를 바인드합니다.
        nativeAd2ViewBinder = NativeAd2ViewBinder.Builder()
            .nativeAd2View(nativeAd2View)
            .mediaView(mediaView)
            .titleTextView(titleTextView)
            .descriptionTextView(descriptionTextView)
            .iconImageView(iconImageView)
            .ctaView(ctaView)
            .build(Constant.YOUR_NATIVE_ID)


        // 필요에 따라 광고 이벤트 상태를 받아올 수 있는 NativeAd2EventListener를 설정하세요.
        // 로그 기록, 단순 알림 외에 다른 동작을 추가하는 것을 권장하지 않습니다.
        // 자동 갱신 등 네이티브 2.0의 기능과 직접 구현한 동작이 충돌할 수 있습니다.
        nativeAd2ViewBinder.addNativeAd2EventListener(object : NativeAd2EventListener {
            override fun onClicked(nativeAd2: NativeAd2) {}
            override fun onImpressed(nativeAd2: NativeAd2) {}
            override fun onParticipated(nativeAd2: NativeAd2) {}
            override fun onRewardRequested(nativeAd2: NativeAd2) {}
            override fun onRewarded(nativeAd2: NativeAd2, rewardResult: RewardResult) {}
        })

        ...
    }
    ...
}
Partially show neighboring ads

Use RecyclerView's padding and clipToPadding properties to reduce item sizes and partially show the previous and next ads, encouraging users to scroll.

XML


<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/carouselRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:clipToPadding="false"
    android:orientation="horizontal"
    android:paddingHorizontal="32dp"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    tools:listitem="@layout/view_native_ad2_item" />
Add spacing between ads

There are several ways to set spacing between ad items in a RecyclerView. This guide uses ItemDecoration. Follow these steps.

  1. Implement PaddingDividerDecoration by extending RecyclerView.ItemDecoration.

    Kotlin
    class PaddingDividerDecoration(private val paddingDp: Int) : RecyclerView.ItemDecoration() {
        override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
            super.getItemOffsets(outRect, view, parent, state)
            // 여백을 한 쪽만 설정하면 PagerSnapHelper가 약간 어긋나게 동작하므로, 양쪽에 동일하게 여백을 넣습니다.
            val paddingPx = paddingDp.dpToPx() / 2
            outRect.left = paddingPx
            outRect.right = paddingPx
        }
    
        private fun Int.dpToPx(): Int {
            return (this * Resources.getSystem().displayMetrics.density).toInt()
        }
    }
    
  2. Add PaddingDividerDecoration to the RecyclerView.

Kotlin
import com.buzzvil.buzzad.benefit.nativead2.api.NativeAd2Pool

private fun initCarouselAdapter(adCount: Int, carouselPool: NativeAd2Pool) {
    ...

    // 양 옆 여백 설정
    binding.carouselRecyclerView.addItemDecoration(PaddingDividerDecoration(16))

    ...
}