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-feature
directory. - Next, create a
my-feature/src
directory. It will contain all the Java code of the new feature module. - Finally, under the
my-feature/src
folder createio/morfly/bfa
packages.
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 BUILD
file.
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/bfa
directory create a new fileMyFeatureWithRes.java
. - After that, create a
my-feature/res
directory that would hold Android resources. - Finally, under the
my-feature/res/values
directory create astrings.xml
file.
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_files
param.custom_package
— is also required to define a correct Java package that is used forR
class 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/BUILD
file. - Add
//my-feature:my-feature
and//my-feature:my-feature-with-res
asdeps
param to thebin
Bazel 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
visibility
param. - Set default visibility for all targets in the package (declared in the same
BUILD
file). This can be done usingpackage
function by specifyingdefault_visibility
param. 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_library
rule with aload
statement inmy-feature/BUILD
file - Replace
android_library
withjava_library
formy-feature
Bazel 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