As an Android developer, it’s not very uncommon to find yourself in situations in which you have a hard time running or testing a feature during development because a certain network service is malfunctioning or unavailable.
Another common scenario: you are developing a feature whose testability requires you to login with an account previously registered on an remote service. However, during the development, someone else (who’s sharing access to the same service) deletes / modifies this account, leaving you on an inconsistent state.
Wouldn’t it be great to have access to reliable and predictable services? Or even greater if such services were running somewhere you can control, like your development machine? What about controlling them with simple commands?
Today we’ll talk about how to achieve all of these (and much more to be honest) by using a little bit of Gradle and Docker.
A practical example
Consider a system like the one on the picture below. On the left, we have a simple Android app that allows its user to login / sign up / logout by communicating with a remote service. On the right, we have the same service:
Nothing special so far. Typically, these two components would be separated by the internet: the app would run on a device, and the service would run on a remote host.
However, what’s cool about this picture is that both are running on the same environment (in this case my development machine), thus completely isolated from the external world!
It’s not magic. It’s Docker automated by Gradle.
You can find both app and service implementations on a self-contained project on GitHub:
login-sample - A simple Android app that allows users to sign up / login / logout.
For the rest of this post, we are going to focus on this particular example. First, we’ll point out what’s going on behind the scenes here. Then, we are going to take a look at how it’s being done in detail. Finally, we’ll point out a few things to be considered when applying the concepts presented here in the real world.
Wanna see it working? Clone the repo, run the
darkGreenRealLocalServerDebugapp variant on a device and start / stop the server by running the
:app:stopLocalServertasks (make sure to install Docker first).
What’s going on behind the scenes
Not really comfortable around Docker? You can learn the basics on the Docker docs.
Basically, what we are doing here comes down to defining a couple of Gradle tasks:
:app:startLocalServer: start the service as a Docker container on the local machine
:app:stopLocalServer: stop the service in case it was previously started
Each task should perform a sequence of Docker operations. This is what
:app:startLocalServer should do:
- Operation 1A: pull the service image from Docker Hub
- Operation 1B: create a Docker container from the image
- Operation 1C: start the created container
- Operation 1D: wait for the service application within the started container to be initialized (as we want the service to be fully ready before the task finishes) by checking the container health. This can be done here because our image defines a HEALTHCHECK instruction on its Dockerfile.
:app:stopLocalServer task should clean up all the resources allocated by
:app:startLocalServer. This corresponds to the following:
- Operation 2A: stop the started container
- Operation 2B: remove the stoped container
How it’s being done
Lucky for us, all the operations described on the previous section can be easily implemented by applying the Gradle Docker Plugin:
This is a really simple yet flexible plugin that allows us to wrap Docker operations in Gradle tasks. These tasks can then be serialized by specifying their dependencies.
This makes the implementations pretty straightforward, as each operation is mapped to a single Gradle task.
For instance, this is how we are implementing the
:app:stopLocalServer task is even simpler:
And that’s it. Note that this setup does not require project to be an Android project or even a Java project. It can be done to any kind of Gradle project. The Android part starts now.
By delegating the responsibility of managing Docker containers / images to Gradle, we are now able to start and stop a service instance on the local machine. Now we need to make sure that the app can communicate with this instance.
In this example, we are passing the IP from the local machine to the app at build time via
BuildConfig. Specifically, we are creating a new build flavor called
RealLocalServer and then adding a
SERVICE_URL string build constant pointing to the local machine:
At first, one may think of using the
localhost loopback address to point to the local machine. However, this would only work when the app is running on emulators hosted on the same machine. As we also want to be able to use real devices, we are using the its IP address within the local network, if available. In order to get this value, we are implementing
getLocalIp() as follows (extracted from this post from Jeremie Martinez):
This will return a private IPv4 address as a string, something like
Then, on the app source, we are accessing the generated URL by calling
BuildConfig.SERVICE_URL. This value should then be used whenever implementing any network-related logic. On our example, we are passing it to Fuel (see the code here).
Now we’re good to go. If we run a
RealLocalServer variant from the app on a device (e.g.
darkGreenRealLocalServerDebug), we'll be able to communicate with local service instances started by the
We can also use the
:app:stopLocalServer tasks when testing.
On our example, we have a set instrumented tests. In order for these tests to pass for
RealLocalServer variants, which require a local service instance to be running. This means that we always have to execute
:app:startLocalServer before running such tests and then
:app:stopLocalServer after they finish, so the resources are released.
However, this doesn’t mean that we have to execute these tasks manually. Thanks to Gradle, this can also be automated by using task dependencies:
What we are doing here is:
- Waiting for the Android Gradle plugin to generate the instrumented test tasks. This is necessary because these tasks are not available right away, as the plugin needs to parse the variants defined on the build script.
- Getting all instrumented test tasks from
- For each task, add a dependency to
startLocalServer. This will cause the local service instance to always be started before testing.
- For each task, finalize it with
stopLocalServer. This will cause the local service instance to always be stopped after testing.
Now, whenever we test a variant that depends on a local service instance, Gradle will automatically start / stop this instance. Take a look at what happens when we run
The real world
On our example, the service was available as a lightweight Docker image, which corresponds to the ideal scenario. However, this may not happen on ‘real world’ projects, as:
- Service applications may be too heavy and may take a lot of time to setup / consume a lot of resources, making it impractical to run them locally.
- Service applications may not be available as Docker images or can’t be easily wrapped in Docker images.
In situations like these, you have the following options: