Private libraries in Android — how to manage them.
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:
- Core: manages the player, domain models, requests, and caching
- Design: provides the Deezer style guide and shared UI components
- Server-driven UI: implements server-driven User Interface
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.
Versioning and release
Libraries are provided as frozen binary artifacts, so they need versioning. Our versioning convention takes its inspiration from Semantic Versioning (semver) [major].[minor].[patch]
- Major: implies important changes such as raising min supported Android API, or important migrations (e.g., AndroidX)
- Minor: for new features, feature enhancement, or minor bug fixes (unlike Semantic Versioning, we allow API breaking changes here)
- Patch: for critical bug fixes that cannot wait for the next minor version (API breaking changes NOT allowed here)
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:
Library updates integration
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:
- Add the new icons to the Design library (on snapshot)
- Generate a build of the Design library snapshot
- On the app, create a new branch design-snapshot from develop
- Update Design library dependency to use the snapshot instead of version 2.2.0
- Develop new feature in-app on design-snapshot branch
- At the end of the usual release train of 2 weeks, release a new version 2.3.0 of the Design library from a snapshot
- On the app, merge design-snapshot to develop
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.
Remaining challenges with multiple libraries
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:
- Keeping dependencies aligned (i.e. same versions) on all libraries and applications is quite cumbersome. For dependencies such as Kotlin, Android Gradle Plugin, AppCompat, MaterialComponents, etc. this can lead to important issues.
- When a private library depends on a second private library, we may encounter some conflicts when an application depends on these libraries, but with a different version for the second one.
- Finally, on applications integrating multiple private libraries, we may have multiple snapshot branches (each per library). This can possibly lead to major conflicts on these parallel branches.
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.
To sum up
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.