Android working on a multi-library project

We will talk about a multi-library project on Android. It’s not something normal, but not something out of the ordinary either. You may have encountered it in your line of work, or you may be looking to break your library into submodules for better structure and organization. No matter the case, you should be well aware of what’s in front of you before you dive in.

Writing your library on Android is good. You have a chance to write some code that can help other developers (or even yourself). Since libraries cannot be a stand-alone project by themselves, they are usually always combined into a project with an application. This allows library development to be a simple process where you add a feature/fix a bug and then test it directly with the app you have in the project. Thus simulating (locally) how a developer will integrate your library.

But what if your library is based on another library you’re developing?

If you are not aware, you should know that a library (read AAR) cannot contain another local library. You can rely on libraries remotely (via dependencies), but not on something local. This is not supported by Android, and while some workarounds emerged over the years (FatAar), these did not always solve the problem and are not up-to-date. There is even a Google Issue Tracker requesting this feature that has been open for quite some time and is getting a lot of attention from the community. But let’s identify which walls we can break down and which ones we can’t.

Imagine your project hierarchy is like this:

So, since InnerLib can’t be part of your original project, where can it reside? And also how could you work locally while developing features in InnerLib?

We will answer these questions in this article.

Git submodule

For most technical problems, there is not always a single solution. Usually, there are more, but each solution has its drawbacks. It’s all a matter of which downsides you’re most comfortable living with at the end of the day.

To answer our first question, where InnerLib might reside, we have several options:

  1. Make InnerLib a submodule of our original project
  2. Make InnerLib its own remote dependency

If you’re not familiar with submodules in Git, the Git documentation is a good place to get familiar with them. Quoting from it (the first paragraph):

“It often happens that while you are working on one project, you have to use another project from within. 👉 Maybe it’s a library developed by a third party or that you’re developing separately and using in multiple parent projects. 👈 A common problem arises in these scenarios: you want to be able to treat the two projects as separate, but still be able to use one from the other.”

shows us that this is exactly our use case. Using a submodule has its advantages. All your code is in one place, easy to manage and easy to develop locally. But submodules have some weak points. One, is the fact that you always have to be aware of which branch your submodule points to. Imagine a scenario where you are on a release branch in your main repository and your submodule is on a feature branch. If you don’t notice, you’re releasing a version of your code with something that isn’t ready for production. wow

Now think about this within a team of developers. A careless mistake can be costly.

If the first option seems problematic, your second option is to host our library in another repository. Setting up the repository is pretty simple, but how do you work locally now?

Working Locally

Now that we have our project set up correctly, we’ll probably have a line like this in our OuterLib build.gradle file:

How can we make the development cycle efficient and easy to work with? If we develop some functions in InnerLib, how do we test things in OuterLib? Or in our application?

One solution that might come up is to import our InnerLib locally into our OuterLib project, while InnerLib .gitignores our OuterLib project. You can easily do this by right-clicking on the project name in the left menu of Android Studio and going to New → Module.

Then, in the window that opens, you can choose the Import option at the bottom left

It seems easy and simple so far, but what’s the problem?

Whenever you modify a file that belongs to InnerLib, the changes will not be reflected in InnerLib as it is ignored. So every change you want to make has to go inside InnerLib and then you have to import it back into OuterLib to see the changes.

That doesn’t seem right. There has to be a better way to do it.

With just a few lines in our settings.gradle file, we can ensure that our files stay in sync when we make changes to InnerLib. When we imported InnerLib into our project, Android Studio made a copy of InnerLib and cached it. That’s why we had to re-import the library for every change we made to it. We can tell Android Studio where to reference the files using the projectDir attribute.

Our settings.gradle might look like this:

To reference our InnerLib locally, we should change settings.gradle to this:

With this approach, our InnerLib files will be linked in our working directory, so any changes we make will be reflected immediately. But, we would like to have flexibility when working locally in OuterLib with a remote version of InnerLib. What we wrote above inside the settings.gradle file will only allow us to work locally and we probably don’t want to commit it as it is.

Local Maven

If the above approach doesn’t quite fit you, you can take a different one. Just as you would publish your library publicly with Maven, you can do the same locally with local Maven. Local Maven is a set of repositories that reside locally on your machine.

Below are the mavenLocal paths based on your machine’s operating system:

  • Mac → /Users/YOUR_USERNAME/.m2
  • Linux → /home/YOUR_USERNAME/.m2
  • Windows → C:\Users\YOUR_USERNAME\.m2

In essence, you can publish your library locally and then link to it in your project. By doing it this way, we can link our project to InnerLib. To enable this setting in our project, we need to do the following:

  1. Add mavenLocal() as a repository inside our repositories clause. This is to allow our project the ability to search repositories locally

2. Change our implementation line inside our dependencies clause to reference our InnerLib as if we were referencing it remotely

3. To publish InnerLib locally, we will create a file called publishingLocally.gradle that will contain the following:

4. Inside the application tier build.gradle file, add the line:

apply from: ‘/.publishingLocally.gradle

If this option seems a little too good to be true, it is. On the one hand, we can develop things locally without problems, as if we were working with a remote library. On the other hand, if we make any changes inside InnerLib while working locally, it needs to be republished locally. Although this is not an expensive task, it does create the need to perform tedious tasks over and over again.

A solution for working locally and remotely

We want to avoid the constant need to republish our InnerLib package whenever we make a change locally. We need to find a way to make our project aware of these changes. In the Working Locally section, we figured out how to do this, but we had a problem with the settings.gradle file commit. To solve this problem so that we can work both locally and remotely with our InnerLib, we will use a parameter that we will define in our file.

The file is a place where you can store project-level settings that configure your development environment. This helps ensure that all developers on a team have a consistent development environment. Some settings you may be familiar with that are inside this file are AndroidX support (android.useAndroidX=true) or JVM arguments (org.gradle.jvmargs=-Xmx1536m). To help us solve our situation, we can add a parameter here to indicate whether we want to work locally or not. Something like:

workingLocally = false

This parameter will give us the ability to distinguish between which parameters we are working, either locally or with production code. First, we modify what we have in our settings.gradle file by wrapping it in a condition that checks if our parameter is true:

This way we tell the project to get our InnerLib files locally from our machine. Another place where we need to change our logic is in our build.gradle file. Here, instead of making the code in our library remotely available in our dependencies block, we can indicate whether we depend on it locally or not.

⚠️ Word of warning: You should never commit the file when working locally.

The journey was long and, for some, very exhausting. But now we have a complete setup to work locally and remotely on a multi-library project.

If you find any problem or want to give your opinion about it, feel free to leave a comment.