Skip to main content

Managing Open-Source Dependencies with JitPack

Managing dependencies for your Android app has gotten a lot easier with tools such as Gradle but there is still one big gotcha that it cannot solve: The dependency itself. It is quite normal for a single Android app to have dozens of third-party dependencies, most of which are open source. There are many benefits to using open-source libraries, but there are some pain points too. For instance, a dependency may contain bugs, lack needed features or may have been abandoned by the original developer. In this article I will discuss ways to deal with that, leveraging the powers of Git and the online service called JitPack.

JitPack Basics

What is JitPack you ask? It’s a package repository for Git-hosted Android library projects (in fact it works with any JVM project, but we’ll focus on Android in this article). What makes JitPack useful is that it checks out the source code from Git, builds the project, and provides the build artifacts to your build process – on demand. GitHub is well integrated, but it also works with BitBucket, GitLab, and even Azure Repos. What this means to a publisher of an Android library is that you don’t need to publish to a public repository so long as it’s hosted on a supported git host. And this is significant because publishing to something like Maven Central isn’t exactly trivial. For those consuming the library, there’s little difference at all.

Much of what we cover here could also be done with other package repositories such as GitHub Packages, which is great for repos on GitHub. JitPack however is a more universal solution and works with many other package hosts, but the concept here is very much transferable to other git repos and package hosts.

To make it work you’ll need to first add the JitPack repository to your Gradle build and then add the Android library dependencies themselves. We’ll start with adding the JitPack package repository itself:

build.gradle (root)

