Where Android and Docker meet

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.

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:

An Android app and a 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:

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 darkGreenRealLocalServerDebug app variant on a device and start / stop the server by running the :app:startLocalServer / :app:stopLocalServer tasks (make sure to install Docker first).

In this example, the service application is available as a Docker image on DockerHub with the tag gabrielhuff/sample-login. It exposes a simple REST API through port 8080.

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:

  1. :app:startLocalServer: start the service as a Docker container on the local machine
  2. :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.

The :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

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:startLocalServer task:

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 "192.168.0.1" or "10.0.0.42".

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 :app:startLocalServer task.

We can also use the :app:startLocalServer and :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:

  1. 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.
  2. Getting all instrumented test tasks from RealLocalServer variants (e.g. connectedDarkBlueGreyRealLocalServerDebug, connectedDarkCyanRealLocalServerDebug, etc).
  3. For each task, add a dependency to startLocalServer. This will cause the local service instance to always be started before testing.
  4. 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 :app:connectedLightIndigoRealLocalServerDebug:

Running Android instrumented tests against a Docker container

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:

  • Create lightweight versions of the services that emulate the same API and make them available as Docker images. To be honest, this is easier than it sounds — we did it with 4 source files by using Spring WebFlux and Reactor (RxJava’s cousin)
  • Just don’t use Docker

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store