Awesome Android SDK Design
Leveraging Modular SDK Builders for Better Library Design and Keeping Your Product Owners Happy
So, you’re building an Android SDK (library). And of course, you’re going to integrate the library inside a sample application before publishing in order to test it thoroughly (and use as a demo for Product and Sales team members). As you develop the library, you are bound to have Debug build variant features (SDK tooling) that you definitely do not want to ship in Production code. But, these debug features are extremely helpful for demo purposes and User Acceptance Testing (UAT).
Let’s start off with a thesis:
Testing / Debug code should never be included in shipped Production code.
So, how do you design an SDK with all of your awesome debug tooling and also avoid the riskiness of shipping them “turned off” in Production code?
Scenario (Part 1)!
Build an Android SDK that retrieves a fake user profile + a demo application with a “locked” down build variant that exposes no debug tooling. Also include “unlocked” build variants that showcase all of the debug features. The library should avoid shipping any testing/debug code in Production. Check out the example that I built below!
Note: That’s my (semi-fake) profile in the Mock flavor implementation hosted on a mock server at http://localhost:8080
!
So… what’s a good way to approach this? It’s common to see code examples online (e.g. StackOverflow answers, other Medium articles) with an isDebuggable
code-switch based on the BuildConfig
to block exposing your debug tooling.
But, this is risky! Debug code is getting shipped in Production code, even if the code paths are not being executed. We can and should be avoiding this. Not only that, but it’s harder to unit test because of the dependency on Android-specific components.
So, how do we achieve this level of code security without leveraging build configuration code-switches (which I would argue is the norm in the Android community) to alternate between our Debug and Release features?
Using Code Optimization Tools
But, does any of this really matter? Why not let your code obfuscation/optimization tools strip out any unused code paths, i.e. paths blocked by app level debuggable properties.
For non-enterprise level applications, this might be acceptable, but I feel it’s an unnecessary risk and easily avoidable. The downside to this option is that you will need the build steps in your release pipeline to include the exact code optimizations to be sure that any debug code has been fully removed.
It is important to mention that this option exists, but are you confident enough in your optimizations/obfuscation?
Leverage Modular Dev and Prod SDK Builders
Instead, let’s consider a different approach to separate Debug and Release features for an enterprise-level Android SDK.
The concept is simple:
Separate your Debug features and your Production features by writing two SDK builders (Dev and Prod).
Only the Prod builder is published with the final Android library artifacts. The Dev builder is only distributed with Debug flavors of your sample application.
Imagine that your Android SDK has many features that need heavy SDK tooling for automated testing, manual testing, product demos, etc.
For instance:
- Custom logger injections.
- Launching debug activities / feature drawers.
- Alternating between Development and Production environment endpoints.
- Switching to a mock server by changing your base URL to
localhost
for stubbing happy and sad path scenarios (e.g. with MockWebServer). - Custom network stacks with network interceptors for testing and certificate pinning for releases.
- On-demand locale switching for language verifications.
The list of potential debug features that you might want goes on and on. And we don’t want any exposed in shipped Production code.
Scenario (Part 2)!
Let’s go back to our user profile Android SDK + demo application scenario — let’s call it Hello. The app launches and returns a random user profile and displays it to the user on screen. These fake profiles are retrieved via the conveniently simple and free UI Names API.
Here’s the flow:
- Client application Hello launches.
- User clicks
Get Profile
button and theViewModel
launches ourUserProfileSdk
. - The SDK accepts an application
Context
parameter, so it begins a newActivity
and starts aViewModel
that performs our data fetching. - A fake profile is retrieved from the API and is rendered on screen to the user with all of the personal information and profile image.
- User clicks
Got It!
button, which exits our SDK and returns aResult
object to the client application Hello via a callback. - The client application renders the
Result
back to the user on the “Hello, My Name Is”ImageView
. 🎉
Let’s start simple — the public interface (our contract for the SDK and client application):
Its implementation (to be obfuscated):
And a simple (unintelligent) builder that the client application builds from:
This simple example gets much more complicated the more features (functions on our builder) that we start including. For example — including a function on the builder to change the base URL that our core network layer of the SDK communicates through (e.g. a local mock server on device for stubbing happy and sad paths). That’s something we don’t want getting shipped in released publications of the library.
Why Not Just Use Debug and Release Build Variants?
You could do that. But, what if you wanted to enable the Debug features of the UserProfileSdk
for any product flavor? The idea is that the Dev builder (its artifact) is never released to consumers, so who cares if the Prod builder is built with availability for both release
and debug
build variants?
If the SDK is built with debug
build variant, then only a debug
app build variant would compile. And vice versa — an SDK built with release
build variant would only compile with an app with a release
build variant.
The Android SDK team that I work with at Capital One implemented a strategy for securely separating Development and Production features from one another, but making the SDK available for consumption in any application’s build variant. Shout out to Mayank Mehta — check out his article on the Coordinator / Navigator pattern inspired by Hannes Dorfmann’s pattern.
This solution was developed for our Capital One Android SDK use case, but it’s a pattern implemented by other common libraries out there — for instance, older versions of Square’s LeakCanary memory leak detection library and the powerful HTTP traffic inspector library Chuck!
LeakCanary
Chuck
Modular SDK Builder Design
The module structure looks like this, but the user-profile-sdk-builder-dev
artifact is never meant to be published (or consumed by external client applications).
Now, we can expose many more public methods on our Dev builder that are not available with the Prod builder. For example, a function to change the base URL to point at a mock server hosted on device for happy / sad path stubbing.
And the Prod builder remains simple enough, excluding all of our debug feature functions (except for appContext
). This builder is locked down and more secure because we’ve hard-coded all of our modifiable parameters from the other Dev builder. This is exactly what we would expect from a Production-level SDK builder and it still returns the same UserProfileSdk
implementation!
And voilà, none of our awesome debug tooling features are protected behind some isDebuggable
code-switch that’s dependent on a BuildConfig
value. 🎉
Demoing the Modular SDK Builders via a Demo Application
Let’s use a demo application named Hello to leverage the modular Dev and Prod SDK builders to integrate our library with.
We can achieve this with product flavors to create new build types in order to test our Debug and Release features in a secure way. These are added to the productFlavors
block in the build configuration of the sample application. We’ll create a new flavor dimension called CONFIGURABLE
.
The Dev build type will consume our Dev SDK builder and the Prod build type will consume our Prod SDK builder in the client application’s build.gradle
dependency block. And now, we can have one (or more) builds that we use for debugging/demoing all of our SDK tooling features, and another build for testing for release.
And finally, the sample application Hello source folder structure becomes the following:
If you are more interested in how this demo application is built, you can check it out on my public GitHub repo. The project is built with Android lifecycle architecture components and an MVI architecture that uses Kotlin coroutines to observe streams of data in a redux pattern. The networking layer is a Retrofit implementation wrapped in Kotlin coroutines. And the UI screen navigation is designed with the Coordinator pattern so that I have the ability to expand on the SDK/application in the future with scalability.
The Application
class is responsible for initializing a manual and library-less dependency injection (DI) object graph and storing it at the app-level. Shout out to Zak Taccardi for this innovative approach!
Anything with reference to the Application
has reference to the object graph and any common objects to the sample application are stored on the base Graph
including the ConfigurableGraph
s build objects relative to each product flavor. For instance, the UserProfileSdkImpl
with our Debug features will be built with the Dev builder in the dev
product flavor’s ConfigurableGraph
.
Here’s where it gets even more interesting — product flavor dimensions allow us to build similar classes that add extension functions to the Application
class and add different functionality to the CONFIGURABLE
flavor dimension.
The ConfigurableDimension.kt
in the dev
flavor builds the Dev flavored DI object graph, which imports the Dev SDK builder and not the Prod SDK builder. This is an elegant way to add different SDK tooling features, without sacrificing security.
Here we can see all of our debug tooling functions are public and accessible on the Dev version of the configurable DI graph. Now, we can inject whatever we like (that conform to their interfaces) into the UserProfileSdk
.
The Prod version of the configurable DI graph will have no concept of a base URL, or logger, or region, etc., since we lock them to what we want in the Prod version of the SDK builder!
Ultimately, this leads us to our common Graph
which is dependent on any of our ConfigurableGraph
objects. They provide an implementation of our UserProfileSdk
.
Let’s take a step back and try to fully understand what is actually happening again here. By leveraging modular SDK builders (i.e. Dev and Prod SDK builders), we can securely separate Debug and Prod features from shipping in unrelated release versions. All of our SDK tooling is hidden away from any consuming client application.
We do not want Debug code to ship in Production code and this elegantly avoids that. No more isDebuggable
code-switches to deal with!
Your sample application (for Product / Sales demoing and testing purposes) can build from each SDK builder type based on dev
, mock
, and prod
flavor dimensions to showcase each build type. Now, your Product Owners and other stakeholders can happily continue using the debug features that they’re accustomed to for UAT and sales purposes.
And you can stay happy that your SDK is released in a protected way. 😄
Additional References
Check out this talk at Droidcon Boston 2017 from another Capital One co-worker (and GDE) Sam Edwards focused on building debug features for your application! Also, this Droidcon NYC 2015 talk about Debug Builds and their benefits to your development experience.
Full (and more imaginative) code on GitHub.
Connect with me on LinkedIn or follow me on Twitter!
DISCLOSURE STATEMENT: © 2019 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.