Library Developer Confidential

Library Developer Confidential

For the last several years of my career I have been tasked with creating and maintaining libraries that handle complex bluetooth communication and Android UI. In this talk we will go over setting up library projects for success, bootstrapping an architecture that will allow you to grow with minimal collisions and boilerplate, how to build/deploy multiple libraries while trying to debug without losing your sanity and more. At the end of this talk it is my hope you will feel confident enough to start and maintain your own library project. This talk is part gradle, part java/kotlin and absolutely free of Git Submodules.

9904145db476b6658d75fff27b1929ed?s=128

Elliott Chenger

July 19, 2018
Tweet

Transcript

  1. 2.
  2. 10.
  3. 11.

    App

  4. 13.

    class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
  5. 14.
  6. 15.

    class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val textView = findViewById<TextView>(R.id.text_view) textView.text = "Pugs are always hungry ${Pug.isHungry}" } }
  7. 16.
  8. 17.

    App

  9. 18.
  10. 20.

    class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
  11. 23.

    class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val textView = findViewById<TextView>(R.id.text_view) textView.text = "Pugs are always hungry ${Pug.isHungry()}" } }
  12. 28.

    - Gradle Documentation “The best way to do work faster

    is to not do work that doesn’t need doing.”
  13. 29.
  14. 31.

    App

  15. 36.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation

    'com.android.support:appcompat-v7:27.1.1' implementation project(':pugs') implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
  16. 37.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation

    'com.android.support:appcompat-v7:27.1.1' implementation project(':pugs') implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
  17. 38.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation

    'com.android.support:appcompat-v7:27.1.1' implementation project(':pugs') implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
  18. 39.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation

    'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
  19. 43.

    App

  20. 50.
  21. 52.
  22. 53.
  23. 54.
  24. 55.
  25. 57.

    Build.gradle dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0'

    implementation 'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' implementation project(":pugs") testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core: 3.0.2' }
  26. 58.

    Build.gradle dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0'

    implementation 'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' implementation project(":pugs") testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core: 3.0.2' }
  27. 61.

    Only deploys in one app? No Does it need to

    be shared with others? Yes
  28. 66.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation

    'com.android.support:appcompat-v7:27.1.1' implementation files('libs/pugs-debug.aar') implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
  29. 67.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation

    'com.android.support:appcompat-v7:27.1.1' implementation files('libs/pugs-debug.aar') implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
  30. 68.

    OR

  31. 70.

    AAR in a Libs Directory dependencies { implementation fileTree(dir: 'libs',

    include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //Including a local gradle module implementation project(":pugs") //Including a local aar implementation(name:'beers-debug', ext:'aar') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }
  32. 71.

    AAR in a Libs Directory dependencies { implementation fileTree(dir: 'libs',

    include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //Including a local gradle module implementation project(":pugs") //Including a local aar implementation(name:'beers-debug', ext:'aar') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }
  33. 72.

    07-01 17:50:12.887 4565-4565/com.omitneedlesscode.myapplication E/AndroidRuntime: FATAL EXCEPTION: main Process: com.omitneedlesscode.myapplication, PID:

    4565 java.lang.NoClassDefFoundError: Failed resolution of: Ltimber/log/Timber$DebugTree; at com.omitneedlesscode.beers.BeerApplication.onCreate(BeerApplication.kt:10) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1118) at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5791) at android.app.ActivityThread.-wrap1(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1661) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
  34. 73.

    07-01 17:50:12.887 4565-4565/com.omitneedlesscode.myapplication E/AndroidRuntime: FATAL EXCEPTION: main Process: com.omitneedlesscode.myapplication, PID:

    4565 java.lang.NoClassDefFoundError: Failed resolution of: Ltimber/log/Timber$DebugTree; at com.omitneedlesscode.beers.BeerApplication.onCreate(BeerApplication.kt:10) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1118) at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5791) at android.app.ActivityThread.-wrap1(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1661) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
  35. 77.

    AAR in a Libs Directory dependencies { implementation fileTree(dir: 'libs',

    include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //Including a local gradle module implementation project(":pugs") //Including a local aar implementation(name:'beers-debug', ext:’aar’) implementation 'com.jakewharton.timber:timber:4.7.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }
  36. 78.

    AAR in a Libs Directory dependencies { implementation fileTree(dir: 'libs',

    include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //Including a local gradle module implementation project(":pugs") //Including a local aar implementation(name:'beers-debug', ext:’aar’) implementation 'com.jakewharton.timber:timber:4.7.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }
  37. 79.

    AAR in a Libs Directory •Easy to use •Quick to

    test changes •Copy over library dependencies •Project switching •GIT LFS or your repo size will balloon •Hard to debug Pros Cons
  38. 83.

    Note // Gradle Composite Builds won't work with local AARs

    implementation(name:'beers-debug', ext:'aar') // They do work with Maven Dependencies implementation 'com.omitneedlesscode:beers:1.0'
  39. 84.

    Note // Gradle Composite Builds won't work with local AARs

    implementation(name:'beers-debug', ext:'aar') // They do work with Maven Dependencies implementation 'com.omitneedlesscode:beers:1.0'
  40. 85.
  41. 86.
  42. 87.
  43. 88.

    •Doesn’t impact other developers •Can change code in both library

    and app •No major modifications to the library project gradle files •Easier to debug •Newer feature so there have been some small bugs •To turn off composite mode may need to re- import library project •Can’t use with provided aars in Lib Pros Cons Gradle Composite Project
  44. 90.

    –Maven Documentation “A repository in Maven is used to hold

    build artifacts and dependencies of varying types.”
  45. 91.

    –Maven Documentation “A repository in Maven is used to hold

    build artifacts and dependencies of varying types.”
  46. 97.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'beers' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/beers-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  47. 98.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'beers' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/beers-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  48. 99.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'beers' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/beers-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  49. 100.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'beers' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/beers-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  50. 101.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'beers' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/beers-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  51. 105.
  52. 106.

    11:06:46 PM: Executing task 'publishToMavenLocal'... Executing tasks: [publishToMavenLocal] Configuration on

    demand is an incubating feature. :beers:generatePomFileForAarPublication :beers:publishAarPublicationToMavenLocal :beers:publishToMavenLocal BUILD SUCCESSFUL in 5s 2 actionable tasks: 2 executed 11:06:51 PM: Task execution finished 'publishToMavenLocal'.
  53. 112.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation

    'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //Including a local gradle module implementation project(":pugs") //Including a local aar implementation ‘com.omitneedlesscode:beers:1.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }
  54. 113.

    dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation

    'com.android.support:support-annotations:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //Including a local gradle module implementation project(":pugs") //Including a local aar implementation 'com.omitneedlesscode:beers:1.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }
  55. 114.
  56. 119.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'pugs' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/pugs-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  57. 120.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'pugs' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/pugs-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  58. 121.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'beers' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/beers-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  59. 122.

    publishing{ publications{ aar(MavenPublication){ groupId 'com.omitneedlesscode' artifactId 'beers' version = android.defaultConfig.versionName

    artifact new File('build/outputs/aar/beers-debug.aar') pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each{ if(it.name!="unspecified"){ def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  60. 125.

    apply plugin: 'maven-publish' ext.filePath = '' ext.expectedArtifactId = '' ext.expectedGroupId

    = '' ext.expectedVersion = '' publishing { publications { aar(MavenPublication) { groupId expectedGroupId artifactId expectedArtifactId version = expectedVersion artifact new File(filePath) pom.withXml { def dependencies = asNode().appendNode('dependencies') configurations.implementation.allDependencies.each { if (it.name != "unspecified") { def dependency = dependencies.appendNode('dependency') dependency.appendNode('groupId', it.group) dependency.appendNode('artifactId', it.name) dependency.appendNode('version', it.version) } } } } } }
  61. 135.

    21:28:57.809 13647-13647/com.omitneedlesscode.nodependency E/AndroidRuntime: FATAL EXCEPTION: main Process: com.omitneedlesscode.nodependency, PID: 13647

    java.lang.NoSuchMethodError: No virtual method load(Ljava/lang/String;)Lcom/bumptech/glide/ DrawableTypeRequest; in class Lcom/bumptech/glide/RequestManager; or its super classes (declaration of 'com.bumptech.glide.RequestManager' appears in /data/app/com.omitneedlesscode.nodependency-m1gfBVcawag- PWVRV_wyZQ==/split_lib_dependencies_apk.apk) at com.omitneedlesscode.pugs.PugActivity.onCreate(PugActivity.kt:16) at android.app.Activity.performCreate(Activity.java:6975) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1213) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892) at android.app.ActivityThread.-wrap11(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
  62. 136.

    21:28:57.809 13647-13647/com.omitneedlesscode.nodependency E/AndroidRuntime: FATAL EXCEPTION: main Process: com.omitneedlesscode.nodependency, PID: 13647

    java.lang.NoSuchMethodError: No virtual method load(Ljava/lang/String;)Lcom/bumptech/glide/ DrawableTypeRequest; in class Lcom/bumptech/glide/RequestManager; or its super classes (declaration of 'com.bumptech.glide.RequestManager' appears in /data/app/com.omitneedlesscode.nodependency-m1gfBVcawag- PWVRV_wyZQ==/split_lib_dependencies_apk.apk) at com.omitneedlesscode.pugs.PugActivity.onCreate(PugActivity.kt:16) at android.app.Activity.performCreate(Activity.java:6975) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1213) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892) at android.app.ActivityThread.-wrap11(Unknown Source:0) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
  63. 139.

    Yes

  64. 146.

    App class GlideImageFetcher: ImageFetcher { override fun loadImageInView(imageUrl: String, defaultImageResource:

    Int, view: ImageView) { GlideApp.with(view).load(imageUrl).placeholder(defaultImageResource).into(view) } }
  65. 148.
  66. 156.