In Android development, we often need to carefully handle the initialization and callbacks of various SDKs, especially when asynchronous operations are involved. In this article, we'll discuss four ways to optimize these callbacks in Kotlin, primarily using several of Google's SDKs as examples.
First, let's revisit the traditional callback mechanism. In Java, callback functions are commonly used to handle the results of asynchronous tasks, but this method has several drawbacks:
In Kotlin, although we have powerful tools like coroutines, callback issues still exist due to the need to maintain compatibility with a large amount of Java code and legacy projects. Taking Google's SDKs as an example, although some have provided KTX extension packages, they have not optimized the initialization process.
AtomicBoolean
to Simply Record StateThe most straightforward method is to use an AtomicBoolean
to record whether the SDK has been successfully initialized.
private val sdkInitialized = AtomicBoolean(false)
MobileAds.initialize(context) { result: InitializationStatus ->
// Check if all adapters are ready
sdkInitialized.set(result.adapterStatusMap.values.any {
it.initializationState == AdapterStatus.State.READY
})
}
The issue is: This method requires manually checking sdkInitialized
every time you call other SDK methods; it cannot await for initialization to complete (while yield the CPU resources).
Example:
fun prepareNextRewardedAd() {
if (sdkInitialized.get()) {
// Proceed with normal initialization
} else {
// Show error message
}
}
Pros:
Cons:
CompletableDeferred
to AwaitCompletableDeferred
allows us to suspend a coroutine until a task is completed, making it ideal for scenarios where we need to wait for SDK initialization.
private val isSDKInitialized = CompletableDeferred<Unit>()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// Initialization successful
} else {
// Initialization failed
}
// Mark as complete regardless of success or failure to avoid the coroutine being suspended indefinitely
isSDKInitialized.complete(Unit)
}
override fun onBillingServiceDisconnected() {
// You can attempt to reconnect
}
})
Usage:
suspend fun queryMerchandise(...) = withContext(Dispatchers.IO) {
// Wait for initialization to complete
isSDKInitialized.await()
billingClient.queryProductDetails(...)
}
Pros:
Cons:
Channel
to Handle Producer-ConsumerWhen you need to handle a callback result in a single producer-consumer scenario, Channel
is a good choice.
Example: Loading a rewarded ad from AdMob SDK
private val rewardedAdChannel = Channel<RewardedAd?>(1)
...
RewardedAd.load(
activity,
adUnitId,
adRequest,
object : RewardedAdLoadCallback() {
override fun onAdLoaded(ad: RewardedAd) {
rewardedAdChannel.trySend(ad).isSuccess
}
override fun onAdFailedToLoad(adError: LoadAdError) {
rewardedAdChannel.trySend(null).isSuccess
}
}
)
Usage:
suspend fun showSingleRewardedAd(activity: Activity) {
// Wait for the ad to load
val ad = rewardedAdChannel.receive()
if (ad != null) {
ad.show(activity) { rewardItem ->
// Handle reward logic
}
} else {
// Handle loading failure
}
}
Pros:
Cons:
Channel
lifecycle to prevent memory leaks.Channel
may block; you need to set an appropriate capacity or handle timeouts.SharedFlow
to Handle Recurring EventsWhen you need to handle multiple initializations or state updates, SharedFlow
is very suitable.
Example: Suppose we have a network status change that needs to be monitored multiple times
private val networkStatusFlow = MutableSharedFlow<NetworkStatus>(replay = 1)
fun startNetworkMonitoring() {
networkMonitor.setOnNetworkStatusChangedListener { status ->
networkStatusFlow.tryEmit(status)
}
}
Usage:
fun observeNetworkStatus() {
lifecycleScope.launch {
networkStatusFlow.collect { status ->
when (status) {
NetworkStatus.Available -> // Network is available
NetworkStatus.Unavailable -> // Network is unavailable
}
}
}
}
Pros:
Cons:
Depending on your specific needs, you can choose different methods to optimize callback handling:
AtomicBoolean
for simple initialization state recording.CompletableDeferred
when you need to wait for initialization to complete within coroutines.Channel
for one-time result callbacks, such as ad loading.SharedFlow
when you need to monitor multiple state updates.I hope this article provides some insights into handling SDK initialization callbacks. When you encounter similar issues in the future, you can try choosing the most suitable method to improve your code's readability and maintainability.