Android Integration Guide

Integrate AppSpike into your Android app and serve your first ad in under 5 minutes.

AppSpike sits on top of your existing ad networks and uses real-time bidding to route every impression to the highest-paying demand source. You keep your current ad network accounts — AppSpike optimizes across all of them.

Prerequisites

Step 1: Add the SDK

Once you are approved for early access, AppSpike will send you the SDK along with setup instructions for your project.

Step 2: Initialize the SDK

Initialize AppSpike as early as possible, ideally in your Application class. Pass your API key and the ad network plugins you want to mediate:

MyApp.kt
import dev.appspike.AppSpike
import dev.appspike.InitResult
import tech.pyde.ads.admob.AdMobPlugin
import tech.pyde.ads.ironsource.IronSourcePlugin

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        AppSpike.initialize(
            context = this,
            apiKey = "YOUR_API_KEY",
            plugins = listOf(AdMobPlugin, IronSourcePlugin),
        ) { result ->
            when (result) {
                is InitResult.Success -> {
                    Log.d("AppSpike", "SDK initialized")
                }
                is InitResult.Error -> {
                    Log.e("AppSpike", "Init failed: ${result.message}")
                }
            }
        }
    }
}

Banners are loaded asynchronously. After loading, add the banner's view to your layout and manage its lifecycle:

MainActivity.kt
import dev.appspike.AppSpike
import tech.pyde.ads.banner.BannerAd

class MainActivity : AppCompatActivity() {
    private var bannerAd: BannerAd? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onResume() {
        super.onResume()
        bannerAd?.resume()

        val adContainer = findViewById<FrameLayout>(R.id.ad_container)
        AppSpike.loadBanner("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { ad ->
                bannerAd?.let { old ->
                    adContainer.removeView(old.bannerView)
                    old.destroy()
                }
                bannerAd = ad
                ad.initialize()
                adContainer.addView(ad.bannerView)
            }.onFailure { error ->
                Log.e("AppSpike", "Banner failed: ${error.message}")
            }
        }
    }

    override fun onPause() {
        bannerAd?.pause()
        super.onPause()
    }

    override fun onDestroy() {
        bannerAd?.destroy()
        super.onDestroy()
    }
}

Add the container to your layout XML:

activity_main.xml
<FrameLayout
    android:id="@+id/ad_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom" />

Step 4: Interstitial Ads

GameActivity.kt
import dev.appspike.AppSpike
import tech.pyde.ads.interstitial.InterstitialAd

class GameActivity : AppCompatActivity() {
    private var interstitialAd: InterstitialAd? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadInterstitial()
    }

    private fun loadInterstitial() {
        AppSpike.loadInterstitial("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { ad ->
                interstitialAd = ad
            }.onFailure { error ->
                Log.e("AppSpike", "Interstitial failed to load: ${error.message}")
            }
        }
    }

    // Call this at a natural break point (e.g., between levels)
    private fun showInterstitial() {
        interstitialAd?.let { ad ->
            ad.show()
            interstitialAd = null
            loadInterstitial()
        } ?: Log.d("AppSpike", "Interstitial not ready yet")
    }
}

Step 5: Rewarded Video Ads

Rewarded video ads use a reactive stream to deliver events. Subscribe to the observable returned by show() to track reward delivery:

StoreActivity.kt
import com.badoo.reaktive.observable.subscribe
import dev.appspike.AppSpike
import tech.pyde.ads.rewarded.RewardedVideo
import tech.pyde.ads.rewarded.RewardedVideoEvent

class StoreActivity : AppCompatActivity() {
    private var rewardedVideo: RewardedVideo? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadRewarded()
    }

    private fun loadRewarded() {
        AppSpike.loadRewardedVideo("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { video ->
                rewardedVideo = video
            }.onFailure { error ->
                Log.e("AppSpike", "Rewarded video failed to load: ${error.message}")
            }
        }
    }

    // Call this when the user opts in (e.g., "Watch ad for 50 coins")
    private fun showRewarded() {
        rewardedVideo?.let { video ->
            video.show().subscribe(
                onNext = { event ->
                    when (event) {
                        is RewardedVideoEvent.RewardReceived -> {
                            Log.d("AppSpike", "User earned reward")
                            grantReward()
                        }
                        is RewardedVideoEvent.ClosedWithReward -> {
                            Log.d("AppSpike", "Ad closed with reward")
                        }
                        is RewardedVideoEvent.ClosedEarly -> {
                            Log.d("AppSpike", "User closed ad early")
                        }
                        is RewardedVideoEvent.Started -> {
                            Log.d("AppSpike", "Rewarded video started")
                        }
                        is RewardedVideoEvent.DisplayError -> {
                            Log.e("AppSpike", "Display error: ${event.message}")
                        }
                    }
                },
                onComplete = {
                    rewardedVideo = null
                    loadRewarded()
                },
                onError = { error ->
                    Log.e("AppSpike", "Rewarded error: ${error.message}")
                },
            )
        } ?: Log.d("AppSpike", "Rewarded video not ready yet")
    }

    private fun grantReward() {
        // Your reward logic here
    }
}

Step 6: Native Ads

Native ads use a view binder pattern. Load the ad, configure the binder with your layout and view IDs, then bind it to produce a View:

FeedActivity.kt
import dev.appspike.AppSpike
import tech.pyde.ads.nativead.NativeAd

