Share
Embed
[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.
Yury
October 30, 2025
Transcript
101 slides
-
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 -
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 -
UX • Users won't be happy if the app crashes
• Users may abandon app and leave negative reviews Droidcon London 2025 5 -
Google Play Vitals Crashes and ANRs affect an app's search
ranking in the Play Store. Droidcon London 2025 6 -
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 -
Snapshots • The bug is fixed but not yet released
• Use -SNAPSHOT dependencies for testing • Latest builds from main branch Droidcon London 2025 9 -
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 -
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 -
Making snapshots stable Download and host a specific snapshot version
locally. Droidcon London 2025 13 -
Issue not fixed When the issue isn't fixed upstream: •
Fix it yourself • Add workarounds to prevent crashes Droidcon London 2025 14 -
First steps • git clone [email protected]:account/name.git • Try to reproduce
your issue in the sample app or tests Droidcon London 2025 15 -
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 -
Direct include • No isolation between projects • Build tool
version conflicts (e.g. AGP) • Configurations may interfere Droidcon London 2025 17 -
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 -
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 -
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 -
Finding the root cause Now we can debug the issue
using our real app environment and usage patterns. Droidcon London 2025 24 -
Production debugging • Use includeBuild setup • Add logs inside
the library • Debug with production data Droidcon London 2025 25 -
Logs • Track user path leading to crashes • Include
inputs and current state Droidcon London 2025 26 -
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 -
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 -
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 -
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 -
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 -
Hosting • Fixes or logging added • Snapshot version locking
• How to deploy to production? Droidcon London 2025 32 -
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 -
Git • Use feature branches • Keep the history clean
• Bonus: public GitHub forks have CI for free Droidcon London 2025 34 -
Git Updating the fork: • git fetch upstream • git
merge upstream/main Alternative: Droidcon London 2025 35 -
Using in the project • Current includeBuild setup breaks for
team & CI • Remove it before continuing Droidcon London 2025 36 -
Naive approach • Build using ./gradlew assembleRelease • Copy .aar
files to project • Add implementation files('library.aar') Droidcon London 2025 37 -
Naive approach • Missing .pom or .module files • Transitive
dependencies lost • Won't work with KMP libraries Droidcon London 2025 38 -
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 -
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 -
Transitive dependencies When not using expected version: • Behavior incompatibility
• Binary incompatibility Droidcon London 2025 43 -
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 -
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 -
Binary compatibility R8 might highlight binary compatibility issues in the
warnings log. Droidcon London 2025 46 -
Local Maven repository Replicates Maven repository (like mavenCentral ) by
storing files locally in a folder. Droidcon London 2025 48 -
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 -
Local Maven repository • Easy temporary solution • Binaries in
git ◦ Use git-lfs if set up • Manual updates required Droidcon London 2025 50 -
Remote Maven repository • "Big boys" approach • Storing dependencies
in a proper place • Hosting & maintenance costs • No builds if down Droidcon London 2025 51 -
Remote Maven repository Options: • Sonatype Nexus Repository Manager •
JFrog Artifactory • Apache Archiva Droidcon London 2025 52 -
Remote Maven repository Most libraries already have POM setup. Required
changes: • Configure repository location • Add credentials • Keep infrastructure changes separate Droidcon London 2025 53 -
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 -
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 -
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 -
Artifact signatures Execution failed for task `:core:signMavenPublication` > Cannot perform
signing task `:core:signMavenPublication` because it has no configured signatory. Droidcon London 2025 57 -
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 -
Artifact signature Disable signing by removing from code: • plugins
{ id "signing" } • signing { ... } • signAllPublications() Droidcon London 2025 59 -
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 -
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 -
Git submodule Reference to another Git repository at a specific
commit. Droidcon London 2025 62 -
Git submodule Init flow: • git submodule add [email protected] my-lib-fork
• Use includeBuild("my-lib-fork") in settings.gradle Droidcon London 2025 63 -
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 -
Git submodule Making changes: • Commit and push in submodule
first • Update main repo reference • Commit and push main repo Droidcon London 2025 65 -
Git submodule • Better than binaries for frequent changes •
Git workflow adds significant complexity Droidcon London 2025 66 -
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 -
Summary Avoid: • Direct binary Small team: • Local Maven
repository • Git submodule / subtree Big team: • Remote Maven repository Droidcon London 2025 68 -
Upstreaming Changes are done and tested in production. What's next?
Droidcon London 2025 69 -
Upstreaming • Share your fixes with the community • Reduce
maintenance burden Droidcon London 2025 70 -
Upstreaming Code is owned by your employer. Seek formal legal
permission to share the changes. Droidcon London 2025 71 -
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 -
Upstreaming • Follow the provided template • Explain what you
are doing • Mention any related issues: #123 Droidcon London 2025 74 -
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 -
Upstreaming • Maintainers may request changes • Use "Allow maintainers
to push to your branch" flag • No obligation to merge your PR Droidcon London 2025 76 -
Closed source But what if the nasty one is a
closed source library? Droidcon London 2025 78 -
What are our options? • Contact support team • Workaround
on our end ◦ May break Terms of Service Droidcon London 2025 79 -
Android component level issues • Auto-initialization via ContentProvider • A
BroadcastReceiver that crashes • Unencrypted SharedPreferences for sensitive data Droidcon London 2025 80 -
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 -
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 -
Android component level issues • ./gradlew :app:processDebugManifest • app/build/intermediates/merged_manifests/release/ processReleaseManifest/AndroidManifest.xml
• Save as baseline • Compare later Droidcon London 2025 83 -
Code-level issues • Memory leaks from static variables • Incorrect
disposals Droidcon London 2025 84 -
Reflection • Access private members • No compile-time safety •
Performance overhead • Obfuscation challenges Droidcon London 2025 85 -
Reflection • Calling private methods • Fixing memory leaks •
Dumping state for analytics Droidcon London 2025 86 -
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 -
Reflection • Reflection can invoke methods and overwrite fields •
Reflection can't change the code itself • Bytecode editing Droidcon London 2025 88 -
Bytecode editing Col-E/Recaf • Supports both .jar and .aar •
Java decompiler • Bytecode editor • Export back Droidcon London 2025 90 -
Our task In androidx.work:work-runtime-ktx:2.7.0 replace the default coroutine dispatcher with
IO Dispatcher. Droidcon London 2025 91 -
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 -
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 -
Hosting Options: • File • Local Maven repository • Remote
Maven repository Droidcon London 2025 98 -
Recaf • Simple patches • LLM • "Show Kotlin Bytecode"
option in Android Studio Droidcon London 2025 99 -
Obfuscation issues • Libraries brings consumer-proguard.pro • Might be excessive
• Unzip .aar and edit directly Droidcon London 2025 100 -
Conclusion • Be brave • Try to fix issues even
when it's not your code • Share the fixes back if possible Droidcon London 2025 101