In the prior chapters, you used Bazel to develop functionality that would most likely be deployed to some backend server. However, Bazel can be (and is) used for more than just backend projects, able to handle mobile clients as well. In this chapter, we will explore using Bazel to build Android applications.
Setup
Workspace
As you might have already guessed, we need to augment our WORKSPACE file with an additional dependency. In this case, we are specifying the rules for building an Android project.
Modifying the WORKSPACE file for the Android rules
Note
In earlier chapters, we mentioned that there are certain rules that you get out of the box (e.g., java_library, cc_library, etc.). The rules for android_library and android_binary technically started as more out-of-the-box rules. However, as Bazel has matured, they have begun removing rules from this core set and creating explicit packages for these rules, for the sake of maintainability.
This should make sense from a development perspective; by making the rules more modular and less part of a monolithic package, they can be revised at their appropriate pace without requiring a rebuild of the entire Bazel system.
Within the WORKSPACE file, we are calling a rule android_sdk_repository. This is used to specify the path to the Android SDK with respect to this project. In this particular case, without any other additional specification within the rule itself, the rule defaults to using the ANDROID_HOME environment variable. We will set this shortly.
Note
The reliance on an environment variable in this case might set off some alarms, given Bazel’s approach to maintaining control over its dependencies. In this particular case, the environment variable may be considered a convenience; however, it is possible to specify an explicit path to the Android SDKs, even relative to your current WORKSPACE file, within the android_sdk_repository rule.
This kind of specification is highly useful when you are checking Android SDK dependencies into source control. This once again returns Bazel to the type of hermetic build that it prefers.
Android Studio
In the course of this chapter, we are downloading Android Studio for the purpose of giving us an easy Android emulator on which to run our work as well as create a convenient location for the Android SDKs. Technically speaking, you can actually use the work you’ve done with Bazel to drive an Android Studio (and IntelliJ) project (akin to using something gradle). However, this integration is outside of the scope of this chapter of this book.
Note
To that end, you might either have a favorite Android emulator you prefer instead or want to just hook up your Android device to the computer to try this out. In these cases, you can largely skip using Android Studio (though please take particular care to make sure you have the Android SDKs you need as specified under the upcoming Environment section).
Go to https://developer.android.com/studio and follow the instructions for downloading and installing Android Studio on your particular platform.
The version of Android Studio used for the examples in this book is 3.4.2. Later versions may have visual changes, so the screenshots may not line up precisely.
Environment
As indicated previously, we need to set the ANDROID_HOME environment variable in order to ensure that Bazel knows where to find the Android SDKs. Make sure to set your ANDROID_HOME variable in your environment with the value pointing to the location of the Android SDK on your machine.
The default locations for the Android SDKs from Android Studio depends on the particular OS. Set the environment variables as appropriate to your system:
One thing to note is that the preceding commands will only set the environment variable for the lifetime of your particular console session. You may want to consider making these a part of the default profile for your particular console and OS.
Downloading SDKs
Creating the Emulator
Creating the Android Echo Client in Bazel
Having done all the preparation, we are now ready to start actually creating our Android application. In this case, we are going to create a mobile version of our echo client.
However, before we get to the actual gRPC code, we’ll start with a simple shell version that just echoes locally (i.e., input text returns immediately). This is simply to get used to the basics of developing an Android application in Bazel.
Creating a basic Android echo client
Save the file to EchoClientMainActivity.java. As evidenced in the preceding code, we are creating a simple text editor to input text; upon the push of the button, the text is then reflected in a text view.
Creating the layout file
Save the file to echo_client_main_activity.xml under client/echo_client/android/res/layout.
AndroidManifest.xml
Save this to client/echo_client/android/AndroidManifest.xml.
Creating the BUILD file for the Android client
Save this to client/echo_client/android/BUILD.
Although the Android build rules are new, you have seen this pattern several times throughout this book. As expected, we needed to explicitly load the android_library and android_binary rules from the rules_android package.
- manifest
Points at the AndroidManifest.xml file.
Required for both android_library and android_binary.
- resource_files
Contains the set of Android resource files (e.g., layout.xml, strings.xml, etc.).
Strictly speaking, this is optional; however, in this particular example, removing this attribute here will cause the build to fail (since it no longer depends upon the files required for the generated classes).
- custom_package
This explicitly specifies the package used by the app.
Both android_library and android_binary have an expectation about the directory structure.
Specifically, they expect that the directory starts with either java or javatests as a way of inferring the Java package.
If the directory does not start with java, then it is necessary to explicitly set the custom_package attribute.
Now, technically speaking, we did not strictly need to have both an android_library instance and an android_binary instance. The android_binary rule actually has a sufficient set of options that we could have put everything (i.e., Java sources and resource files) we needed there. However, the separation here is intended to illustrate both rules.
Congratulations! You have built your first Android application using Bazel. We are now ready to test it out on the emulator.
Starting Up the Android Emulator Instance
Bazel Mobile Install
Having built and set up our emulator, we are now ready to deploy our application. On first blush, we could make use of the Android Debug Bridge (ADB) commands to get our application onto the emulator. However, Bazel provides a very useful command mobile-install. In a similar fashion to run, mobile-install builds the target application and then installs onto the connected device (in this case, the emulator).
As an added bonus, we can also add the option start_app in order to immediately start the app as soon as it is installed. Using this option makes mobile-install a functional mobile equivalent to run used in prior chapters.
Congratulations! Your application is alive and will locally echo whatever you write into the text editor.
Note
The mobile-install command is not just a convenient proxy for the ADB functionality. Since it is tied into the Bazel build system, it can also be used for deploying incremental changes to your mobile applications. This can greatly improve your development times, allowing you to take a more iterative approach to working on various aspects of the mobile application (e.g., the UI). However, the particulars around using this incremental deployment are outside of the scope of this book.
Adding gRPC Support
While the prior example was useful for creating a basic Android application, it is not a very functional app. Carrying over our theme from prior chapters, let’s now add the real echo client functionality to this application. Fortunately, you have already done the heavy lifting in prior chapters; we get to reuse that here.
Before we jump into the Android code itself, we have to first make sure we add the appropriate permission; otherwise, we will get errors when we attempt to perform interprocess communication.
Adding permission to access networking to the Android app
Save the changes to AndroidManifest.xml.
Modifying the Android client to support gRPC
Save the changes to EchoClientMainActivity.java .
Note
A careful reader will note that we have changed the address from localhost to 10.0.2.2. Within the emulator localhost refers to the emulated device, rather than the machine the emulator is running on. Android Studio designates a special IP address (i.e., 10.0.2.2) in order to connect to processes running on your development machine. You don’t need to change anything on the server side.
Should one decide to use a different emulation platform, please consult with that platform’s documentation to see if it designates a different address (or uses a different strategy) in order to connect to processes running on your development machine.
All of the added code should look familiar to you; with only a couple of changes, it is identical to the code on the command line Java client from the previous chapter.
Note
In both the last chapter and this one, we are making use of the blocking stub version for calling into the backend. Although convenient for us here, in general, you would not want to make such an I/O call on a UI thread, since such a call could make the UI unresponsive. Instead, you likely would want to investigate some of the other generated versions (e.g., using a Future) of this call as a best fit for your application.
Adding dependencies into the Android BUILD file
Note
Once again, an astute reader will see that we have replaced our former @io_grpc_grpc_java//netty runtime dependency with the @io_grpc_grpc_java/okhttp static dependency. To be sure, netty is most appropriate for supporting both client and server functionality. OkHttp is designed to be lightweight for client-only, hence its inclusion into Android here.
Save the changes to the BUILD file.
In this particular case, you haven’t done anything wrong; you might have run into this error due to the evolving nature of Bazel. In this case, one of your dependencies has an error due to a deprecated attribute. All is not lost, however, and the highlighted line above gives us an answer as to how to proceed.
If we add in the flag –-incompatible_disable_deprecated_attr_params=false to the build (and mobile-install) command, you can see the issue correct itself.
Strictly speaking, although this works, this should not be considered a permanent solution. When you actually encounter something like this, the correct solution will be to fix the problem in the dependency (possibly submitting a change for it). This also illustrates the advantage of keeping your advantages as local as possible, to make it easier to control your own destiny.
One thing this does illustrate, however, is that Bazel, even in its own rapid evolution, seeks to provide you with “escape valves” to keep development moving forward. While not permanent solutions, these do enable you to more easily transition between versions of the software.
Running the Android Client Against the Backend
Having augmented our Android client to make RPCs against a local server, let’s now start both the server and the client up.
Previously, you needed two console instances, since your client and server were both command line tools. Here, you will be able to use just one.
Congratulations! You have created your Android echo client, communicating with the local echo server.
Final Word
Over the course of the latest chapter, you augmented your knowledge by learning how to build Android applications using Bazel. Furthermore, you were able to take what you have learned in earlier chapters to create an application that communicated with the work you’ve done previously. Although this is clearly a toy example of remote communication, it serves as a seed for larger projects.
Having demonstrated Bazel’s ability to work with building Android project, we will expand in the next chapter to also work on iOS applications.
Exercise – Using Java Lite Protos On Android
Over the course of the last chapter, you were able to make use of your work from earlier chapters to easily add gRPC support to your Android application. One note that was glossed over during the course of this work is that the output of the java_proto_library tends to be rather verbose in terms of sheer number of functions; that is, the number of generated classes, inner classes, static classes, and so on tends to be large. While this is less of an issue for programs that run on servers, this actually starts to become an issue at scale for Android programs. Veterans of writing Android programs are familiar with the dreaded function count within a single DEX file (i.e., 64K functions).
Although you can adopt a multidex strategy to handle this increased number of functions, this can become complicated depending on the version of the Android SDK. Fortunately, at least in the case of the java_proto_library, we have a viable alternative in the java_lite_proto_library. This rule produces proto Java code that has fewer overall features, while still retaining the core functionality that is used most of the time.
Notably, the java_lite_proto_library is (currently) one of those out-of-the-box rules which also requires some outside support (which might give an indication about the migration path for this particular set of rules). The rule is specified nearly identically to the java_proto_library. Protos generated from java_lite_proto_library do not implement all of the functionality of the original version but can be often used as drop-in replacements for the most common use cases (as is the case here).
To that end, you will also need to ensure that the grpc library that you are using also produces code that is compatible with the output of the java_lite_proto_library. Fortunately, our existing rule of java_grpc_library already handles this for us; all you need to do is to add the option flavor = "lite" to the application.
Once you’ve created your java_lite_proto_library and java_grpc_library (with “lite” proto support) instances, redirect your app’s android_library to point at these newly created rules. The way we are using protobufs in this instance means you don’t have to change any of your Android code. You should be able to build and run the code, with no functional change.
However, you should look at the size of your app before/after this change. You should see a significant reduction in size (and if you really want to know the difference, use Android Studio’s profiling tool to see the difference in the number of functions).
For toy projects, this difference will not amount to much; however, as you grow your Android apps to significant sizes using protobufs, you will want to make use of the “lite” versions to help keep your application sizes small and avoid the use of multidex.