Develop an Android library together with apps using it

Develop an Android library together with apps using it

Imagine that you are developing a taxi service like Uber, with two separate apps (one for passengers, another for drivers). However, they are might be using the same shared libraries (e.g. for network protocols or UI animation), and both teams need access to it. Did you know, that monorepo is not the only way to handle this?

This post describes a way to organize sources of 2 Android apps ('app1' and 'app2') using the same common library ('commonlib'). The configuration will allow one to work on the library sources together with both apps.

Why do you need this? A common approach would be to put the common library to the Maven and to use it as a binary dependency for each of the apps. This approach makes it difficult to quickly test changes in the library code: to do that one has to

  • fix the 'commonlib',
  • build and upload changes to the Maven (often this is done by CI and involves runnings tests),
  • obtain the updated artifact from the 'app1' or 'app2' project,
  • and, finally, build and re-run the app to see what's different now.

If the changes do not work as intended, one has to repeat.

Another approach would be to put the 'commonlib', code into the same repository and project as 'app1' or 'app2'. With just one app that would have worked. But with two or more apps it would be an obvious code duplication and therefore not an option.

Finally, there's the "monorepo" approach: put all code - ('app1', 'app2', and 'commonlib') into the same Git repository and manage it together. But, besides other well-known drawbacks of the monorepo approach, it's the best practice to place all code components inside one project directory - yet the 'commonlib' can't be inside the same directory with 'app1' and 'app2' at the same time

The solution sketch. We will adopt a modification of the second approach because it's natural to the Android Studio. When you create an Android library in the Android Studio, it puts the library as another module into the same app project.

Android app projects are usually built with Gradle. They are organized as Gradle projects containing Gradle modules. One of the modules is the app module itself, while libraries it uses could be added as separate modules (together with their sources if developed simultaneously with the app itself) or as binary Maven dependencies (and then one cannot change their sources on the fly).

In our example, we will create 2 projects: 'app1' and 'app2'. Each of them would contain an app-related module (named 'app' by default) and a 'commonlib' module with the sources of the library. We just need to make sure that the 'commonlib' module of both 'app1' and 'app2' projects uses the same sources.

A possible solution would be to put the 'commonlib' module into a separate Git repository and insert it as a Git submodule into the other two Git repositories: 'app1' and 'app2' (corresponding to Gradle projects with the same names). But you know...

02-submodules-tweets|690x250

Git submodules have too many disadvantages.

So we will use Git X-Modules instead. This is a drop-in replacement for Git submodules, working at the server's side. From the Git perspective, they are just regular directories. 29-structure|690x486

Infrastructure. For the purpose of this post I will use Atlassian Bitbucket Server/Data Center as Git server software. It's one of the most popular self-hosted Git solutions and Git X-Modules has a dedicated app with a nice UI for it. Yet the same solution would also work for almost any other Git server software - there's a command-line version of it with the same capabilities, just without the GUI.

I'm using Android Studio 4.4.1 and Gradle 6.7.1. Both are the latest versions to date.

Step 1. Create the 'app1' project.

Create a new project, choose "Basic Activity" as the template. 01-choose-template|656x500 Choose the name of the app (App1), click Finish. 02-choose-app1-name|663x500 Run the project to make sure everything works smoothly. 03-make-sure-app1-can-be-built|690x461

Step 2. Create the 'commonlib' library.

Choose File | New | New Module... 04-create-new-module|465x499 Choose "Android library" as the module type. 05-choose-module-type-library|663x500 Choose the module name: 'commonlib'. 06-choose-commonlib-name|664x500 Create CommonLib class in the 'commonlib' module with a method. 07-create-commonlib-class|616x500

Add a dependency for the default ('app') module on the 'commonlib' module and use the method in the application. 08-use-commonlib-in-app1|689x396 Run the application to make sure everything works smoothly. 09-app1-with-commonlib-emulator|235x500

Step 3. Create 'app1' and 'commonlib' Git repositories.

The 'App1' project now has the following structure:

├── app
├── build.gradle
├── commonlib     <--- this should go to commonlib.git
├── gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle

