Awesome Android SDK Design

Leveraging Modular SDK Builders for Better Library Design and Keeping Your Product Owners Happy

Jackson Cheek
Capital One Tech
9 min readJun 24, 2019

--

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:

  1. Client application Hello launches.
  2. User clicks Get Profile button and the ViewModel launches our UserProfileSdk.
  3. The SDK accepts an application Context parameter, so it begins a new Activity and starts a ViewModel that performs our data fetching.
  4. 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.
  5. User clicks Got It! button, which exits our SDK and returns a Result object to the client application Hello via a callback.
  6. The client application renders the Result back to the user on the “Hello, My Name Is” ImageView. 🎉
Example Flow of the UserProfileSdk Demo Application

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).

Module Structure for the UserProfileSdk

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.

Dependency Blocks for the Product Flavors

And finally, the sample application Hello source folder structure becomes the following:

Product Flavor Directory Structure with ConfigurableGraph and Special Dimension Extension Functions

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 ConfigurableGraphs 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.

--

--