Good fit when
- Your library works in a debug build and a customer reports it failing in release.
- The native Android and iOS side is fine and the React Native, Flutter or Unity wrapper keeps falling behind it.
- The same integration questions arrive repeatedly, which usually means the entry points or the guide are the problem.
- Nobody can state which OS, host framework and dependency versions the current release actually supports.
How we build it
- 1Agree the public surface: entry points, threading, failure behaviour, and what is safe to call twice.
- 2Build the native Android and iOS libraries first, then wrap them one platform at a time.
- 3Ship keep rules with the library and test them from a release build of a consuming app.
- 4Verify from the packaged artifact — the AAB, APK or framework — rather than from the source.
- 5Publish with a support matrix, sample apps that resolve the published version, and a guide a developer can follow unaided.
Expected outcome
An SDK another team can integrate without contacting you, that survives their release build, and whose next version does not break their app.
An SDK is judged by what the host app is allowed to assume
An app owns its process. It decides when it starts, which thread does what, how much it logs, and what happens when it crashes. An SDK owns none of that. Somebody else's app initializes your code, on a thread of their choosing, possibly before their first screen exists, next to other SDKs, compiled with their build settings, their minimum OS version, their dependency versions and their release configuration.
That changes what the deliverable is. The public surface becomes the product: a small number of entry points that are safe to call early, safe to call twice, and documented as to which thread they expect and what they do when they fail. An SDK that throws from an initializer takes the host app down with it, and the crash report lands on their desk with your class names in it. Their user blames them.
The second thing an integrator inherits is your dependency list. Every library you pull in is a library you force on them, at a version that has to coexist with whatever they already use. Gradle resolves a version conflict by taking the highest request, so a transitive dependency you bumped casually can change the behaviour of code you have never seen. Keeping the footprint small is a compatibility decision, not a style preference.
Per-platform reality, and why the wrapper is where it breaks
Native Android and iOS are one problem. React Native, Flutter and Unity add a wrapper layer, and the wrapper is where most breakage happens, because its whole job is forwarding calls one way and callbacks the other way while preserving things the underlying SDK cares about: which thread it is on, which lifecycle object it is attached to, and whether a callback fires once or twice.
None of those survive a naive bridge. A promise resolved twice crashes some runtimes and silently does nothing on others. A callback delivered on a background thread reaches code that then touches a view, which is a main-thread-only operation on both platforms. An object identity that the native SDK uses to correlate a request with its result gets recreated across the bridge and the correlation is lost. These are not exotic bugs; they are the default outcome of writing the wrapper by hand and testing it once on the happy path.
What each target actually requires
Android
A Kotlin or Java library published as an .aar, shipping its own keep rules through consumerProguardFiles so the host does not have to paste anything into their configuration. Your minimum SDK version and your dependency versions become constraints on every app that embeds you.
iOS
An XCFramework consumed through Swift Package Manager or CocoaPods, with the simulator and device slices that customers expect. A binary Swift framework needs library evolution enabled to stay usable across compiler versions, and Apple's privacy manifest and signature requirements for third-party SDKs apply to you rather than to the app that embeds you.
React Native
A native module plus typed JavaScript definitions, an autolinking-friendly package and a podspec. Module methods do not run on the main thread by default, so anything that creates or attaches a view has to be dispatched there deliberately, and event emitters have to survive a reload during development.
Flutter
A Dart facade over per-platform implementations, communicating through method and event channels. Native views need a platform view, channel errors have to be mapped into Dart exceptions rather than silently returning null, and every value crossing the channel has to be a type the codec can carry.
Unity
A C# API over an Android plugin and an iOS plugin. Callbacks from Android come back through a proxy object, activity references have to be fetched from the player rather than cached, and Unity controls its own Gradle template and minification settings, so keep rules have to be delivered there too.
The release build is a different product from the debug build
On Android, release builds run R8, which shrinks, renames and optimizes based on what it can prove is reachable. Anything resolved by name at runtime is invisible to that analysis: a class named in a string, an adapter looked up reflectively, an entry point referenced from a manifest or a configuration file, a field name a serializer expects. The code compiles. The debug build works. The release build fails, or worse, quietly does nothing.
Keep rules are the fix, and reading the source cannot tell you whether yours are correct. A rule is a pattern; whether it matched the classes you meant is a property of the output, not of the rule. This is the single most common reason an SDK behaves differently at a customer's site than in your own project, because the customer builds release and you build debug all day.
An SDK also has to carry its own rules rather than documenting them. Rules in an integration guide get pasted once and never updated, so a customer on an old copy of your instructions is running last year's configuration against this year's library. Shipping them with the artifact makes the correct configuration the default one.
Verify from the artifact, not from the source
Unit tests structurally cannot catch this class of problem. They run against compiled classes in a test configuration, not against the shrunk, renamed bundle that ships. A class can pass every test and then be absent from the artifact a customer installs. The same gap covers packaging: a resource that did not get bundled, a slice missing from a framework, a transitive dependency that resolved locally because of a project path reference and does not exist for anyone consuming the published version.
So the verification that matters is mechanical and physical. Build the thing you will publish, consume it the way a customer will, look inside it, and then run it.
The checks that actually catch packaging failures
Depend on it by version, not by path
Publish a release candidate and have the sample app resolve it as a normal dependency. A local project reference hides exactly the packaging mistakes you are looking for.
Build with shrinking on
The host sample app's release variant, minification enabled, your consumer rules in effect. This is the configuration your customers use and the one most SDK teams never build.
List what is in the bundle
Inspect the packages and classes in the dex output and confirm that everything resolved by name at runtime still exists under that name. Keep the R8 mapping file for the release, because it is also what lets you retrace a stack trace a customer sends you six months later.
Inspect the framework, not the build log
On iOS, check the architectures and the symbols present in the built binary. A successful build is not evidence that the symbol an integrator links against is exported.
Exercise every entry point on a device
A reflective lookup fails at the moment it runs and not before, so the release artifact has to be executed, not just produced.
Versioning is a promise to people who will not read the changelog
Host apps upgrade an SDK during a sprint that is about something else. That is the situation your version numbers have to survive. Semantic versioning honestly applied, deprecation before removal, and one short migration note per breaking change cover most of it. Removing a method in a minor version is the fastest way to be pinned to an old release forever.
The subtler breaking change is a dependency bump. If your library requires a newer version of a third-party SDK, you have changed the host app's build whether or not your own API moved, and if their code depends on the older behaviour, your minor upgrade broke them. Compile against the oldest version you claim to support, test against the newest, and say in writing which range you actually cover.
That statement of coverage is worth maintaining as a table: minimum OS versions, minimum React Native, Flutter or Unity versions, and which versions of any SDK you integrate with. Most support conversations are really requests for that table.
App Store and Google Play mechanics land on you anyway
An SDK does not ship to a store, but every one of its customers does, so store requirements become your requirements one step removed. Anything your library declares gets merged into the host's manifest, including permissions, which means a permission you added for an optional feature is a permission somebody now has to justify in a review or a data-safety declaration. Apple's privacy manifest and signing rules for third-party SDKs sit in the same category: they are conditions on your artifact that the app developer cannot satisfy on your behalf.
Platform deadlines also arrive as your problem, because a target API level requirement or a new declaration form reaches the app first and the app cannot move until its SDKs have. Being ahead of that is a large part of what makes an SDK pleasant to depend on.
The last piece is documentation and samples, which are part of the product rather than an afterthought. A working sample app per platform, resolved from the published artifact, is simultaneously the integration guide, the regression test and the reproduction case a support conversation starts from.
Worked example: the pass that proves a release build is intact
This is the sequence we run before an SDK version goes out, because it is the only one that catches shrinking and packaging problems. None of these steps can be replaced by reviewing code or by adding tests.
- 1Publish a release candidate firstThe artifact under test is the published one. Sample apps resolve it by version like any other dependency, so anything that was only working because of a local project reference fails here, where it is cheap.
- 2Build the host app for releaseMinification on, the library's consumer rules in effect, mapping file saved. This is the build configuration a customer ships and the one a debug-only test cycle never produces.
- 3Look inside the bundleEnumerate what survived and confirm that every class resolved by name at runtime is present under that name, not renamed and not removed.
- 4Run the built artifact and touch everythingInstall the release build on a device and exercise each public entry point, including the failure paths, because a missing class announces itself only when the lookup happens.
- 5Repeat per wrapperThe React Native, Flutter and Unity samples are separate builds with separate configuration and separate minification settings. The one nobody rebuilt is reliably the one that breaks in the field.
The failure this prevents is the expensive one: a customer's release build crashing or silently doing nothing while your own build and your test suite are both green, and the report arriving days later with an obfuscated stack trace attached.
Questions we get asked about this
Can you work on the SDK we already have?
That is the usual starting point, and the first pass is normally not a code review. It is building the artifacts and reproducing the reported failure in a release build, because a defect that only appears after shrinking or packaging cannot be found by reading the source that produced it.
Do we need all three wrappers?
No, and each one you add is a surface you maintain forever: its own sample app, its own release, its own set of host framework versions to support. Native Android and iOS first, then whichever wrapper your integrators actually ask for. A wrapper published and then left behind the native library is worse than not having it.
Why can't tests cover the release-build problem?
Because tests run before the step that causes it. Shrinking and renaming happen when the shippable artifact is assembled, and a test suite exercises classes that have not been through that. A green suite and a broken release build are entirely compatible states, which is why verification has to read the artifact.
How is this priced?
Build work is priced at $5,000 to $10,000 per estimated month of work, positioned in that range by complexity. Work after release, including per-version verification passes and support for a new host framework version, is billed at $85 to $165 per hour for hours actually worked. There is no standing retainer.
Typical deliverables
- Native Android and iOS library targets
- React Native, Flutter or Unity wrapper layer
- Consumer keep rules and release-build verification
- Sample apps, integration guide and a support matrix
Best for
Product and engineering teams who distribute functionality to other companies' apps, and anyone whose SDK works in debug and breaks in a customer's release build.
Discuss this service