class FeedActivity : AppCompatActivity() {
    private var nativeAd: NativeAd? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_feed)
    }

    override fun onResume() {
        super.onResume()
        nativeAd?.resume()

        val adContainer = findViewById<FrameLayout>(R.id.native_ad_container)
        AppSpike.loadNativeAd("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { ad ->
                nativeAd?.let { old ->
                    adContainer.removeView(old.binder().bind(this@FeedActivity))
                    old.destroy()
                }
                nativeAd = ad

                val binder = ad.binder()
                binder.setNativeAdLayoutResId(R.layout.native_ad_layout)
                binder.setHeadlineViewId(R.id.ad_headline)
                binder.setBodyViewId(R.id.ad_body)
                binder.setCallToActionViewId(R.id.ad_call_to_action)
                binder.setIconViewId(R.id.ad_icon)
                binder.setMediaContentViewId(R.id.ad_media)
                binder.setOptionsViewId(R.id.ad_options)

                val adView = binder.bind(this@FeedActivity)
                ad.initialize()
                adContainer.addView(adView)
            }.onFailure { error ->
                Log.e("AppSpike", "Native ad failed: ${error.message}")
            }
        }
    }

    override fun onPause() {
        nativeAd?.pause()
        super.onPause()
    }

    override fun onDestroy() {
        nativeAd?.destroy()
        super.onDestroy()
    }
}

Create a custom layout for your native ad:

res/layout/native_ad_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="8dp">

    <TextView android:id="@+id/ad_headline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:textStyle="bold" />

    <TextView android:id="@+id/ad_body"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ImageView android:id="@+id/ad_icon"
        android:layout_width="40dp"
        android:layout_height="40dp" />

    <FrameLayout android:id="@+id/ad_media"
        android:layout_width="match_parent"
        android:layout_height="200dp" />

    <FrameLayout android:id="@+id/ad_options"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button android:id="@+id/ad_call_to_action"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

Step 7: Rewarded Interstitial Ads

Rewarded interstitials combine the full-screen experience of an interstitial with a reward. They follow the same reactive pattern as rewarded video ads:

LevelActivity.kt
import com.badoo.reaktive.observable.subscribe
import dev.appspike.AppSpike
import tech.pyde.ads.rewarded.RewardedInterstitial
import tech.pyde.ads.rewarded.RewardedVideoEvent

class LevelActivity : AppCompatActivity() {
    private var rewardedInterstitial: RewardedInterstitial? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadRewardedInterstitial()
    }

    private fun loadRewardedInterstitial() {
        AppSpike.loadRewardedInterstitial("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { ad ->
                rewardedInterstitial = ad
            }.onFailure { error ->
                Log.e("AppSpike", "Rewarded interstitial failed to load: ${error.message}")
            }
        }
    }

    private fun showRewardedInterstitial() {
        rewardedInterstitial?.let { ad ->
            ad.show().subscribe(
                onNext = { event ->
                    when (event) {
                        is RewardedVideoEvent.RewardReceived -> grantReward()
                        is RewardedVideoEvent.DisplayError -> {
                            Log.e("AppSpike", "Display error: ${event.message}")
                        }
                        else -> {}
                    }
                },
                onComplete = {
                    rewardedInterstitial = null
                    loadRewardedInterstitial()
                },
                onError = { error ->
                    Log.e("AppSpike", "Error: ${error.message}")
                },
            )
        } ?: Log.d("AppSpike", "Rewarded interstitial not ready yet")
    }

    private fun grantReward() {
        // Your reward logic here
    }
}

Jetpack Compose

Use AndroidView to embed banner ads in Compose layouts:

Banner in Compose
import androidx.compose.runtime.*
import androidx.compose.ui.viewinterop.AndroidView

@Composable
fun BannerAdSection() {
    var bannerAd by remember { mutableStateOf<BannerAd?>(null) }
    var bannerView by remember { mutableStateOf<View?>(null) }

    LaunchedEffect(Unit) {
        AppSpike.loadBanner("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { ad ->
                ad.initialize()
                bannerAd = ad
                bannerView = ad.bannerView
            }
        }
    }

    DisposableEffect(Unit) {
        onDispose { bannerAd?.destroy() }
    }

    bannerView?.let { view ->
        key(view) {
            AndroidView(
                factory = { view },
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
}
Interstitial in Compose
@Composable
fun LevelCompleteScreen() {
    var interstitialAd by remember { mutableStateOf<InterstitialAd?>(null) }

    LaunchedEffect(Unit) {
        AppSpike.loadInterstitial("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { interstitialAd = it }
        }
    }

    Button(onClick = {
        interstitialAd?.show()
        interstitialAd = null
    }) {
        Text("Continue")
    }
}

Fragment Example

Load and display a banner ad inside a Fragment:

AdFragment.kt
import dev.appspike.AppSpike
import tech.pyde.ads.banner.BannerAd

class AdFragment : Fragment(R.layout.fragment_ad) {
    private var bannerAd: BannerAd? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val adContainer = view.findViewById<FrameLayout>(R.id.ad_container)

        AppSpike.loadBanner("YOUR_PLACEMENT_ID") { result ->
            result.onSuccess { ad ->
                bannerAd = ad
                ad.initialize()
                adContainer.addView(ad.bannerView)
            }
        }
    }

    override fun onResume() {
        super.onResume()
        bannerAd?.resume()
    }

    override fun onPause() {
        bannerAd?.pause()
        super.onPause()
    }

    override fun onDestroyView() {
        bannerAd?.destroy()
        bannerAd = null
        super.onDestroyView()
    }
}

Testing

Enable test mode during development to avoid serving live ads:

AppSpike.initialize(
    context = this,
    apiKey = "YOUR_API_KEY",
    plugins = listOf(AdMobPlugin, IronSourcePlugin),
    testMode = true,
) { result ->
    // Test ads will always fill and never charge
}

ProGuard / R8

If you use code shrinking, add to your proguard-rules.pro:

-keep class dev.appspike.** { *; }
-keep class tech.pyde.ads.** { *; }

Best Practices

Next Steps