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

[DroidKaigi 2025] Nasty Dependencies: Surviving...

Avatar for Yury Yury
September 11, 2025

[DroidKaigi 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

September 11, 2025
Tweet

Other Decks in Programming

Transcript

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

    Vlad linkedin.com/in/yury-vlad DroidKaigi 2025 1
  2. UX • Users won't be happy if the app crashes

    • They might stop using it and leave negative reviews • It takes around six five-star reviews to cancel out a single one-star review DroidKaigi 2025 6
  3. Google Play Vitals Crashes and ANRs affect an app's search

    ranking in the Play Store. DroidKaigi 2025 7
  4. GitHub page of the library • A good place to

    start • Search by error message or stacktrace parts • The fix might already be released DroidKaigi 2025 9
  5. Snapshots • The bug is fixed but not yet released

    • Some libraries provide -SNAPSHOT dependencies • These snapshots are the latest builds from the main branch • Follow the library's instructions for testing DroidKaigi 2025 10
  6. 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 ... } DroidKaigi 2025 11
  7. Reproducible builds • Reproducible builds • Android builds are reproducible

    when: ◦ All dependencies and tools are locked to specific versions ◦ JDK version and vendor are identical • F-Droid provides a detailed guide DroidKaigi 2025 13
  8. Making snapshots stable Download a specific snapshot version and host

    it locally to lock it in place. See the Hosting section for details. DroidKaigi 2025 14
  9. Issue not fixed When the issue isn't fixed upstream: •

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

    your issue in the sample app or tests DroidKaigi 2025 16
  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 DroidKaigi 2025 17
  12. Direct include • Shares the same build configuration • No

    isolation • Build tools version mismatch (e.g. AGP) • Gradle configs may affect each other unexpectedly DroidKaigi 2025 18
  13. Composite build Gradle Composite Builds to the rescue: • git

    clone git@url ../library-under-investigation • Add includeBuild('../library-under-investigation') to settings.gradle DroidKaigi 2025 19
  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') ... } } DroidKaigi 2025 20
  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 } DroidKaigi 2025 24
  16. Finding the root cause Now we can debug the issue

    using our real app environment and usage patterns. DroidKaigi 2025 25
  17. Production debugging • Debug with production data • Still follow

    the same setup and use includeBuild • Add logs and meta information inside the library DroidKaigi 2025 26
  18. Logs • Help understand the user path that led to

    the issue • Can include useful meta info like inputs and current state DroidKaigi 2025 27
  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 } } DroidKaigi 2025 28
  20. Logs • Add Logger.log in key places: navigation, clicks, method

    calls • Keep logs short — size limits apply • Use acronyms • Catch exceptions and use Logger.recordException with metaInfo • Don’t put dynamic info in the message, pass it in metaInfo • Wrap the exception, if dynamic info is already presented DroidKaigi 2025 29
  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 } } DroidKaigi 2025 30
  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) } } } } DroidKaigi 2025 31
  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) } }); } } DroidKaigi 2025 32
  24. Hosting • Changes are done and tested locally • Or

    there are no changes and we want a snapshot version • How to deploy to production? DroidKaigi 2025 33
  25. Git We cloned the original repo, now let’s set it

    up as a fork. • Create a private repo on your own server • If allowed, create GitHub fork instead • git remote rename origin upstream (to track the original) • git remote add origin fork-repo-url.git • git push -u origin main DroidKaigi 2025 34
  26. Git • Use feature branches for your changes • Keep

    the history clean • Bonus: public GitHub forks have CI for free DroidKaigi 2025 35
  27. Git Updating the fork: • git fetch upstream • git

    merge upstream/main Alternative: DroidKaigi 2025 36
  28. Before committing • Local setup breaks if committed • library-under-investigation

    is outside the app repo • Revert includeBuild changes before continuing DroidKaigi 2025 37
  29. Naive approach • ./gradlew assembleRelease the library • Copy .aar

    files into the main project • implementation files('library.aar') DroidKaigi 2025 38
  30. Naive approach • No .pom or .module files • Information

    about transitive dependencies is lost • Incompatible with Kotlin Multiplatform libraries DroidKaigi 2025 39
  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 DroidKaigi 2025 41
  32. 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 ). DroidKaigi 2025 43
  33. 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) DroidKaigi 2025 44
  34. Local Maven repository Replicates Maven repository (like mavenCentral ) by

    storing files locally in a folder. DroidKaigi 2025 47
  35. Local Maven repository • ./gradlew publishMavenLocal will create an ~/.m2/repository

    folder in your home directory • com/sample/library/core/1.0.0/core-1.0.0.jar , *.pom and other files that are required for Maven • cp ~/.m2/repository my-app/local-maven-repo • repositories { maven { url uri('local-maven-repo') } } DroidKaigi 2025 48
  36. Local Maven repository • Easy temporary solution • Storing binaries

    in git is bad ◦ But we are already doing it anyway (image, assets, etc.) ◦ If you have git-lfs setup, that's great – use it! • Updating is a little bit painful • Drop the folder on each version change to not store previous versions DroidKaigi 2025 49
  37. Remote Maven repository • "Big boys" approach • Storing dependencies

    in a proper place • Hosting costs • Maintenance costs • Security • No builds if down DroidKaigi 2025 50
  38. Remote Maven repository Options: • Sonatype Nexus Repository Manager •

    JFrog Artifactory • Apache Archiva DroidKaigi 2025 51
  39. Remote Maven repository Already done: • POM setup (what to

    publish) is already set up and independent Required changes to the library: • Setting up a new repository • Providing secure credentials • Separating logical and infrastructure changes DroidKaigi 2025 52
  40. Remote Maven repository Follow your repository publishing guidelines. publishing {

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

    • Environment variables ◦ ORG_GRADLE_PROJECT_privateMavenUsername ◦ ORG_GRADLE_PROJECT_privateMavenPassword DroidKaigi 2025 54
  42. 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. DroidKaigi 2025 55
  43. Artifact signatures Execution failed for task `:core:signMavenPublication` > Cannot perform

    signing task `:core:signMavenPublication` because it has no configured signatory. DroidKaigi 2025 56
  44. Artifact signatures Allow verification of uploaded artifact authenticity. • Private

    key creates .arc signature file for each artifact • Public key verifies signature and confirms authenticity Required by Maven Central. • Prevents attackers from uploading malware even with stolen credentials DroidKaigi 2025 57
  45. Artifact signature Disable signing by removing from code: • plugins

    { id "signing" } • signing { ... } • signAllPublications() DroidKaigi 2025 58
  46. 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. DroidKaigi 2025 59
  47. 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 DroidKaigi 2025 60
  48. Git submodule • Reference to another Git repository at a

    specific commit • Checked out separately in a subfolder DroidKaigi 2025 61
  49. Git submodule Init flow: • git submodule add [email protected] my-lib-fork

    • Use includeBuild("my-lib-fork") in settings.gradle DroidKaigi 2025 62
  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 latest commits DroidKaigi 2025 63
  51. Git submodule Possible to directly change both the fork and

    the main project: • Make changes in the submodule directory • Commit changes inside the submodule • Push the submodule changes • Update the main repo to point to the new commit • Commit the submodule reference update in the main repo • Push the main repo changes DroidKaigi 2025 64
  52. Git submodule • Better than binaries for frequent changes •

    Git workflow adds significant complexity DroidKaigi 2025 65
  53. Git subtree • Merges external repository content directly into the

    main repo • Work with the code as usual • But harder to push back changes DroidKaigi 2025 66
  54. Summary Hosting options: • File • Local Maven repository •

    Remote Maven repository • Git submodule • Git subtree DroidKaigi 2025 67
  55. Upstreaming • Sharing is caring • You already have a

    fork with changes • Reduce difference between the fork and the original DroidKaigi 2025 69
  56. Upstreaming Code is owned by your employer. Seek formal legal

    permission to share the changes. DroidKaigi 2025 70
  57. Upstreaming • git fetch upstream • git checkout -b fix-something-branch

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

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

    "I am passing ownership of this code to you with no further legal claims" DroidKaigi 2025 74
  60. Upstreaming • Might ask to change something or change it

    themselves • Use "Allow maintainers to push to your branch" flag • No obligations to merge your commit • Can even redo it from scratch in their own way DroidKaigi 2025 75
  61. Closed source But what if the nasty one is a

    closed source library? DroidKaigi 2025 77
  62. What are our options? • Reach out to engineering &

    support team • Workaround on our end ◦ May break Terms of Service DroidKaigi 2025 78
  63. Android component level issues • Unwanted uncontrolled initialization code in

    ContentProvider • Unwanted BroadcastReceiver that crashes • Unencrypted SharedPreferences for sensitive data DroidKaigi 2025 79
  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" /> DroidKaigi 2025 80
  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) DroidKaigi 2025 81
  66. Reflection • Access private members by their name • No

    compile-safety, can fail at runtime • Performance overhead • Obfuscation challenges DroidKaigi 2025 83
  67. Reflection • Calling private methods • Fixing memory leaks •

    Dumping state for analytics DroidKaigi 2025 84
  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) DroidKaigi 2025 85
  69. Reflection • Reflection can invoke methods and overwrite fields •

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

    Java decompiler • Bytecode editor • Export back DroidKaigi 2025 88
  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 ... } DroidKaigi 2025 90
  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; DroidKaigi 2025 94
  73. Recaf • Simple patches • LLM • "Show Kotlin Bytecode"

    option in Android Studio DroidKaigi 2025 97
  74. Conclusion • Be brave • Try to fix issues even

    when it's not your code • Share the fixes back if possible DroidKaigi 2025 98