1                allprojects {

2                            repositories {

3                                        …

4                                        maven { url ‘https://jitpack.io’ }

5                            }

6                }

This adds the JitPack repository to your list of repositories that the Gradle build system will check for dependencies. There’s not much to configure here if your dependency is on a public Git repository, but for a private one you might want to read the full instructions here.

Adding the dependency is straightforward, you just need to know the group, artifact and version of the dependency.

The group is a combination of the Git host itself and the username of the dependency publisher (or in the case of Azure, the project name). Here are some examples from various git repositories:

Repository        Group

GitHub                  com.github.Username
BitBucket              org.bitbucket.Username
GitLab                   com.gitlab.Username
Gitee                      com.gitee.Username
Azure                     com.azure.Project

The artifact simply refers to the repository that contains the dependency you wish to consume. The version is the name of a Git tag, commit hash or “BranchName-SNAPSHOT” to get the latest commit for a particular branch. With all that info ready you can add your dependency like so:

build.gradle (app-level)

1                dependencies {

2                            implementation ‘com.github.mveroukis:MyAwesomeRepo:master-SNAPSHOT’

3                }

The above provides the build artifacts from the most recent commit on the master branch from the MyAwesomeRepo repository. Those are the basics of using JitPack, and for the most part that’s all you really need to know. To learn more, click here.

Open-Source Pain Points

When using any third-party library there are risks. The library could contain unforeseen bugs or design flaws. Months into a project it could be revealed that an expected feature was in fact missing or just didn’t perform as expected. Or even worse, the library is abandoned by the developer. Any one of these issues could impact your project, but open-source software provides an out that closed-source does not: We can simply take the code and fix what’s broken ourselves.

That’s not exactly revolutionary insight right there, that has been a big selling point for open-source software from the start. But we now must decide how to convert this dependency into responsibility, and we have some options. We can simply download the code for our dependency and add it to our project’s source tree. This seems like a logical choice at first but there are downsides. This will increase your build time as you now have a lot more code to build. Placing adopted third-party projects into their own module can help with that, but then you still have the issue that the more projects you adopt, the more cluttered and bloated your source tree becomes. Another consideration is that the code style and practices of your adopted project will most certainly differ greatly, which really throws a wrench into the continuity of your own project’s style and layout. This approach becomes even more perilous if you’re maintaining several projects, all with their own repositories and all depending on this newly adopted project. I call this The Borg approach, as you assimilate various libraries into one giant collective that in the end just creates problems. Resistance is futile. Well, maybe not, but it’s best to just avoid it.

Another approach is to fork it, pushing your adopted open-source dependency right into its own repository in whatever version-control system you happen to be using. If you’re using git, you can add it to your consuming project as a git submodule. This way the two repositories are separated, and independent projects can include the adopted project as a git submodule. The submodule is its own repository with its own source tree and builds process. You’re probably best off building the submodule on its own and adding the resulting binaries to the parent repository’s libs folder. Then just build the parent project and you’re done. Great, problems solved, right? This is a step in the right direction, but you now have the issue that your build process is far more complicated and potentially error-prone. Git submodules need extra care and although it can be done, there’s a long list of gotchas. You can read more about the pitfalls of Git submodules here.

There is another approach that solves these problems, and if you’re wondering what that might be, it was heavily foreshadowed in the first half of this article. That’s right, a package repository such as JitPack. You’d start off by creating your own fork, and then making whatever changes you need to it. The only change to your consuming project would be to update the dependency in your build.gradle file to point to your own fork instead of retrieving it from the original package repository such as Maven Central. In fact, it’s possible that the original package was already using JitPack as its package repository, so all you’d need to do is update the group portion of the dependency specification to include your username. Then, when you perform a Gradle Sync in Android Studio, it will fetch your dependency from JitPack. JitPack in turn will fetch the source from your Git repository, build the dependency with your changes and pass along the artifacts to your build. You might be wondering if this takes longer, and the answer is yes, but only the first time because JitPack does cache its build outputs.

Real-Life Example

Ok, that sounds great, but it would be nice to see it all in action. So let’s look at a real-life example of what I’m talking about.

I have an app that makes use of an open-source Android package named storage-chooser. The source is hosted on GitHub and the build artifacts are distributed by JitPack. It’s a little package that provides a nifty file and folder selection dialog – useful for when you need the user to specify a path on the local device. To add it to your project you just need to add JitPack to your root build.gradle (see above) and this to your app level build.gradle:

build.gradle (app-level)

1                dependencies {

2                            com.github.codekidX:storage-chooser:2.0.4.4

3                }

It all worked great for years, but eventually, the author stopped making updates, and then Android 11 came along. As many developers know, Android 11 changed how apps can access storage, so no big surprise that this caused problems. In this case, any app using storage-chooser would crash. The good news was that someone fixed it and the changes were sitting on a public fork just waiting to bring harmony back into the world. All we needed was for the author to merge it into the master branch. The bad news was that the author had gone silent for some time and the urgent fix we needed wasn’t coming through.

One option was to just clone the new fork with the required changes, build the Android Library, and keep it in our libs folder. But that didn’t appeal to me for a number of reasons already listed above. Instead, I forked the fork with the Android 11 fix to make it my own. This allowed me to make my own changes that I felt were needed but also to git-tag the latest commit with a new version label. I chose to incrementally bump the version to 2.0.4.4a, that way making the base version clear yet still be different. My intentions here were to provide a new version that is functionally the same as 2.0.4.4 but just works on Android 11. What you tag your own forked fixes is up to you. So now, once I pushed up the changes and tag to my own fork, all I needed to do was make the following changes:

build.gradle (app-level)

1                dependencies {

2                            com.github.mveroukis:storage-chooser:2.0.4.4a

3                }

And that’s it. Did I need to create an account with JitPack? Nope. And the reason for that is I’m not using a private repository, the original fork and my own fork are public, so there’s no need. If you want to use a private repository with JitPack you can, but that’s when you need to create an account. That’s not the use-case we’re dealing with so I won’t get into that here.

Interestingly, other fellow developers made subsequent fixes and improvements and made their own forks. They used the same trick as above, making it easy for all of us to keep up to date with an open-source project that is no longer maintained by the original author.

Pro Tip #1

JitPack has a nifty lookup feature where you can give it the URL to any GitHub repository, and it provides you with a step-by-step guide on how to add that dependency to your Gradle build files. Not only that, but you can also view the build results of all past builds and if it’s failing, this is where you can get the build logs. Check it out at jitpack.io.

Pro Tip #2

Create a free account with JitPack. Doing so allows for extra features such as using private repositories. But just as useful is the ability to link JitPack and your GitHub, Bitbucket, GitLab, Gitee, and possibly other repositories. A clear benefit here is that you can not only see a list of all your repositories in one place, but also view their build status. You also have the option to setup email notifications for build events.

Upgrading Android Gradle Plugin Gotcha

If you’re updating a third-party library that is out of date, you’ll naturally want to update its dependencies along with the Android Gradle Plugin. One gotcha is that Android Gradle Plugin 7.0 requires Java 11. This can cause existing projects to fail on JitPack, but there is a fix.

First, if you encounter this, you’ll notice that the steps above will not produce a happy build. Instead, Android Studio will inform you that it cannot find the dependency. You’ll see something like:

Failed to resolve: com.github.mveroukis:storage-chooser:2.0.4.4a Show in Project Structure dialog Affected Modules: app

Basically, this says that the artifact doesn’t exist, and that’s because JitPack failed to build it. If you look at the build logs on JitPack (see Pro Tip #1 above) you’ll probably see something like this:

1    * What went wrong:

2    A problem occurred evaluating project ‘:{your-package-name-here}’.

3    > Failed to apply plugin ‘com.android.internal.library’.

4       > Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8.

5         You can try some of the following options:

6           – changing the IDE settings.

7           – changing the JAVA_HOME environment variable.

8           – changing `org.gradle.java.home` in `gradle.properties`.

There are a few things you’ll need to do to get around this. Make the following changes to your dependency, commit the changes, tag it as we mentioned above (use a new tag every time) and then push it up to your main branch:

1.     In your dependency’s root folder, add a file named jitpack.yml and add the following contents:

jdk:

– openjdk11

2.     Your dependency’s build.gradle should have the maven-publish plugin added to it. Also, ensure there’s an after evaluate section that specifies what is being published. It should look like this:

1    apply plugin: ‘com.android.library’

2    apply plugin:’maven-publish’

3

4    android {

5        compileSdkVersion 31

6        buildToolsVersion “30.0.3”

7

8        …

9    }

10

11   …

12

13   afterEvaluate {

14       publishing {

15           publications {

16               release(MavenPublication) {

17                   from components.release

18                   groupId = ” com.codekidlabs.storagechooser”

19                   artifactId = ‘ storagechooser’

20                   version = ‘1.0’

21               }

22           }

23       }

24   }

The above adds the maven-publish plugin and indicates which package is being published. Both steps are required for this to work. To read up more on this, check out these links:

·         JitPack: Guide to publishing libraries

·         Google: Use the Maven Publish plugin

·         Gradle: Maven Publish Plugin

 

Conclusion

As you can see, we have some options when dealing with problematic open-source dependencies. What I demonstrated above is not specific to GitHub or JitPack or even Android development. Not only do these tools allow us to keep our codebase and dependencies tidy, but also allow for one other often unappreciated benefit; it allows us to effortlessly give back to the open-source community.

Need help making sense of all this information?

Imaginet is here to help. For over 20 years, Imaginet has helped organizations across the world with their Microsoft technologies. We offer consulting, implementation, migration, customization, custom app development, integration, data analysis, and managed services.

Let's Talk
Michael Veroukis

Michael is a seasoned programmer analyst with a B.Sc. Computer Science and over 20 years of experience. He has an extensive background in mobile development, web development, object oriented programming, relational database development, client-server architecture, UX design and strong problem-solving analysis who works well in team environments.

Let‘s Talk.

Let's talk!