Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The definitive guide to Android library development

Jeroen Mols
October 20, 2021

The definitive guide to Android library development

Video: https://www.droidcon.com/2021/11/10/the-definitive-guide-to-android-library-development/

Android library/SDK development unfortunately is harder than it should be. This is caused by inferior tooling (Why do you need fat AAR?), missing documentation (What causes transitive dependency conflicts?), and different architecture considerations (Should you modularize your SDK?).

Having switched to full-time Android SDK development two years ago, I'm summarizing my lessons learned into a talk I wish would have existed when I got started.

⚠️ This talk is aimed at anyone curious to level up their knowledge of the Android ecosystem, not just at beginning/seasoned SDK developers!

---------------------------

Further reads:
- Maintaining compatibility in Kotlin libraries by Márton Braun
- The hidden Kotlin gem: Deprecations with ReplaceWith by Marc Reichelt
- Publishing libraries to Maven Central in 2021 by Marton Braun
- Java interoperability policy for major version updates by Jake Wharton
- Public API challenges in Kotlin by Jake Wharton
- How to publish and distribute your Android library by Marco Gomiero
- What is a diamond dependency conflict by Google
- All about Opt-in annotationsby Márton Braun

Jeroen Mols

October 20, 2021
Tweet

More Decks by Jeroen Mols

Other Decks in Programming

