Private libraries in Android — how to manage them. This post is part of a series on private libraries: Part 1: Private libraries in Android — why you should consider it Part 2: Private libraries in Android — how to manage them In the first part of the series, we talked about the benefits of private libraries. On top of modularizing our code, they […]
This post is part of a series on private libraries:
Part 2: Private libraries in Android — how to manage them
In the first part of the series, we talked about the benefits of private libraries. On top of modularizing our code, they help us build reusable components across different projects/applications. In the second part, I will explain how we manage them in the day-to-day project routine at Deezer.
For this, we will use the Deezer Android libraries as our example. Note that the breakdown and the content of the libraries are not the point of this article. They will only serve as examples to show how we develop and integrate them and how we adapted our processes to handle frequent changes.
We currently have three private libraries for Android projects at Deezer:
For the rest of the article, we will focus on the Design library as the main example.
Like any of our private libraries, the Design library is meant to be used in several applications. Therefore, it is developed as an independent Gradle project, with its own Git repository. This helps us to define clear boundaries for each library’s scope. It enables a good separation of concern and enforces better interface and abstraction.
The Design library is composed of several modules. Each module will produce a JAR or AAR; hence they can be independently loaded in an app. Notice that some modules may depend on other modules from the library, bringing them along when loaded in an app.
Libraries are provided as frozen binary artifacts, so they need versioning. Our versioning convention takes its inspiration from Semantic Versioning (semver) [major].[minor].[patch]
Similar to our applications, the library’s release lifecycle of minor (or more rarely, major) versions follow a “release train” of two weeks. As soon as a library version is released, the library’s master branch becomes the snapshot of the next version. This snapshot will be released as the next version upon release two weeks later. Patches can be made at any time in order to fix a critical bug and do not follow the release train lifecycle.
On each release, the library gets a release note describing all changes from the newer version. Release notes are important in order to keep track of changes in a library. It is quite essential when updating the library in any project/application. To help with this recurring task japicmp generates reports on the differences between two library versions (comparing java class files contained in JAR archives).
In order to make our libraries available to every developer within Deezer, we use a Nexus repository. Nexus is a repository manager that allows us to store, manage, and load library binary artifacts (JAR / AAR). The usage is quite similar to JCenter or MavenCentral, except that Nexus (open source version) is installed on Deezer servers and is reachable only within Deezer.
The publishing process is handled with our Continuous Integration server (Jenkins in our case). A dedicated job takes care of building the library and uploading it to the Nexus. The job is triggered automatically every night on the snapshot of the Design library to embed changes made during the day. It is also used to build release versions of the library, being triggered manually in this case.
Furthermore, we can also publish the Design library locally on our machine, on a local maven repository. This is often used to test the impact of some changes from the library on an application. To achieve this, the library’s package name is overridden with the suffix _local when publishing the library locally (e.g.,com.deezer.design_local instead of com.deezer.design). Then, the app also adds the suffix _local to the name of the Design library dependency, forcing it to switch to the locally generated library. To keep things clear and maintainable, this override is done in a separate Gradle file dependency_local_overrides.gradle:
Main development branches on applications always target a released version of the Design library. It should not target the library’s current snapshot, since, by definition, the snapshot is considered unstable and can be subject to API changes. However, we sometimes need to start integrating some changes in the library that are only on the snapshot, into an application.
Let’s consider an example. Our last released version of the Design library is 2.2.0. New icons need to be added in the Design System and can be required in a feature on the Deezer application.
We add these icons in the Design library, but at first, they are only available on the snapshot (that will become the release 2.3.0 some point). The issue here is that our develop branch in the app is targeting Design library 2.2.0, meaning we don’t have access to the new icons. To address this situation, we define a specific branch in the app, targeting the snapshot version of the library, which we call design-snapshot. The whole process would be as follows:
This way we can develop new features on the app based on the last change from the Design library snapshot without impacting the main development branch from the app.
Documenting a library is an important aspect not to be neglected. Without any documentation, it is very difficult to know what the purpose of the library is, what features it provides, and how to integrate it. A proper README on the library (and on each module) is a good way to address these points.
However, sometimes concrete examples can even be more powerful than words. For example, a Playground app within the library can be used as a showcase of the library to list and demonstrate the features it provides.
Furthermore, a Sample app can be very helpful to show how to integrate the library into an application. It can complement documentation as a living code example. We currently don’t have a Sample app for our Design library but we have one for our Core library that shows how to integrate the player as well as other features.
The processes described in this article allow smooth updates and integration of a library. However, it becomes more complicated when dealing with multiple libraries. Currently, each of our libraries are managed with their own repository as an independent project. Private libraries sometimes depend on other ones, although we try to make them as independent as possible. This can cause multiple issues:
We are currently investigating the possibility of putting all of our libraries under the same Git repository. This could allow us to manage these dependencies more easily while keeping them independent.
Private libraries help us develop multiple applications at a fast pace by allowing us to reuse components and features while ensuring consistency of behaviors among all Deezer applications.
In this article, we have shown how we manage our private libraries at Deezer with the example of the Design libraries. Each library is an independent project with its own repository. CI jobs make the release process partially automated — handling build and publication on the Nexus repository — allowing frequent releases. Each release is documented with release notes. App’s main development branches always target a released version of libraries, while branches dedicated to snapshots allow the integration of changes from a not-yet-released library version. Finally, documentation plays a key part in library usage and integration. It is achieved with proper READMEs, but also with Playground and Sample applications.
If you would like to help us build and deliver the best experience on Deezer, take a look at our open positions and join one of our teams!
Private libraries in Android — how to manage them. was originally published in Deezer I/O on Medium, where people are continuing the conversation by highlighting and responding to this story.