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...
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.
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.
Step 2. Create the 'commonlib' library.
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.
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
Put everything except
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
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.
Now go to the 'app1' Git repository page. Click the "Git X-Modules" button on the sidebar.
Now click "Add Module" to add 'commonlib' to the project.
Choose the 'commonlib' repository.
And the 'master' branch.
Make sure "This Repository Path" is 'commonlib'. It's the path in 'app1' where the 'commonlib' repository will be inserted.
Click "Add Module" and apply changes. Now 'app1' Git repository has 'commonlib' directory with 'commonlib' Git repository inserted there.
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.
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
Step 6. Insert 'commonlib' into the 'app2' repository using Git X-Modules.
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
settings.gradle' to include ':commonlib' subdirectory.
rootProject.name = "App2"
app/build.gradle to add:
implementation project(path: ':commonlib')
to the dependencies list.
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'.
Step 9. Test changing 'commonlib'.
git commit -a -m "'commonlib' updated"
git push origin master
rm -rf app1
git clone http://example.org/scm/android/app1.git app1/