Transcript

  1. @MOLSJEROEN APK •Compiled code •Processed resources •Android manifest •Compiled transitive

    dependencies •Can be run on Android device •Signed JAR •Compiled code •Cannot be run on Android device •Unsigned
  2. @MOLSJEROEN APK •Compiled code •Processed resources •Android manifest •Compiled transitive

    dependencies •Can be run on Android device •Signed AAR •Compiled code •Processed resources •Android manifest •Cannot be run on Android device •Unsigned
  3. @MOLSJEROEN DISTRIBUTE - STEPS 1.Build a release (see before) 2.Generate

    and publish PGP key 3.Sign build 4.Upload to Maven central => Tutorial by Márton Braun: 
 https://getstream.io/blog/publishing-libraries-to-mavencentral-2021/
  4. @MOLSJEROEN POM - XML FILE <project xmlns="http://maven.apache.org/POM/4.0.0" > <groupId>com.jeroenmols</groupId> <artifactId>library</artifactId>

    <version>1.0.0</version> <dependencies> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp3</artifactId> <version>4.9.0</version> </dependency> </dependencies> </project>
  5. @MOLSJEROEN POM - XML FILE <project xmlns="http://maven.apache.org/POM/4.0.0" > <groupId>com.jeroenmols</groupId> <artifactId>library</artifactId>

    <version>1.0.0</version> <dependencies > <dependency > <groupId>com.squareup.okhttp3</groupId > <artifactId>okhttp3</artifactId > <version>4.9.0</version > </dependency > </dependencies > </project>
  6. @MOLSJEROEN POM - XML FILE <project xmlns="http://maven.apache.org/POM/4.0.0" > <groupId>com.jeroenmols</groupId >

    <artifactId>library</artifactId > <version>1.0.0</version > <dependencies> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp3</artifactId> <version>4.9.0</version> </dependency> </dependencies > </project>
  7. @MOLSJEROEN APP MODULARISATION APP FEATURE 2 FEATURE 3 FEATURE 4

    FEATURE 5 FEATURE 6 CORE 4 CORE 4 CORE 2 CORE 3 FEATURE 1 CORE 1
  8. @MOLSJEROEN TEST SETUP // app Library().initialize() // library class Library

    { fun initialize() = Database().initialize() } // database class Database { fun initialize() = System.out.println(“database ready") }
  9. @MOLSJEROEN LIBRARY SUBMODULE TEST ./gradlew :library:assembleReleas e // App build.gradl

    e dependencies { // implementation project(':library') implementation fi les('../library/build/outputs/aar/library-release.aar') ... }
  10. @MOLSJEROEN 1. RELEASE SUBMODULES TO MAVEN // Library build.gradl e

    dependencies { implementation "com.jeroenmols:database:1.0.0" implementation "com.jeroenmols:ui-components:1.0.0" }
  11. @MOLSJEROEN 2. FAT AAR apply plugin: 'com.kezong.fat-aar' ... dependencies {

    embed project(path: ':modules:database', con fi guration:'default') embed project(path: ':modules:ui-components', con fi guration:'default') }
  12. @MOLSJEROEN KOTLIN VISIBILITY MODIFIERS • private: visible inside this class

    only • protected: same as private + visible in subclasses too • internal: visible to all classes inside this module • public: visible to all classes
  13. @MOLSJEROEN MODULARIZATION RECOMMENDATION • Modularizing SDKs is tedious • Single

    module for small and mid sized SDKs • Spin off reusable maven artefacts for larger SDKs • Reduce public API surface • Tooling is lacking
  14. @MOLSJEROEN TRANSITIVE DEPENDENCY CONFLICTS PROJECT MAVEN TRANSITIVE DEPENDENCY 1 CUSTOMER

    APP ANOTHER LIBRARY LIBRARY TRANSITIVE DEPENDENCY 2 TRANSITIVE DEPENDENCY 3
  15. @MOLSJEROEN CONFLICT RESOLUTION 1.Force dependency resolution in CustomerApp 2.Loosen dependency

    requirements in library 3.Remove transitive dependency from library
  16. @MOLSJEROEN 1. FORCE DEPENDENCY IN CUSTOMERAPP con fi gurations.all {

    resolutionStrategy { force 'com.squareup.okhttp3:okhttp:3.12.0' } }
  17. @MOLSJEROEN 1. FORCE DEPENDENCY IN CUSTOMERAPP • No library update

    required • Force untested combinations of dependencies • Burden for SDK customers
  18. @MOLSJEROEN CASE STUDY: BREAKING API CHANGES PROJECT MAVEN OKHTTP V2

    CUSTOMER APP ANOTHER LIBRARY LIBRARY OKHTTP V3
  19. @MOLSJEROEN INCOMPATIBLE TRANSITIVE DEPENDENCIES $ ./gradlew clean assembleDebug > Task

    :app:checkDebugDuplicateClasses FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':myproject:checkReleaseDuplicateClasses'. > 1 exception was raised by workers: java.lang.RuntimeException: Duplicate class com.google.protobuf.AbstractMessageLite found in modules protobuf-java-3.11.1.jar (com.google.protobuf:protobuf-java:3.11.1) and protobuf-javalite-3.11.0.jar (com.google.protobuf:protobuf-javalite:3.11.0) Duplicate class com.google.protobuf.AbstractMessageLite$Builder found in modules protobuf-java-3.11.1.jar (com.google.protobuf:protobuf-java:3.11.1) and protobuf-javalite-3.11.0.jar (com.google.protobuf:protobuf-javalite:3.11.0) Duplicate class com.google.protobuf.AbstractMessageLite$Builder$LimitedInputStream found in modules protobuf-java-3.11.1.jar (com.google.protobuf:protobuf-java:3.11.1) and protobuf-javalite-3.11.0.jar (com.google.protobuf:protobuf-javalite:3.11.0) ...
  20. @MOLSJEROEN INCOMPATIBLE TRANSITIVE DEPENDENCIES +--- com.google. fi rebase: fi rebase-perf:19.0.7

    | +--- com.google. fi rebase: fi rebase-con fi g:19.0.4 | | +--- com.google. fi rebase: fi rebase-abt:19.0.0 | | | \--- com.google.protobuf:protobuf-lite:3.0.1
  21. @MOLSJEROEN INCOMPATIBILITY RESOLUTION 1.Substitute dependency in CustomerApp 2.Remove dependency from

    transitive dependency of library 3.Remove transitive dependency from library
  22. @MOLSJEROEN 2. REMOVE FROM TRANSITIVE DEPENDENCY .. . <dependency> <groupId>pro.streem.pbandk</groupId

    > <artifactId>pbandk-runtime-jvm</artifactId> <version>0.9.0</version> <exclusions> <exclusion> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> </exclusion> </exclusions> </dependency>
  23. @MOLSJEROEN 2. REMOVE FROM TRANSITIVE DEPENDENCY .. . <dependency >

    <groupId>pro.streem.pbandk</groupId > <artifactId>pbandk-runtime-jvm</artifactId > <version>0.9.0</version > <exclusions> <exclusion> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> </exclusion> </exclusions > </dependency>
  24. @MOLSJEROEN 2. REMOVE FROM TRANSITIVE DEPENDENCY pom.withXml { def dependencies

    = asNode().appendNode('dependencies') con fi gurations.getByName(“releaseCompileClasspath").getResolvedCon fi guration( ) .getFirstLevelModuleDependencies().each { ... def dependency = dependencies.appendNode(‘dependency’ ) if (it.moduleName.contains("pbandk")) { def exclusions = dependency.appendNode(‘exclusions' ) def protobufExclusion = exclusions.appendNode('exclusion') protobufExclusion.appendNode('groupId', "com.google.protobuf") protobufExclusion.appendNode('artifactId', "protobuf-java") } } }
  25. @MOLSJEROEN 2. REMOVE FROM TRANSITIVE DEPENDENCY pom.withXml { def dependencies

    = asNode().appendNode('dependencies' ) con fi gurations.getByName(“releaseCompileClasspath").getResolvedCon fi guration( ) .getFirstLevelModuleDependencies().each { .. . def dependency = dependencies.appendNode(‘dependency’ ) if (it.moduleName.contains("pbandk")) { def exclusions = dependency.appendNode(‘exclusions' ) def protobufExclusion = exclusions.appendNode('exclusion') protobufExclusion.appendNode('groupId', "com.google.protobuf") protobufExclusion.appendNode('artifactId', "protobuf-java") } } }
  26. @MOLSJEROEN TRANSITIVE DEPENDENCY RECOMMENDATION 1. Minimize transitive dependencies 2. Use

    stable versions 3. Use versions without known vulnerabilities 4. Don’t use latest dependency versions 5. Prefer exact versions over ranges 6. Only use libraries that handle breaking changes => Put exceptions in release notes
  27. @MOLSJEROEN INVESTIGATE DEPENDENCY CONFLICTS ./gradlew :library:dependencie s $ ./gradlew --console

    plain :app:dependencies --con fi guration releaseRuntimeClasspath > Task :app:dependencies ------------------------------------------------------------ Project :app ------------------------------------------------------------ releaseRuntimeClasspath - Runtime classpath of compilation 'release' (target (androidJvm)). +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72 | \--- org.jetbrains:annotations:13.0 +--- androidx.core:core-ktx:1.3.2 | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.71 -> 1.3.72 (*) | +--- androidx.annotation:annotation:1.1.0 | \--- androidx.core:core:1.3.2 | \--- ... +--- androidx.appcompat:appcompat:1.2.0 | +--- androidx.annotation:annotation:1.1.0
  28. @MOLSJEROEN 2. LIBRARY RESOURCES - HIDE <!-- library/src/main/res/values/public.xml --> <?xml

    version="1.0" encoding="utf-8"?> <resources> <public name="library_name" type="string"/> </resources>
  29. @MOLSJEROEN 3. COMPATIBILITY: SOURCE class CustomerApp { fun useLibrary() =

    Library().initialize( ) } // Library v1 class Library { fun initialize() = Database().initialize() }
  30. @MOLSJEROEN 3. COMPATIBILITY: SOURCE class CustomerApp { fun useLibrary() =

    Library().initialize( ) } // Library v2 class Library { fun initialize(useCaching: Boolean) = Database().initialize() }
  31. @MOLSJEROEN 3. COMPATIBILITY: SOURCE class CustomerApp { fun useLibrary() =

    Library().initialize() } // Library v2 class Library { fun initialize(useCaching: Boolean) = Database().initialize() } no value passed for parameter useCaching
  32. @MOLSJEROEN 3. COMPATIBILITY: SOURCE class CustomerApp { fun useLibrary() =

    Library().initialize() } // Library v2 class Library { fun initialize(useCaching: Boolean = true) = Database().initialize() }
  33. @MOLSJEROEN 3. COMPATIBILITY: BINARY // Bytecode library v1 public fi

    nal class com/jeroenmols/library/Library { public fi nal static initialize() V } // Bytecode library v2 public fi nal class com/jeroenmols/library/Library { public fi nal static initialize(Z) V public static synthetic initialize$default(ZILjava/lang/Object;) V }
  34. @MOLSJEROEN 3. COMPATIBILITY: BINARY // Bytecode library v 1 public

    fi nal class com/jeroenmols/library/Library { public fi nal static initialize() V } // Bytecode library v 2 public fi nal class com/jeroenmols/library/Library { public fi nal static initialize(Z) V public static synthetic initialize$default(ZILjava/lang/Object;) V }
  35. @MOLSJEROEN 3. COMPATIBILITY: BINARY // Bytecode library v 1 public

    fi nal class com/jeroenmols/library/Library { public fi nal static initialize() V } // Bytecode library v 2 public fi nal class com/jeroenmols/library/Library { public fi nal static initialize(Z) V public static synthetic initialize$default(ZILjava/lang/Object;) V }
  36. @MOLSJEROEN 3. COMPATIBILITY: BINARY Maintaining Compatibility in Kotlin libraries by

    Márton Braun 
 https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/ Public API challenges in Kotlin by Jake Wharton https://jakewharton.com/public-api-challenges-in-kotlin/
  37. @MOLSJEROEN 3. COMPATIBILITY: GRADLE PLUGIN // top level build.gradle plugins

    { id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.8.0-RC' } apiValidation { ignoredPackages += ["com.jeroenmols.library.internal", “com.jeroenmols.library.databinding"] ignoredClasses += ["com.jeroenmols.library.BuildCon fi g"] ignoredProjects += ["app"] }
  38. @MOLSJEROEN 3. COMPATIBILITY: GRADLE PLUGIN // Snapshot current API into

    api subfolder $ ./gradlew apiDum p // Verify current API against snapshot $ ./gradlew apiCheck
  39. @MOLSJEROEN 4. MINIMIZING API SURFACE • Easy to learn •

    Hard to make mistakes • Reduce maintenance • Avoid deprecation • Easier to change
  40. @MOLSJEROEN 4. MINIMIZE API SURFACE // build.gradle fi le of

    each module android { ... kotlinOptions { freeCompilerArgs += '-Xexplicit-api=strict' } } // library class Library { fun initialize() { ... } } Visibility must be specified in explicit API mode
  41. @MOLSJEROEN 4. MINIMIZE API SURFACE // build.gradle fi le of

    each module android { ... kotlinOptions { freeCompilerArgs += '-Xexplicit-api=strict' } } // librar y class Library { fun initialize() { ... } }
  42. @MOLSJEROEN 4. MINIMIZE API SURFACE // build.gradle fi le of

    each modul e android { .. . kotlinOptions { freeCompilerArgs += '-Xexplicit-api=strict ' } } // library class Library { fun initialize() { ... } } Visibility must be specified in explicit API mode
  43. @MOLSJEROEN 5. EXPERIMENTAL APIS @RequiresOptIn(level = RequiresOptIn.Level.ERROR, message = "This

    is an experimental API.") @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) public annotation class ExperimentalMyLibraryApi
  44. @MOLSJEROEN 5. EXPERIMENTAL APIS @RequiresOptIn(level = RequiresOptIn.Level.ERROR, message = "This

    is an experimental API.") @Retention(AnnotationRetention.BINARY ) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION ) public annotation class ExperimentalMyLibraryApi
  45. @MOLSJEROEN 5. EXPERIMENTAL APIS // library @ExperimentalMyLibraryApi fun doSomething() //

    ap p @OptIn(ExperimentalMyLibraryApi::class ) override fun myFunction() { doSomething( ) }
  46. @MOLSJEROEN 5. EXPERIMENTAL APIS // librar y @ExperimentalMyLibraryAp i fun

    doSomething( ) // app @OptIn(ExperimentalMyLibraryApi::class) override fun myFunction() { doSomething( ) }
  47. @MOLSJEROEN 5. EXPERIMENTAL APIS @kotlin.RequiresOptIn( level = kotlin.RequiresOptIn.Level.ERROR, ) public

    annotation class ExperimentalMyLibraryAp i @androidx.annotation.RequiresOptIn( level = androidx.annotation.RequiresOptIn.Level.ERROR, ) public annotation class ExperimentalMyLibraryApi
  48. @MOLSJEROEN 6. DEPRECATION - REPLACE class Library { @Deprecated(message =

    “Please use the version with caching options”, replaceWith = ReplaceWith(“initialize(true)")) fun initialize( ) fun initialize(useCaching: Boolean ) }
  49. @MOLSJEROEN 6. DEPRECATION - HIDE class Library { @Deprecated(message =

    “Please use the version with caching options”, level = DeprecationLevel.HIDDEN) fun initialize( ) fun initialize(useCaching: Boolean ) }
  50. @MOLSJEROEN 7. PROGUARD android { defaultCon fi g { ...

    consumerProguardFiles 'consumer-rules.pro' } }
  51. @MOLSJEROEN FURTHER READS Maintaining compatibility in Kotlin libraries by Márton

    Braun The hidden Kotlin gem: Deprecations with ReplaceWith by Marc Reichelt Publishing libraries to Maven Central in 2021 by Marton Braun Java interoperability policy for major version updates by Jake Wharton Public API challenges in Kotlin by Jake Wharton How to publish and distribute your Android library by Marco Gomiero What is a diamond dependency con fl ict by Google All about Opt-in annotations by Márton Braun