Building Android apps with Bazel build system
Bazel for Android — is a series of blog posts that shows the basics of building Android projects with the Bazel build system.
- Part 1 — Getting started
- Part 2 — Multi-module projects ← you are here
- Part 3 — Using Kotlin (coming soon)
- Part 4 — Adding external dependencies (coming soon)
- Part 5 — Running tests (coming soon)
In this part, we will see how to build multi-module applications with Bazel. Many Android apps reach the point where a single-module architecture is no longer able to bear with constantly increasing complexity and thus, more source code separation is required. Splitting large applications into modules makes them scalable and features become easier to develop and maintain.
We are going to see how to create new Android and JVM modules from scratch and build them with Bazel. All the source code in this blog post is built on top of the project created in the previous part.
You can find the source code on GitHub:
Creating a new module
Let’s see how to create a new Bazel module from scratch. This will be an Android library module that contains the functionality of a specific feature of the app. In the context of this blog post, the contents of the feature do not really matter. For simplicity of demonstration, it will consist just of one class that returns some information about the feature.
Now we need to open a project that was created in the previous part of this series. We will need to create a couple of directories to set up a file structure for our new feature module:
- In the root project directory, we need to create a
my-featuredirectory. - Next, create a
my-feature/srcdirectory. It will contain all the Java code of the new feature module. - Finally, under the
my-feature/srcfolder createio/morfly/bfapackages.
After the steps above, the project folder structure looks like this:
bazel-for-android ├── app │ ├── my-feature │ └── src │ └── io │ └── morfly │ └── bfa │ └── WORKSPACE
Now, we are ready to write some code.
Under a my-feature/src/io/morfly/bfa directory create a MyFeature.java file and fill it with the following source code:
| package io.morfly.bfa; | |
| public class MyFeature { | |
| public String getInfo() { | |
| return "🦑 My feature"; | |
| } | |
| } |
MyFeature class contents
Next, in the my-feature directory create a file named BUILD. It will contain all the build configurations for the new feature module. Add there the code below:
| load("@rules_android//android:rules.bzl", "android_library") | |
| android_library( | |
| name = "my-feature", | |
| srcs = ["src/io/morfly/bfa/MyFeature.java"], | |
| ) |
my-feature/BUILD file contents
We are using android_library Bazel rule to create a target that builds our module. In the simplest use case, it only has to have a unique name and list of source files (srcs) specified.
That’s it. Now we are ready to build it. To do it, run the command below in your terminal:
bazelisk build //my-feature:my-feature
If all is done correctly, the build should be successfully completed. Below is the file structure of our project so far.
bazel-for-android ├── app │ ├── my-feature │ ├── src │ │ └── io/morfly/bfa │ │ └── MyFeature.java │ │ │ └── BUILD │ └── WORKSPACE
Multiple modules in a single BUILD file
Bazel is flexible enough to let you define multiple modules in the same BUILDfile.
Let’s say we want to define another feature that does the same as the previous one, but for the sake of example, uses resources from strings.xml file instead of a hardcoded string.
We can define a new MyFeatureWithRes class that would refer to R.string.my_feature_info resource string. To do this:
- Under the
my-feature/src/io/morfly/bfadirectory create a new fileMyFeatureWithRes.java. - After that, create a
my-feature/resdirectory that would hold Android resources. - Finally, under the
my-feature/res/valuesdirectory create astrings.xmlfile.
You can find the contents of MyFeatureWithRes.java and strings.xml files below.
| package io.morfly.bfa; | |
| import android.content.Context; | |
| public class MyFeatureWithRes { | |
| public String getInfo(Context context) { | |
| return context.getString(R.string.my_feature_info); | |
| } | |
| } |
| <?xml version="1.0" encoding="utf-8"?> | |
| <resources> | |
| <string name="my_feature_info">🥞 My feature with resources</string> | |
| </resources> |
MyFeatureWithRes class that uses string resources
Now we need to configure this module with Bazel. Open my-feature/BUILD file and add there a new my-feature-with-res target as shown below:
| load("@rules_android//android:rules.bzl", "android_library") | |
| android_library( | |
| name = "my-feature", | |
| ... | |
| ) | |
| android_library( | |
| name = "my-feature-with-res", | |
| srcs = ["src/io/morfly/bfa/MyFeatureWithRes.java"], | |
| resource_files = ["res/values/strings.xml"], | |
| manifest = "AndroidManifest.xml", | |
| custom_package = "io.morfly.bfa", | |
| ) |
Adding another target to the same my-feature/BUILD file
Job Offers
As you can see, this target has more params to configure. In addition to name and srcs it has:
resource_files— that defines Android resources to be used in the module.manifest— which is mandatory when definingresource_filesparam.custom_package— is also required to define a correct Java package that is used forRclass generation.
Finally, we need to actually create a manifest file that we’ve just specified. Under my-feature directory create AndroidManifest.xml file and fill it with the content below.
| <?xml version="1.0" encoding="utf-8"?> | |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
| package="io.morfly.bfa"> | |
| </manifest> |
my-feature/AndroidManifest.xml file content
Now, we are ready to build this module. To do this, run the following command in your terminal:
bazelisk build //my-feature:my-feature-with-res
If all is done correctly, the build should be successfully completed. Below you can find an updated file structure of the project.
bazel-for-android ├── app │ ├── my-feature │ ├── res │ │ └── values │ │ └── strings.xml │ ├── src │ │ └── io/morfly/bfa │ │ ├── MyFeature.java │ │ └── MyFeatureWithRes.java │ │ │ ├── AndroidManifest.xml │ └── BUILD │ └── WORKSPACE
Running the app
Now, that we created brand new feature modules, we need to include them in the application target. To do so, follow these steps:
- Open
app/BUILDfile. - Add
//my-feature:my-featureand//my-feature:my-feature-with-resasdepsparam to thebinBazel target, as shown below.
| load("@rules_android//android:rules.bzl", "android_binary") | |
| android_binary( | |
| name = "bin", | |
| custom_package = "io.morfly.bfa", | |
| manifest = "AndroidManifest.xml", | |
| manifest_values = { | |
| "minSdkVersion": "21", | |
| "targetSdkVersion": "29", | |
| }, | |
| srcs = ["src/io/morfly/bfa/MainActivity.java"], | |
| resource_files = glob(["res/**"]), | |
| deps = [ | |
| "//my-feature:my-feature", | |
| "//my-feature:my-feature-with-res", | |
| ] | |
| ) |
Updated app/BUILD file contents
Now, let’s use our newly created modules in the source code of the app. Open app/src/io/morfly/bfa/MainActivity.java file and update its source code as shown below.
| package io.morfly.bfa; | |
| import android.app.Activity; | |
| import android.os.Bundle; | |
| import android.widget.TextView; | |
| public class MainActivity extends Activity { | |
| // Instaitiate features. | |
| MyFeature myFeature = new MyFeature(); | |
| MyFeatureWithRes myFeatureWithRes = new MyFeatureWithRes(); | |
| @Override | |
| protected void onCreate(Bundle savedInstanceState) { | |
| super.onCreate(savedInstanceState); | |
| setContentView(R.layout.activity_main); | |
| TextView textView = findViewById(R.id.textView); | |
| // Display info about included features. | |
| String appInfo = textView.getText() | |
| + "\n\nIncluded features:\n" | |
| + myFeature.getInfo() | |
| + "\n" | |
| + myFeatureWithRes.getInfo(this); | |
| textView.setText(appInfo); | |
| } | |
| } |
Updated MainActivity.java file contents
As you can see, we instantiate both of our features and display information about them on a TextView.
Now, let’s run the app and see the result of our work. Run the command below.
bazelisk mobile-install //app:bin --start_app
Don’t worry if the build has failed, as this is expected behavior. Your build actually should fail with these 2 errors:
ERROR: /.../bazel-for-android/app/BUILD:3:15: in android_binary rule //app:bin: target '//my-feature:my-feature' is not visible from target '//app:bin'. Check the visibility declaration of the former target if you think the dependency is legitimate ERROR: /.../bazel-for-android/app/BUILD:3:15: in android_binary rule //app:bin: target '//my-feature:my-feature-with-res' is not visible from target '//app:bin'. Check the visibility declaration of the former target if you think the dependency is legitimate
If we read carefully the error message we can notice that our newly created modules are not visible to our binary target.
This is due to a concept in Bazel called visibility. It controls whether a target can be used (depended on) by targets in other packages. Learn more about it from the official documentation.
By default, all Bazel targets have private visibility unless otherwise is specified. To fix this we need to explicitly make our module targets public. There are two ways to do it:
- Set visibility for each target specifically using
visibilityparam. - Set default visibility for all targets in the package (declared in the same
BUILDfile). This can be done usingpackagefunction by specifyingdefault_visibilityparam. We will go with this approach.
To do so, open my-feature/BUILD file and add the following line right below all load statements.
package(default_visibility = ["//visibility:public"])
The complete source code of a my-feature/BUILD file is shown below.
| load("@rules_android//android:rules.bzl", "android_library") | |
| load("@rules_java//java:defs.bzl", "java_library") | |
| package(default_visibility = ["//visibility:public"]) | |
| java_library( | |
| name = "my-feature", | |
| srcs = ["src/io/morfly/bfa/MyFeature.java"], | |
| ) | |
| android_library( | |
| name = "my-feature-with-res", | |
| srcs = ["src/io/morfly/bfa/MyFeatureWithRes.java"], | |
| resource_files = ["res/values/strings.xml"], | |
| manifest = "AndroidManifest.xml", | |
| custom_package = "io.morfly.bfa", | |
| ) |
Complete contents of my-feature/BUILD file
Now, we are finally ready to build and launch the app:
bazelisk mobile-install //app:bin --start_app
If everything is done correctly, you will see the app successfully launched on your device, listing all included features on the screen. An example of a displayed message is shown below.
“Bazel for Android 🍃
Included features:
🦑 My feature
🥞 My feature with resources”
What about JVM-only modules?
Not all feature modules necessarily need Android SDK. Sometimes it is required to create a plain JVM module. In this case, instead of android_library rule we can use java_library.
If we take a look at my-feature module that we’ve created earlier, you will notice that it does not use any API from Android SDK. So, in this case, we can just turn it into a Java library.
To do so, we must:
- Import
java_libraryrule with aloadstatement inmy-feature/BUILDfile - Replace
android_librarywithjava_libraryformy-featureBazel target.
| load("@rules_java//java:defs.bzl", "java_library") | |
| java_library( | |
| name = "my-feature", | |
| srcs = ["src/io/morfly/bfa/MyFeature.java"], | |
| ) |
Making my-feature a Java-only library
You can find a complete source code of a my-feature/BUILD file below.
| load("@rules_android//android:rules.bzl", "android_library") | |
| load("@rules_java//java:defs.bzl", "java_library") | |
| package(default_visibility = ["//visibility:public"]) | |
| java_library( | |
| name = "my-feature", | |
| srcs = ["src/io/morfly/bfa/MyFeature.java"], | |
| ) | |
| android_library( | |
| name = "my-feature-with-res", | |
| srcs = ["src/io/morfly/bfa/MyFeatureWithRes.java"], | |
| resource_files = ["res/values/strings.xml"], | |
| manifest = "AndroidManifest.xml", | |
| custom_package = "io.morfly.bfa", | |
| ) |
Complete contents of my-feature/BUILD file
Now, we can run the app again and verify that it still builds and launches successfully.
bazelisk mobile-install //app:bin --start_app
Conclusion
In this blog post, we’ve discovered how to create multimodule applications built with Bazel by creating new Android and Java feature modules.
You can find source code on GitHub at the link below.
In the next part, we will see how to use a Kotlin language in Bazel Android projects.
This article was originally published on proandroiddev.com on February 21, 2021



