The KMP vs Flutter decision is usually framed around UI and shared business logic, but integration teams care about a different problem: wrapping native SDKs without codegen pain. That has become an achievable goal for analytics and ad SDKs in both Kotlin Multiplatform and Flutter, provided you treat the wrapper as a narrow contract instead of a mirror of the native API.
Where codegen pain starts with native SDKs
Analytics, attribution, and ad mediation SDKs share a common feature: a wide public API. A typical native SDK exposes request builders, listener interfaces, consent status helpers, and dozens of model classes. Generated bindings that overlay this API may feel productive during the first integration, but they come with hidden costs. Each type is represented in generated source, and each generated type will need to be updated when the SDK publishes a new version.
The worst codegen loops happen on CI. If you regenerate bindings after an SDK update and that regeneration fails, your release is blocked until someone diagnoses a type mismatch in a header you barely understand. This is true for Kotlin Multiplatform projects using cinterop over an entire iOS SDK and equally true for Flutter packages whose generated channels mirror the SDK’s model layer.
The workaround is not better codegen; it is less surface area.
KMP: expect/actual instead of generated bindings
Kotlin Multiplatform offers two common ways to reach native code: cinterop and expect/actual. Developers who want to integrate a native ad SDK directly often start with cinterop, asking it to generate Kotlin bindings from a framework or header. If you point cinterop at the whole SDK, codegen becomes part of your routine. KMP’s expect/actual gives you a better starting point because it lets you define the API you actually want, then fill in that API manually per platform.
Imagine your app needs to record an ad revenue event in its shared analytics pipeline:
// commonMain
expect fun reportAdRevenue(
adUnitId: String,
revenueMicros: Long,
currency: String
)
The Android actual calls the native SDK’s Java or Kotlin API directly, with no binding layer between them:
// androidMain
actual fun reportAdRevenue(
adUnitId: String,
revenueMicros: Long,
currency: String
) {
AdRevenueCollector.shared.record(
adUnitId = adUnitId,
revenueMicros = revenueMicros,
currency = currency
)
}
The iOS actual does not need to cover the whole SDK either. Write a small Swift or Objective-C facade that wraps exactly the SDK calls you use, then expose that facade to Kotlin/Native. The interop definition then lists one facade header instead of the entire SDK umbrella header. This one change drastically reduces codegen output and keeps the generated iOS surface readable.
When the upstream ad SDK updates its API, you update the facade in the native Xcode project and leave the expect declaration untouched.
Cover behavior, not SDK shape
A recurring mistake in KMP projects is creating an expect/actual declaration for every method and delegate callback found in the SDK. That recreates the codegen problem by hand. Instead, study how your analytics backend consumes data: an ad impression has an ad unit identifier, a revenue value, a currency, and maybe a network name. Map the common API to the events your product actually uses, such as consent changes, impressions, and revenue callbacks.
Flutter: platform channels as your SDK contract
In Flutter, the codegen-free way to reach native SDKs is the platform channel API. MethodChannel handles one-shot requests, while EventChannel works well for recurring native events such as impression callbacks and reward completions. Both use a small set of message types that are easy to serialize across languages: strings, numbers, booleans, maps, and lists. For analytics payloads, that is usually enough.
Rather than exposing every method of an ad SDK through a channel, define your own Dart wrapper around a narrow channel contract:
class AdRevenueChannel {
static const _channel = MethodChannel('app/ad_events');
Future<void> report({
required String adUnitId,
required double revenue,
required String currency,
}) {
return _channel.invokeMethod('report', {
'adUnitId': adUnitId,
'revenue': revenue,
'currency': currency,
});
}
}
Swift on the native side handles only the same few fields:
channel.setMethodCallHandler { call, result in
guard call.method == "report",
let args = call.arguments as? [String: Any] else {
return
}
let adUnitId = args["adUnitId"] as? String ?? ""
let currency = args["currency"] as? String ?? "USD"
// forward to the native ad network SDK
}
The Dart class becomes the single source of truth for channel names and argument keys. If you want compile-time safety without adding codegen, write a small unit test that lists the method names and string keys and compares them to the native constants. That trade is easier to maintain than a stack of generated Swift and Kotlin files.
If you do use a codegen package such as pigeon, keep its contract centered on your analytics domain events, not on the SDK object model. When generated Dart code starts to mirror the full shape of a native ad SDK, the regeneration pain returns.
KMP vs Flutter decision guide for SDK integrations
The right choice is not about which framework is more modern. It is about minimizing maintenance load around a native SDK as your ad mediation setup grows.
Choose Kotlin Multiplatform with expect/actual when
- Your product already shares analytics, networking, or storage code in a KMP module, so adding a wrapper is natural.
- The native SDK returns rich types you need to inspect, such as mediation responses or consent state classes.
- Your team has an iOS engineer who can maintain a short Swift or Objective-C facade.
- You value compile-time guarantees over rapid Dart-driven prototyping.
Choose Flutter with platform channels when
- Your app is Dart-first and you want the smallest possible native code footprint.
- Ad and analytics events fit a simple key-value structure and you do not need deep introspection of native SDK objects.
- You want to iterate on mapping logic in Dart and use hot reload instead of a new native build.
- You prefer a focused unit test over codegen for keeping the native contract safe.
Pitfalls to avoid in either approach
- Do not wrap API methods your team never calls; each added method becomes a maintenance obligation.
- Do not place native SDK class names inside your expect declarations or channel method names.
- Do not merge an ad SDK update separately from its wrapper code; keep the two dependencies in the same pull request.
- Do not create several custom channels for one SDK; one ad event channel is easier to audit.
Final thought
Wrapping native analytics and ad SDKs without codegen pain is not a fantasy; it is a matter of scope. KMP’s expect/actual and Flutter’s platform channels both support a deliberately small, behavior-shaped contract that lets your analytics pipeline stay independent from the fragmented and frequently changing SDKs that power mobile ad monetization.