Now we will create the 'commonlib' Git repository and put the 'commonlib' subdirectory there. All other files (except those that shouldn't be in Git at all) will go to the 'app1' Git repository. After that, the 'commonlib' repository will be inserted into the 'app1' Git repository.

Create the 'commonlib' Git repository using the Atlassian Bitbucket Server/Data Center interface. 10-create-commonlib-repository|658x500 Copy the Git URL of this newly created repository, e.g.

http://example.org/scm/android/commonlib.git

The simplest way to push 'commonlib' content (except the 'build' directory) to this Git repository is the following:

cd commonlib/
git init .
$ git add src
git add build.gradle 
git add consumer-rules.pro
git add proguard-rules.pro
git commit -m "Initial."
git push http://example.org/scm/android/commonlib.git master

11-commonlib-repository-toc|541x500 Now create the 'app1' Git repository with Atlassian Bitbucket Server/Data Center. 12-create-app1-repository|657x500 Suppose its URL is

http://example.org/scm/android/app1.git

Put everything except commonlib, app/build, and local.properties to this repository.

cd ..
git init .
git add app/build.gradle 
git add app/libs
git add app/proguard-rules.pro 
git add app/src
git add gradle
git add gradlew
git add gradlew.bat 
git add gradle.properties 
git add build.gradle
git add settings.gradle 
git commit -m "Initial."
git push http://example.org/scm/android/app1.git master

13-app1-repository-toc|512x500

Step 4. Insert 'commonlib' into the 'app1' repository.

We will be using Git X-Modules. It inserts one repository into another on the server's side as Git trees. So, for the Git client, the module will be just a part of a regular Git tree.

If you don't have the Git X-Modules app installed, go to Administration | Find new apps | Search the Marketplace and type "X-Modules" in the Bitbucket Server/Data Center UI to install this app. 07-install-x-modules-app|690x203 Now go to the 'app1' Git repository page. Click the "Git X-Modules" button on the sidebar. 09-x-modules-button|690x181 Now click "Add Module" to add 'commonlib' to the project. 11-x-modules-add-first-module|690x275 Choose the 'commonlib' repository. 17-x-modules-choose-commonlib|606x500 And the 'master' branch. 18-x-modules-choose-master-branch|690x409 Make sure "This Repository Path" is 'commonlib'. It's the path in 'app1' where the 'commonlib' repository will be inserted. 19-x-modules-apply-changes|690x426 Click "Add Module" and apply changes. Now 'app1' Git repository has 'commonlib' directory with 'commonlib' Git repository inserted there. 19-app1-repository-toc|398x500 Now any team member can clone the 'app1' Git repository, create local.properties, and build it with ./gradlew build. Alternatively one could add local.properties.example into the repository to make it simpler to start working with the project.

Step 5. Create the 'app2' repository.

In the same way as with 'App1', create the 'App2' application project. 01-choose-template|656x500 21-choose-app2-name|656x500 Create the 'app2' Git repository using Bitbucket Server/Data Center UI. 22-create-app2-repository|658x500 Put 'App1' project content to the 'app1' Git repository.

cd app2
git init .
git add app/
git add build.gradle
git add gradle/
git add gradle. properties
git add gradlew
git add gradlew.bat
git add settings.gradle
git add .gitignore
git commit -m "Initial."
git remote set-url origin http://example.org/scm/android/app1.git
git push origin master

http://example.org/scm/android/app2.git is the Git URL of the 'app2' repository. 22-app2-repository-toc|426x500

Step 6. Insert 'commonlib' into the 'app2' repository using Git X-Modules.

In a similar way, click the Git X-Modules button on the 'app2' repository page, add the 'commonlib' repository as X-Module to the 'commonlib' directory ("This repository path"). 23-x-modules-app2-preview|690x462 Apply the changes. 23-x-modules-apply-changes|690x432 23-app2-repository-toc|411x500

Step 7. Add dependency for 'app2' on 'commonlib'.

To fetch the changes, run from 'app2':

git remote set-url origin http://example.org/scm/android/app2.git
git pull --rebase

Change `settings.gradle' to include ':commonlib' subdirectory.

include ':app'
include ':commonlib'
rootProject.name = "App2"

Change app/build.gradle to add:

   implementation project(path: ':commonlib')

to the dependencies list. 24-diff|690x460 Commit and push the changes:

git add app/build.gradle
git add settings.gradle
git commit -m "Add dependency on 'commonlib'."
git push origin master

Step 8. Test using 'commonlib' in 'app2'.

Change MainActivity in 'app2' to call CommonLib.helloFromCommonLib(). 25-change-app2|690x313 Run the result in the emulator to see how it works. 26-changed-app2-emulator|233x500 Commit and push the changes.

Step 9. Test changing 'commonlib'.

Change 'commonlib' in the 'app2' repository, 27-update-commonlib|690x395 push the changes, get the changes in 'app1' and make sure it's updated.

git commit -a -m "'commonlib' updated"
git push origin master
cd ..
rm -rf app1
git clone http://example.org/scm/android/app1.git app1/

Run 'app1' to make sure 'commonlib' is automatically updated. 28-updated-commonlib-app1-emulator|233x500 As you may see, the 'commonlib' is now automatically synchronized between both apps.