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

[Droidcon London 2025] Nasty Dependencies: Surv...

Avatar for Yury Yury
October 30, 2025

[Droidcon London 2025] Nasty Dependencies: Surviving and fixing bugs in 3rd-party libraries

As developers, we are responsible for bugs in our apps, even when they are caused by third-party dependencies.

In this session, I will give an overview of different ways to handle this situation, including:
- Debugging, logging, or fixing issues using advanced Gradle techniques;
- Forking libraries and using them in your project via Git submodules, Nexus repositories, or direct artifacts;
- Upstreaming your changes to the original library repository;
- Possible approaches for working with closed-source libraries.

This session will act more like a guidebook than a deep technical dive, offering practical options developers can choose from depending on their situation.

Avatar for Yury

Yury

October 30, 2025
Tweet

More Decks by Yury

Other Decks in Programming

Transcript

  1. Nasty Dependencies: Surviving and fixing bugs in 3rd-party libraries Yury

    Vlad linkedin.com/in/yury-vlad Andrew Belous linkedin.com/in/abelous Droidcon London 2025 1
  2. Agenda • Investigating issues in 3rd-party dependencies • Debugging with

    your real data • Forking and maintaining libraries • Approaches for closed-source libraries Droidcon London 2025 2
  3. UX • Users won't be happy if the app crashes

    • Users may abandon app and leave negative reviews Droidcon London 2025 5
  4. Google Play Vitals Crashes and ANRs affect an app's search

    ranking in the Play Store. Droidcon London 2025 6
  5. GitHub page of the library • A good place to

    start • Search by error message or stacktrace parts • The fix might already be released Droidcon London 2025 8
  6. Snapshots • The bug is fixed but not yet released

    • Use -SNAPSHOT dependencies for testing • Latest builds from main branch Droidcon London 2025 9
  7. Snapshot usage // app/build.gradle repositories { ... maven { url

    'https://oss.sonatype.org/content/repositories/snapshots' } } dependencies { ... implementation 'org.sample.library:core:3.0.0-SNAPSHOT' implementation 'org.sample.library:utils:3.0.0' // Never mix versions implementation 'org.sample.library:utils:3.0.0-SNAPSHOT' // Use snapshot everywhere ... } Droidcon London 2025 10
  8. Reproducible builds • Reproducible builds • Android builds reproducebility: ◦

    All dependencies and tools are locked ◦ JDK version and vendor are identical • F-Droid provides a detailed guide Droidcon London 2025 12
  9. Issue not fixed When the issue isn't fixed upstream: •

    Fix it yourself • Add workarounds to prevent crashes Droidcon London 2025 14
  10. First steps • git clone [email protected]:account/name.git • Try to reproduce

    your issue in the sample app or tests Droidcon London 2025 15
  11. Issue reproducible only in your app Intuitive approach: • cd

    my-app • git clone [email protected]:account/name.git library-under-investigation • Add include 'library-under-investigation' to settings.gradle Droidcon London 2025 16
  12. Direct include • No isolation between projects • Build tool

    version conflicts (e.g. AGP) • Configurations may interfere Droidcon London 2025 17
  13. Composite build Gradle Composite Builds to the rescue: • git

    clone git@url ../library-under-investigation • Add includeBuild('../library-under-investigation') to settings.gradle Droidcon London 2025 18
  14. Composite build // settings.gradle includeBuild('../library-under-investigation') { dependencySubstitution { substitute module('org.sample.library:core')

    using project(':core') substitute module('org.sample.library:utils') using project(':utils') ... } } Droidcon London 2025 19
  15. Composite build // app/build.gradle dependencies { implementation 'org.sample.library:core' // Works

    implementation project(':library-under-investigation:core') // Does not } // library-under-investigation/core/build.gradle dependencies { implementation project(':app') // Does not work } Droidcon London 2025 23
  16. Finding the root cause Now we can debug the issue

    using our real app environment and usage patterns. Droidcon London 2025 24
  17. Production debugging • Use includeBuild setup • Add logs inside

    the library • Debug with production data Droidcon London 2025 25
  18. Logs • Track user path leading to crashes • Include

    inputs and current state Droidcon London 2025 26
  19. Logs package org.sample.library interface Logger { fun log(message: String) fun

    recordException(exception: Exception, metaInfo: Map<String, String> = emptyMap()) companion object { lateinit var Instance: Logger } } Droidcon London 2025 27
  20. Logs • Add Logger.log in key places • Keep logs

    short — size limits apply • Use Logger.recordException with metadata • Put dynamic data in metaInfo , not the message Droidcon London 2025 28
  21. Logs package com.my.app object LoggerImpl : Logger { override fun

    log(message: String) { // Log using app-specific crash reporting tool } override fun recordException(exception: Exception, metaInfo: Map<String, String>) { // Log using app-specific crash reporting tool } } Droidcon London 2025 29
  22. Crashlytics Crashlytics logger implementation: package com.my.app object LoggerImpl : Logger

    { override fun log(message: String) { // Crashlytics limits logs to 64kB and deletes older log entries Firebase.crashlytics.log(message) } override fun recordException(exception: Exception, metaInfo: Map<String, String>) { Firebase.crashlytics.recordException(exception) { // 64 key/value pairs maximum // Each pair can be up to 1 kB (~1000 characters) metaInfo.forEach { (k, v) -> key(k, v) } } } } Droidcon London 2025 30
  23. Sentry Sentry logger implementation: package com.my.app object LoggerImpl : Logger

    { override fun log(message: String) { // Possible to use Breadcrumb class directly with customizable fields // Messages are limited to 8192 characters Sentry.addBreadcrumb(message) } override fun recordException(exception: Exception, metaInfo: Map<String, String>) { Sentry.captureException(exception, scope -> { // Context objects are limited to 8KB metaInfo.forEach { (k, v) -> scope.setContexts(k, v) } }); } } Droidcon London 2025 31
  24. Hosting • Fixes or logging added • Snapshot version locking

    • How to deploy to production? Droidcon London 2025 32
  25. Git Set up your fork: • Create a private repo

    (or GitHub fork) • git remote rename origin upstream • git remote add origin fork-repo-url.git • git push -u origin main Droidcon London 2025 33
  26. Git • Use feature branches • Keep the history clean

    • Bonus: public GitHub forks have CI for free Droidcon London 2025 34
  27. Git Updating the fork: • git fetch upstream • git

    merge upstream/main Alternative: Droidcon London 2025 35
  28. Using in the project • Current includeBuild setup breaks for

    team & CI • Remove it before continuing Droidcon London 2025 36
  29. Naive approach • Build using ./gradlew assembleRelease • Copy .aar

    files to project • Add implementation files('library.aar') Droidcon London 2025 37
  30. Naive approach • Missing .pom or .module files • Transitive

    dependencies lost • Won't work with KMP libraries Droidcon London 2025 38
  31. Transitive dependencies use 'core:1.0' use 'library' .pom, core:1.1 App org.utils:core:

    1.0 -> 1.1 Library org.utils:core:1.1 Droidcon London 2025 40
  32. Version lock Gradle dependency locking: // app/build.gradle configurations.configureEach { if

    (name == "releaseRuntimeClasspath") { resolutionStrategy.activateDependencyLocking() } } ./gradlew app:dependencies --write-locks creates gradle.lockfile . ./gradlew assembleRelease fails on mismatch. Droidcon London 2025 41
  33. Binary compatibility Transitive dependency v1.0 → v1.1 adds a field

    with default value: data class TransitiveDepClass( val fieldOne: Int = 0, + val fieldTwo: String = "", // Source compatible change ) Library is compiled against v1.1. class LibrarySdk { val instance = TransitiveDepClass() } Your app provides v1.0 (due to missing .pom ). Droidcon London 2025 44
  34. Binary compatibility App code: // Crash with NoSuchMethodException(TransitiveDepClass constructor(.,.,.)) val

    library = LibrarySdk() Data class constructors change in bytecode: • v1.0: constructor(fieldOne, defaultMarkers) • v1.1: constructor(fieldOne, fieldTwo, defaultMarkers) Droidcon London 2025 45
  35. Local Maven repository Replicates Maven repository (like mavenCentral ) by

    storing files locally in a folder. Droidcon London 2025 48
  36. Local Maven repository • ./gradlew publishMavenLocal creates ~/.m2/repository folder •

    cp ~/.m2/repository my-app/local-maven-repo • repositories { maven { url uri('local-maven-repo') } } Droidcon London 2025 49
  37. Local Maven repository • Easy temporary solution • Binaries in

    git ◦ Use git-lfs if set up • Manual updates required Droidcon London 2025 50
  38. Remote Maven repository • "Big boys" approach • Storing dependencies

    in a proper place • Hosting & maintenance costs • No builds if down Droidcon London 2025 51
  39. Remote Maven repository Options: • Sonatype Nexus Repository Manager •

    JFrog Artifactory • Apache Archiva Droidcon London 2025 52
  40. Remote Maven repository Most libraries already have POM setup. Required

    changes: • Configure repository location • Add credentials • Keep infrastructure changes separate Droidcon London 2025 53
  41. Remote Maven repository Follow your repository publishing guidelines. publishing {

    repositories { maven { name = "privateMaven" url = uri("https://my-maven-repo.com") credentials(PasswordCredentials) } } } Droidcon London 2025 54
  42. Remote Maven repository • ./gradlew publish_PublicationName_ToPrivateMavenRepository \ -PprivateMavenUsername=username \ -PprivateMavenPassword=password

    • Environment variables ◦ ORG_GRADLE_PROJECT_privateMavenUsername ◦ ORG_GRADLE_PROJECT_privateMavenPassword Droidcon London 2025 55
  43. Remote Maven repository If you see: plugins { id "com.vanniktech.maven.publish"

    } Utilize the following plugin task for simplicity: ./gradlew publishAllPublicationsToPrivateMavenRepository Pass the credentials in the same way. Droidcon London 2025 56
  44. Artifact signatures Execution failed for task `:core:signMavenPublication` > Cannot perform

    signing task `:core:signMavenPublication` because it has no configured signatory. Droidcon London 2025 57
  45. Artifact signatures Allow verification of uploaded artifact authenticity. • Private

    key creates .arc signature files • Public key verifies signature Required by Maven Central. • Prevents malware uploads with stolen credentials Droidcon London 2025 58
  46. Artifact signature Disable signing by removing from code: • plugins

    { id "signing" } • signing { ... } • signAllPublications() Droidcon London 2025 59
  47. Artifact signature Generate temporary signing config: • Use gpg to

    create key pair • Add signing parameters: ./gradlew ... -Psigning.secretKeyRingFile=~/.gnupg/secring.gpg \ -Psigning.password=secret \ -Psigning.keyId=24875D73 Gradle documentation to signing plugin. Droidcon London 2025 60
  48. Artifact signature Remove signature Stub signature More changes in the

    code base Fewer changes in the code base More problems with merging upstream Fewer problems with merging upstream Droidcon London 2025 61
  49. Git submodule Init flow: • git submodule add [email protected] my-lib-fork

    • Use includeBuild("my-lib-fork") in settings.gradle Droidcon London 2025 63
  50. Git submodule Usage: • git clone --recursive for new clones

    • git submodule update --init --recursive to initialize submodules in existing repos • git submodule update --remote to update to the latest commits Droidcon London 2025 64
  51. Git submodule Making changes: • Commit and push in submodule

    first • Update main repo reference • Commit and push main repo Droidcon London 2025 65
  52. Git submodule • Better than binaries for frequent changes •

    Git workflow adds significant complexity Droidcon London 2025 66
  53. Git subtree • Merges external repository content directly into the

    main repo • Work with the code as usual • But harder to push back changes Droidcon London 2025 67
  54. Summary Avoid: • Direct binary Small team: • Local Maven

    repository • Git submodule / subtree Big team: • Remote Maven repository Droidcon London 2025 68
  55. Upstreaming • Share your fixes with the community • Reduce

    maintenance burden Droidcon London 2025 70
  56. Upstreaming Code is owned by your employer. Seek formal legal

    permission to share the changes. Droidcon London 2025 71
  57. Upstreaming • git fetch upstream • git checkout -b fix-branch

    upstream/main • Cherry pick commits from the fork branch into the new one • Alternative: export as a patch and apply • Exclude infrastructure changes Droidcon London 2025 72
  58. Upstreaming • Follow the provided template • Explain what you

    are doing • Mention any related issues: #123 Droidcon London 2025 74
  59. Upstreaming CLA – Contributor License Agreement. • Legal requirement •

    "I am passing ownership of this code to you with no further legal claims" Droidcon London 2025 75
  60. Upstreaming • Maintainers may request changes • Use "Allow maintainers

    to push to your branch" flag • No obligation to merge your PR Droidcon London 2025 76
  61. Closed source But what if the nasty one is a

    closed source library? Droidcon London 2025 78
  62. What are our options? • Contact support team • Workaround

    on our end ◦ May break Terms of Service Droidcon London 2025 79
  63. Android component level issues • Auto-initialization via ContentProvider • A

    BroadcastReceiver that crashes • Unencrypted SharedPreferences for sensitive data Droidcon London 2025 80
  64. Android component level issues Disable a component: <!-- AndroidManifest.xml -->

    <provider android:name="com.thirdparty.sdk.SomeContentProvider" android:authorities="com.thirdparty.provider" android:enabled="false" tools:node="replace" /> Droidcon London 2025 81
  65. Android component level issues We can provide a custom Context

    to a library: val contextWrapper = object : ContextWrapper(this) { override fun getSharedPreferences(name: String, mode: Int): SharedPreferences = TODO("Provide encrypted SharedPreferences") } Library.initialize(contextWrapper) Droidcon London 2025 82
  66. Reflection • Access private members • No compile-time safety •

    Performance overhead • Obfuscation challenges Droidcon London 2025 85
  67. Reflection • Calling private methods • Fixing memory leaks •

    Dumping state for analytics Droidcon London 2025 86
  68. Reflection val sdk = MySdk() // access private field val

    field = sdk.javaClass.getDeclaredField("isInitialized") field.isAccessible = true Logger.log("SDK is initialized: ${field.get(sdk)}") // call private method val method = sdk.javaClass.getDeclaredMethod("dispose") method.isAccessible = true method.invoke(sdk) Droidcon London 2025 87
  69. Reflection • Reflection can invoke methods and overwrite fields •

    Reflection can't change the code itself • Bytecode editing Droidcon London 2025 88
  70. Bytecode editing Col-E/Recaf • Supports both .jar and .aar •

    Java decompiler • Bytecode editor • Export back Droidcon London 2025 90
  71. CoroutineWorker package androidx.work public abstract class CoroutineWorker(...) : ListenableWorker(appContext, params)

    { /** * The coroutine context on which [doWork] will run. By default, it is [Dispatchers.Default]. */ private val coroutineContext: CoroutineDispatcher = Dispatchers.Default ... } Droidcon London 2025 92
  72. Bytecode editor aload this invokestatic - kotlinx/coroutines/Dispatchers.getDefault + kotlinx/coroutines/Dispatchers.getIO ()Lkotlinx/coroutines/CoroutineDispatcher;

    putfield androidx/work/CoroutineWorker.coroutineContext Lkotlinx/coroutines/CoroutineDispatcher; Droidcon London 2025 96
  73. Hosting Options: • File • Local Maven repository • Remote

    Maven repository Droidcon London 2025 98
  74. Recaf • Simple patches • LLM • "Show Kotlin Bytecode"

    option in Android Studio Droidcon London 2025 99
  75. Obfuscation issues • Libraries brings consumer-proguard.pro • Might be excessive

    • Unzip .aar and edit directly Droidcon London 2025 100
  76. Conclusion • Be brave • Try to fix issues even

    when it's not your code • Share the fixes back if possible Droidcon London 2025 101