Share
Awesome Android SDK Design
https://medium.com/capital-one-tech/awesome-android-sdk-design-fef427604546
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?
jacksoncheek
Transcript
118 slides
-
@jacksoncheek Awesome Android SDK Design And Keeping Your Product Owners
Happy With Debug Features! #AndroidSummit -
@jacksoncheek 1. Application debug features 2. What about debug features
for SDKs? Outline -
@jacksoncheek 1. Application debug features 2. What about debug features
for SDKs? 3. A wild scenario appears! Outline -
@jacksoncheek 1. Application debug features 2. What about debug features
for SDKs? 3. A wild scenario appears! 4. Leveraging modular SDK builders Outline -
@jacksoncheek 1. Application debug features 2. What about debug features
for SDKs? 3. A wild scenario appears! 4. Leveraging modular SDK builders 5. Demo application that integrates our SDK Outline -
@jacksoncheek • Easy testing and debugging • Speed up dev
time Application Debug Features -
@jacksoncheek • Easy testing and debugging • Speed up dev
time • Helpful for User Acceptance Testing (UAT) Application Debug Features -
@jacksoncheek • Easy testing and debugging • Speed up dev
time • Helpful for User Acceptance Testing (UAT) • Demoing for Product Owners and Sales Teams Application Debug Features -
@jacksoncheek • Easy testing and debugging • Speed up dev
time • Helpful for User Acceptance Testing (UAT) • Demoing for Product Owners and Sales Teams • Do NOT ship to Production Application Debug Features -
@jacksoncheek • Custom logger injections Examples Log.d(TAG, "Boring log message
with a static call to the Android logger :(“) vs. // Android logger in Debug, no-op logger in Release, System logger in Test logger.log("Awesome log message :)") -
@jacksoncheek • Mock web server https://github.com/square/okhttp/tree/master/mockwebserver Examples private val
mockWebServer: MockWebServer = MockWebServer().apply { setDispatcher(mockDispatcher) mockScope.launch { start(8080) } } -
@jacksoncheek • Stub happy paths Examples class MockDispatcher : Dispatcher()
{ override fun dispatch(request: RecordedRequest): MockResponse { val body = """ { "presenter": "Jackson Cheek", "twitter": "@jacksoncheek" } """.trimIndent().toByteArray() return MockResponse().apply { setResponseCode(200) setBody(Buffer().readFrom(body.inputStream())) } } } -
@jacksoncheek • Stub sad paths Examples class MockDispatcher : Dispatcher()
{ override fun dispatch(request: RecordedRequest): MockResponse { val body = """ { "message": "Login Required" } """.trimIndent().toByteArray() return MockResponse().apply { setResponseCode(401) setBody(Buffer().readFrom(body.inputStream())) } } } -
@jacksoncheek Examples UI Elements fr_CA French es_US Spanish en_US English
• Localization changes -
@jacksoncheek • Deep linking Examples App Launch Skip FTUX Skip
Login Skip Registration -
@jacksoncheek • Simulating push notifications • Inspecting analytics • Bug
reporting • … And More! -
@jacksoncheek Other Devs You Product Owners QA Sales Backend Devs
Other Stakeholders Executives -
@jacksoncheek Other Devs You Product Owners QA Sales Backend Devs
Other Stakeholders Executives -
@jacksoncheek Testing / Debug code should never be included in
shipped Production code But… -
@jacksoncheek // Debug Dependencies dependencies { debugImplementation project(":example-debug-module") } //
Release Dependencies dependencies { releaseImplementation project(“:example-no-op-module") } Application Breakdown -
@jacksoncheek // Debug Dependencies dependencies { debugImplementation project(":example-debug-module") } //
Release Dependencies dependencies { releaseImplementation project(“:example-no-op-module") } Application Breakdown -
@jacksoncheek How do you design a library with all of
your awesome debug tooling? But, What About SDKs? -
@jacksoncheek How do you design a library with all of
your awesome debug tooling? But, What About SDKs? How do you avoid the riskiness of shipping them “turned off” in Production code? -
@jacksoncheek • GitHub: https://github.com/jacksoncheek/UserProfileSdk • Link to slides will be
available through Twitter Time for Some Code -
@jacksoncheek Common Approach /** Builder code to return your CustomSdk
implementation */ class CustomSdkBuilder { /** Builds the CustomSdkImpl */ fun build(): CustomSdk { return CustomSdkImpl() } } -
@jacksoncheek Common Approach /** Builder code to return your CustomSdk
implementation */ class CustomSdkBuilder { /** Builds the CustomSdkImpl */ fun build(): CustomSdk { return CustomSdkImpl( logger = logger ) } fun logger(logger: Logger) = apply { this.logger = logger } } -
@jacksoncheek Common Approach /** Builder code to return your CustomSdk
implementation */ class CustomSdkBuilder { /** Builds the CustomSdkImpl */ fun build(): CustomSdk { return CustomSdkImpl( logger = logger ) } fun logger(logger: Logger) = apply { this.logger = logger } } -
@jacksoncheek Common Approach /** Builder code to return your CustomSdk
implementation */ class CustomSdkBuilder { /** Builds the CustomSdkImpl */ fun build(): CustomSdk { return CustomSdkImpl( logger = logger // will be NoOpLogger in Release builds ) } fun logger(logger: Logger) = apply { if (isDebuggable) { this.logger = logger } else { this.logger = NoOpLogger() } } // BAD!!! private val isDebuggable: Boolean = if (BuildConfig.DEBUG) true else false } -
@jacksoncheek Common Approach /** Builder code to return your CustomSdk
implementation */ class CustomSdkBuilder { /** Builds the CustomSdkImpl */ fun build(): CustomSdk { return CustomSdkImpl( logger = logger // will be NoOpLogger in Release builds ) } fun logger(logger: Logger) = apply { if (isDebuggable) { this.logger = logger } else { this.logger = NoOpLogger() } } // BAD!!! private val isDebuggable: Boolean = if (BuildConfig.DEBUG) true else false } -
@jacksoncheek “The code paths aren't being executed in Release. Just
let ProGuard’s code optimization strip it out…” Why Is This Risky? -
@jacksoncheek “The code paths aren't being executed in Release. Just
let ProGuard’s code optimization strip it out…” Why Is This Risky? How confident are you in your release pipeline build steps? -
@jacksoncheek Modular Builders Separate your Debug features and your Production
features by writing two SDK builders (Dev and Prod). -
@jacksoncheek Modular Builders 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. -
@jacksoncheek Modular Builders 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. -
@jacksoncheek • Modify network endpoints • HTTP traffic interceptors •
Custom logger injections • Mock web server • Stub happy and sad paths • Localization changes • Deep linking • Simulating push notifications • Alternate app resources • Inspecting analytics • Bug reporting • Plus more! ? ? ? ? ? SDK Debug Tooling -
@jacksoncheek • Modify network endpoints • HTTP traffic interceptors •
Custom logger injections • Mock web server • Stub happy and sad paths • Localization changes • Deep linking • Simulating push notifications • Alternate app resources • Inspecting analytics • Bug reporting • Plus more! AW ESOME SDK Debug Tooling -
@jacksoncheek Build an Android SDK that fetches a user’s profile
A Wild Scenario Appears! -
@jacksoncheek Build an Android SDK that fetches a user’s profile
A Wild Scenario Appears! Acceptance Criteria: • Demo application to showcase the SDK -
@jacksoncheek Build an Android SDK that fetches a user’s profile
A Wild Scenario Appears! Acceptance Criteria: • Demo application to showcase the SDK • Include a “locked” down build variant with no debug tooling -
@jacksoncheek Build an Android SDK that fetches a user’s profile
A Wild Scenario Appears! Acceptance Criteria: • Demo application to showcase the SDK • Include a “locked” down build variant with no debug tooling • Include an “unlocked” build variant with awesome debug features -
@jacksoncheek Hello, My Name Is 1. Client application Hello launches.
2. User clicks “Get Profile” button and the ViewModel launches our UserProfileSdk. 3. The SDK begins a new Activity and starts a ViewModel that performs our data fetching. 4. A user profile is retrieved from the API and is rendered on screen. 5. User clicks “Got It!” button, which exits the SDK and returns a Result object to Hello via a callback. 6. The client application renders the Result back to the user on the “Hello, My Name Is” ImageView. -
@jacksoncheek /** Public interface. */ interface UserProfileSdk { /** Begins
the UserProfileSdk flow. * * @param callback invoked asynchronously after completion. */ fun startFlow( callback: ResultCallback ) } Starting Simple (Contract) -
@jacksoncheek /** Public interface. */ interface UserProfileSdk { /** Begins
the UserProfileSdk flow. * * @param callback invoked asynchronously after completion. */ fun startFlow( callback: ResultCallback ) } typealias ResultCallback = (Result) -> Unit Starting Simple (Contract) -
@jacksoncheek internal class UserProfileSdkImpl(private val params: Params) : UserProfileSdk {
// ... override fun startFlow( callback: ResultCallback ) { } } SDK Implementation -
@jacksoncheek internal class UserProfileSdkImpl(private val params: Params) : UserProfileSdk {
// ... override fun startFlow( callback: ResultCallback ) { sdkSession.launch { uiSdk.startFlow(callback) } } } SDK Implementation -
@jacksoncheek class UserProfileSdkBuilder { private lateinit var params: Params fun
params(params: Params) = apply { this.params = params } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { requireNotNull(params) { "`params` cannot be null" } return UserProfileSdkImpl( params = params ) } } SDK Builder -
@jacksoncheek class UserProfileSdkBuilder { private lateinit var params: Params fun
params(params: Params) = apply { this.params = params } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { requireNotNull(params) { "`params` cannot be null" } return UserProfileSdkImpl( params = params ) } } SDK Builder -
@jacksoncheek class UserProfileSdkBuilder { private lateinit var params: Params fun
params(params: Params) = apply { this.params = params } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { requireNotNull(params) { "`params` cannot be null" } return UserProfileSdkImpl( params = params ) } } SDK Builder -
@jacksoncheek class UserProfileSdkBuilder { private lateinit var params: Params fun
params(params: Params) = apply { this.params = params } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { requireNotNull(params) { "`params` cannot be null" } return UserProfileSdkImpl( params = params ) } } SDK Builder -
@jacksoncheek class UserProfileSdkBuilder { private lateinit var params: Params fun
params(params: Params) = apply { this.params = params } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { requireNotNull(params) { "`params` cannot be null" } return UserProfileSdkImpl( params = params ) } } SDK Builder -
@jacksoncheek class UserProfileSdkBuilder { fun baseUrl(baseUrl: BaseUrl) = apply {
this.baseUrl = baseUrl } // ... } Let’s Start Adding Features -
@jacksoncheek class UserProfileSdkBuilder { fun baseUrl(baseUrl: BaseUrl) = apply {
this.baseUrl = baseUrl } fun region(region: Region) = apply { this.region = region } // ... } Let’s Start Adding Features -
@jacksoncheek class UserProfileSdkBuilder { fun baseUrl(baseUrl: BaseUrl) = apply {
this.baseUrl = baseUrl } fun region(region: Region) = apply { this.region = region } fun okHttpBuilder(okHttpBuilder: OkHttpClient.Builder) = apply { this.okHttpBuilder = okHttpBuilder } // ... } Let’s Start Adding Features -
@jacksoncheek class UserProfileSdkBuilder { fun baseUrl(baseUrl: BaseUrl) = apply {
this.baseUrl = baseUrl } fun region(region: Region) = apply { this.region = region } fun okHttpBuilder(okHttpBuilder: OkHttpClient.Builder) = apply { this.okHttpBuilder = okHttpBuilder } fun logger(logger: Logger) = apply { this.logger = logger } // ... } Let’s Start Adding Features -
@jacksoncheek Modular SDK Builder Design Demo Application Prod SDK Builder
Dev SDK Builder Dev Flavors Injecting Debug Features Prod Flavors Locked Down Release Features -
@jacksoncheek Modular SDK Builder Design Prod SDK Builder Dev SDK
Builder UserProfileSdk Common SDK Impl Demo Application -
@jacksoncheek Modular SDK Builder Design Core Library UI Library UserProfileSdk
Base Libraries Prod SDK Builder Dev SDK Builder Demo Application -
@jacksoncheek Modular SDK Builder Design Demo Application Prod SDK Builder
Dev SDK Builder Core Library UI Library UserProfileSdk -
@jacksoncheek // LeakCanary dependencies { debugImplementation "com.squareup.leakcanary:leakcanary-android:1.3.1" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:1.3.1" }
// Chuck dependencies { debugCompile "com.readystatesoftware.chuck:library:1.1.0" releaseCompile "com.readystatesoftware.chuck:library-no-op:1.1.0" } Existing Examples -
@jacksoncheek // LeakCanary dependencies { debugImplementation "com.squareup.leakcanary:leakcanary-android:1.3.1" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:1.3.1" }
// Chuck dependencies { debugCompile "com.readystatesoftware.chuck:library:1.1.0" releaseCompile "com.readystatesoftware.chuck:library-no-op:1.1.0" } Existing Examples -
@jacksoncheek // LeakCanary dependencies { debugImplementation "com.squareup.leakcanary:leakcanary-android:1.3.1" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:1.3.1" }
// Chuck dependencies { debugCompile "com.readystatesoftware.chuck:library:1.1.0" releaseCompile "com.readystatesoftware.chuck:library-no-op:1.1.0" } Existing Examples -
@jacksoncheek /** Prod SDK Builder */ class UserProfileSdkBuilder { fun
appContext(appContext: Context) = apply { this.appContext = appContext } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { // Return the UserProfileSdkImpl } } Prod SDK Builder -
@jacksoncheek /** Prod SDK Builder */ class UserProfileSdkBuilder { fun
appContext(appContext: Context) = apply { this.appContext = appContext } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { // Return the UserProfileSdkImpl } } Prod SDK Builder -
@jacksoncheek /** Dev SDK Builder */ class UserProfileSdkBuilder { fun
appContext(appContext: Context) = apply { this.appContext = appContext } fun baseUrl(baseUrl: BaseUrl) = apply { this.baseUrl = baseUrl } fun region(region: Region) = apply { this.region = region } fun okHttpBuilder(okHttpBuilder: OkHttpClient.Builder) = apply { this.okHttpBuilder = okHttpBuilder } fun logger(logger: Logger) = apply { this.logger = logger } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { // Return the UserProfileSdkImpl } } Dev SDK Builder -
@jacksoncheek /** Dev SDK Builder */ class UserProfileSdkBuilder { fun
appContext(appContext: Context) = apply { this.appContext = appContext } fun baseUrl(baseUrl: BaseUrl) = apply { this.baseUrl = baseUrl } fun region(region: Region) = apply { this.region = region } fun okHttpBuilder(okHttpBuilder: OkHttpClient.Builder) = apply { this.okHttpBuilder = okHttpBuilder } fun logger(logger: Logger) = apply { this.logger = logger } /** Builds the UserProfileSdkImpl. */ fun build(): UserProfileSdk { // Return the UserProfileSdkImpl } } Dev SDK Builder -
@jacksoncheek Voilà None of our AWESOME debug tooling features are
hidden behind isDebuggable code-switches. -
@jacksoncheek • Framework-less dependency injection • SDK instance scoped to
the MainApplication class Demo App + SDK Integration -
@jacksoncheek • Framework-less dependency injection • SDK instance scoped to
the MainApplication class • Product flavors for separating debug / release dependencies Demo App + SDK Integration -
@jacksoncheek Injecting the SDK class MainApplication : Application() { val
sdk: UserProfileSdk by lazy { UserProfileSdkBuilder() .appContext(this) .build() } override fun onCreate() { super.onCreate() // Initialize the UserProfileSdk before onCreate() finishes sdk } } -
@jacksoncheek Injecting the SDK class MainApplication : Application() { val
sdk: UserProfileSdk by lazy { UserProfileSdkBuilder() .appContext(this) .build() } override fun onCreate() { super.onCreate() // Initialize the UserProfileSdk before onCreate() finishes sdk } } Which Builder? -
@jacksoncheek Launch the SDK class MainActivity : AppCompatActivity() { val
app get() = application as MainApplication val sdk: UserProfileSdk get() = app.sdk override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.sample_activity_main) sdk.startFlow { result -> println("UserProfileSdk Result: $result") } } } -
@jacksoncheek Flavor Dimensions android { flavorDimensions "CONFIGURABLE" productFlavors { dev
{ dimension "CONFIGURABLE" applicationIdSuffix ".dev" } mock { dimension "CONFIGURABLE" applicationIdSuffix ".mock" } prod { dimension "CONFIGURABLE" } } } -
@jacksoncheek Flavors Dependencies // Dev Dependencies dependencies { devImplementation project(":user-profile-sdk-builder-dev")
} // Mock Dependencies dependencies { mockImplementation project(":user-profile-sdk-builder-dev") } // Prod Dependencies dependencies { prodImplementation project(":user-profile-sdk-builder-prod") } // Common Dependencies dependencies { // Everything else } -
@jacksoncheek Flavors Dependencies // Dev Dependencies dependencies { devImplementation project(":user-profile-sdk-builder-dev")
} // Mock Dependencies dependencies { mockImplementation project(":user-profile-sdk-builder-dev") } // Prod Dependencies dependencies { prodImplementation project(":user-profile-sdk-builder-prod") } // Common Dependencies dependencies { // Everything else } -
@jacksoncheek DI Object Graph (Mock) /** A graph for bindings
specific to the MOCK flavor. */ interface ConfigurableGraph { val baseUrl: BaseUrl val okHttpBuilder: OkHttpClient.Builder val userProfileSdk: UserProfileSdk } -
@jacksoncheek DI Object Graph (Mock) /** A graph for bindings
specific to the MOCK flavor. */ interface ConfigurableGraph { val baseUrl: BaseUrl val okHttpBuilder: OkHttpClient.Builder val userProfileSdk: UserProfileSdk } -
@jacksoncheek DI Object Graph (Mock) /** A graph for bindings
specific to the MOCK flavor. */ interface ConfigurableGraph { val baseUrl: BaseUrl val okHttpBuilder: OkHttpClient.Builder val userProfileSdk: UserProfileSdk } -
@jacksoncheek DI Object Graph (Mock) /** A graph for bindings
specific to the MOCK flavor. */ interface ConfigurableGraph { val baseUrl: BaseUrl val okHttpBuilder: OkHttpClient.Builder val userProfileSdk: UserProfileSdk } -
@jacksoncheek DI Object Graph (Mock) import com.jacksoncheek.userprofile.builder.dev.UserProfileSdkBuilder /** A graph
for bindings specific to the MOCK flavor. */ class ConfigurableGraphImpl(val appContext: Context) : ConfigurableGraph { override val baseUrl by lazy { "http://localhost:8080" } override val okHttpBuilder by lazy { OkHttpClient.Builder() .addInterceptor(ChuckInterceptor(appContext).showNotification(true)) } override val userProfileSdk by lazy { UserProfileSdkBuilder() .appContext(appContext) .baseUrl(baseUrl) .okHttpBuilder(okHttpBuilder) .build() } } -
@jacksoncheek DI Object Graph (Mock) import com.jacksoncheek.userprofile.builder.dev.UserProfileSdkBuilder /** A graph
for bindings specific to the MOCK flavor. */ class ConfigurableGraphImpl(val appContext: Context) : ConfigurableGraph { override val baseUrl by lazy { "http://localhost:8080" } override val okHttpBuilder by lazy { OkHttpClient.Builder() .addInterceptor(ChuckInterceptor(appContext).showNotification(true)) } override val userProfileSdk by lazy { UserProfileSdkBuilder() .appContext(appContext) .baseUrl(baseUrl) .okHttpBuilder(okHttpBuilder) .build() } } -
@jacksoncheek DI Object Graph (Mock) import com.jacksoncheek.userprofile.builder.dev.UserProfileSdkBuilder /** A graph
for bindings specific to the MOCK flavor. */ class ConfigurableGraphImpl(val appContext: Context) : ConfigurableGraph { override val baseUrl by lazy { "http://localhost:8080" } override val okHttpBuilder by lazy { OkHttpClient.Builder() .addInterceptor(ChuckInterceptor(appContext).showNotification(true)) } override val userProfileSdk by lazy { UserProfileSdkBuilder() .appContext(appContext) .baseUrl(baseUrl) .okHttpBuilder(okHttpBuilder) .build() } } Dev SDK Builder -
@jacksoncheek DI Object Graph (Mock) import com.jacksoncheek.userprofile.builder.dev.UserProfileSdkBuilder /** A graph
for bindings specific to the MOCK flavor. */ class ConfigurableGraphImpl(val appContext: Context) : ConfigurableGraph { override val baseUrl by lazy { "http://localhost:8080" } override val okHttpBuilder by lazy { ... } override val userProfileSdk by lazy { ... } companion object { fun create(appContext: Context): ConfigurableGraph { return ConfigurableGraph.Impl(appContext) } } } -
@jacksoncheek DI Object Graph (Prod) import com.jacksoncheek.userprofile.builder.prod.UserProfileSdkBuilder /** A graph
for bindings specific to the PROD flavor. */ interface ConfigurableGraph { val userProfileSdk: UserProfileSdk // And NO debug tooling features } class ConfigurableGraphImpl(val appContext: Context) : ConfigurableGraph { override val userProfileSdk by lazy { UserProfileSdkBuilder() .appContext(appContext) .build() } } -
@jacksoncheek DI Object Graph (Prod) import com.jacksoncheek.userprofile.builder.prod.UserProfileSdkBuilder /** A graph
for bindings specific to the PROD flavor. */ interface ConfigurableGraph { val userProfileSdk: UserProfileSdk // And NO debug tooling features } class ConfigurableGraphImpl(val appContext: Context) : ConfigurableGraph { override val userProfileSdk by lazy { UserProfileSdkBuilder() .appContext(appContext) .build() } } Prod SDK Builder -
@jacksoncheek • Extension functions for the MainApplication class Flavor Dimensions
/** Add different functionality to the `CONFIGURABLE` flavor dimension. */ fun MainApplication.onCreateConfigurableFlavorDimension(): ConfigurableGraph { // Initialize and return the MOCK DI object graph return ConfigurableGraph.create(this) } -
@jacksoncheek • Extension functions for the MainApplication class Flavor Dimensions
/** Add different functionality to the `CONFIGURABLE` flavor dimension. */ fun MainApplication.onCreateConfigurableFlavorDimension(): ConfigurableGraph { // Initialize and return the MOCK DI object graph return ConfigurableGraph.create(this) } -
@jacksoncheek Flavors Source Structure \--- :user-profile-sdk-sample-app (demo application) +--- src
| +--- dev | | \--- java | | \--- com.jacksoncheek.userprofile.sample | | +--- ConfigurableDimension.kt | | \--- ConfigurableGraph.kt | +--- main | | \--- java | | \--- com.jacksoncheek.userprofile.sample | | +--- Graph.kt | | +--- MainActivity.kt | | \--- MainApplication.kt | +--- mock | | \--- java | | \--- com.jacksoncheek.userprofile.sample | | +--- ConfigurableDimension.kt | | \--- ConfigurableGraph.kt | +--- prod | \--- java | \--- com.jacksoncheek.userprofile.sample | +--- ConfigurableDimension.kt | \--- ConfigurableGraph.kt \--- build.gradle -
@jacksoncheek Flavors Source Structure \--- :user-profile-sdk-sample-app (demo application) +--- src
| +--- dev | | \--- java | | \--- com.jacksoncheek.userprofile.sample | | +--- ConfigurableDimension.kt | | \--- ConfigurableGraph.kt | +--- main | | \--- java | | \--- com.jacksoncheek.userprofile.sample | | +--- Graph.kt | | +--- MainActivity.kt | | \--- MainApplication.kt | +--- mock | | \--- java | | \--- com.jacksoncheek.userprofile.sample | | +--- ConfigurableDimension.kt | | \--- ConfigurableGraph.kt | +--- prod | \--- java | \--- com.jacksoncheek.userprofile.sample | +--- ConfigurableDimension.kt | \--- ConfigurableGraph.kt \--- build.gradle -
@jacksoncheek Injecting the SDK via Graph class MainApplication : Application()
{ val configurableGraph: ConfigurableGraph by lazy { onCreateConfigurableFlavorDimension() } override fun onCreate() { super.onCreate() // Initialize the configurable graph before onCreate() finishes configurableGraph } } -
@jacksoncheek Injecting the SDK via Graph class MainApplication : Application()
{ val configurableGraph: ConfigurableGraph by lazy { onCreateConfigurableFlavorDimension() } override fun onCreate() { super.onCreate() // Initialize the configurable graph before onCreate() finishes configurableGraph } } -
@jacksoncheek Launch the SDK via Graph class MainActivity : AppCompatActivity()
{ val app get() = application as MainApplication val configurableGraph: ConfigurableGraph get() = app.configurableGraph override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.sample_activity_main) configurableGraph.userProfileSdk.startFlow { result -> println("UserProfileSdk Result: $result") } } } -
@jacksoncheek Launch the SDK via Graph class MainActivity : AppCompatActivity()
{ val app get() = application as MainApplication val configurableGraph: ConfigurableGraph get() = app.configurableGraph override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.sample_activity_main) configurableGraph.userProfileSdk.startFlow { result -> println("UserProfileSdk Result: $result") } } } -
@jacksoncheek • Leveraging modular SDK builders to securely separate debug
features from release builds. Recap -
@jacksoncheek • Leveraging modular SDK builders to securely separate debug
features from release builds. • Dev SDK builder artifact is NOT published. Recap -
@jacksoncheek • Leveraging modular SDK builders to securely separate debug
features from release builds. • Dev SDK builder artifact is NOT published. • Debug / testing code is not shipped in production code. Recap -
@jacksoncheek • Leveraging modular SDK builders to securely separate debug
features from release builds. • Dev SDK builder artifact is NOT published. • Debug / testing code is not shipped in production code. • Product Owners and other stakeholders can happily continue using the debug features they’re accustomed to . Recap -
@jacksoncheek • Leveraging modular SDK builders to securely separate debug
features from release builds. • Dev SDK builder artifact is NOT published. • Debug / testing code is not shipped in production code. • Product Owners and other stakeholders can happily continue using the debug features they’re accustomed to . • Devs can stay happy that their SDK design is protected! Recap -
@jacksoncheek Awesome Android SDK Design And Keeping Your Product Owners
Happy With Debug Features! #AndroidSummit Questions? Jackson Cheek @jacksoncheek