Android/iOS cross-platform project setup

Francesco, publicly an Android developer at Novoda, is an undercover multi technologist who likes to hack with all kinds of systems and watches too many TV shows. Clear eyes, full hearts, can't lose.

Is there a good way of setting up a cross-platform Android and iOS project? How can we help developers from different backgrounds contribute as much as possible on other technologies they’re not familiar with? What is the “least effort, maximum gain” set of tools we can use to achieve all of this?


One of the most painful parts of starting the development of a new project is going through the setup of build tools, plugins, helpers, shrinkers, continuous integration, etc. When we have to develop for multiple platforms, the effort is exponentially bigger: achieving feature parity means that developers from those platforms must be able to start together from a common ground, then evolve gradually.

Here at Novoda, we are experimenting with different approaches for cross-platform projects. Read through to make your own idea about the optimal way of doing it.

Photo by clement127

Apples are fruit, androids are robots

For Oddschecker, which we developed for both Android and iOS, we created two separate Github repositories. This setup allowed us to use the most appropriate, known and widespread tools for each platform:

This reflected on a number of different things.

First, we had to create and maintain many Jenkins jobs for each repository: one for the Pull Request Builder, one for the nightly builds and one on the master branch. For Android, we also use the Monkey runner to stress test the applications we develop.

Secondly, having separate projects also tends to discourage cross-platform contributions, which is something we pride ourselves in. Having Android developers check on iOS pull requests (and vice versa), make comments and ask questions helps all of us grow professionally and learn.

One repository to rule them all

The desire to improve cross-platform collaboration and decrease setup times led us to shift our approach towards a single repository that contains both Android and iOS projects.

The root directory would contain a simple README to guide first-time developers through the specific project folders, which then include their own README and structure, just as if they were separate repositories.

This structure has helped us reach the level of cross-platform collaboration we wanted; we currently have a rule that any pull request cannot be approved if at least a “developer from the other side” hasn’t approved it.
Such collaboration is fundamental especially in projects where we want to have a common architecture across platforms. Thanks to the shared knowledge and vocabulary we can discuss concepts and develop components together.

The single repository is also a strong point towards the simplification of the CI setup: less configuration means less mistakes, which leads to improved time management for everyone!

But how can we use a single repository and compact CI configurations for technologies that share very little between them?

Gradle as the Master of Ceremonies

When we started our multi-platform project we immediately asked ourselves what was going to drive the repository build and test commands.

We immediately thought of Gradle as the ideal tool for the job. There are two reasons for the choice. Firstly, we have an extensive and proven knowledge of Gradle. And secondly, iOS doesn’t really have an official configurable command line development tool.

An Android developer hears about iOS dev tools

In case you haven’t heard about it just yet, Gradle is an extensible set of libraries written in Groovy and running on the JVM, which acts as a configuration-based task runner reading configurations from .gradle scripts.

Android and iOS as Gradle modules

We started our configuration process by creating two projects, android and ios, in directories with the same names under the main root folder.

Then, we declared these two directories as Gradle modules by adding a settings.gradle file to include those projects:

include 'android', 'ios'

After that, we just had to create a build.gradle for each platform-specific project.

Plain Gradle for Android, duh

Android projects are Gradle-based by default since 2013, but including them as sub-projects didn’t turn out to be as easy as we imagined. In fact, we required that:

Achieving all of this didn’t prove painless, since we got stuck with relative path errors and inconsistent behaviours with custom plugins. After a few hours spent tweaking and fiddling with the configurations, our project built perfectly.

xcodebuild vs fastlane for Apple

Apple’s tool chain, on the other hand, is heavily tied to the Xcode IDE. The associated command line tool, xcodebuild, can be used to test and archive your app from the shell.
The main weaknesses of xcodebuild is that is not easy to configure a task, and that there is no task manager, so you have to create your own collection of bash scripts to reuse the original xcodebuild command.

According to our experience, xcodebuild is not the best solution to setup and configure our tasks, for this reason we moved to fastlane, a build tool that in the last few years has become the de-facto-standard in iOS development.

fastlane, in fact, has a proper task manager and comes with a great collection of actions that will simplify your life as an iOS developer. Building, testing and deploying are just some of the basic actions, but there are more advanced capabilities. For example you can generate screenshots for the AppStore, setup certificates on the local machine and notify your team with a Slack message.

Actions can be collected in “lanes” that you can “drive” using the fastlane command line. The output of an action is available for the next actions, so you don’t have to worry about passing parameters around; normally you just have to setup your action with the bare minimum amount of parameters in order to make it work. Also, information will be inferred whenever possible, so you don’t have to be super explicit. For instance, you don’t have to specify the workspace if you only have one in your working directory.

Most of the actions available are meant to be used in a CI machine. scan, for example, runs tests for your project and generates a report that can be seamlessly consumed by Jenkins.

Bridging fastlane to Gradle was ironically much easier than making the Android project work consistently, since we simply execute commands on the shell, calling fastlane with the appropriate arguments.

Good enough, ship it?

Overall, we were pretty happy with this setup. However, we weren’t sure about the maintainability of the Android configuration, since it is pretty complex and many code quality plugins rely on relative paths.

Our main problem, though, was related to the Gradle bootstrapping time, which took from the optimistic 5 to the more realistic 10 seconds depending on the machine just to evaluate all projects configurations. This meant that an iOS developer who wants to run the same iOS tests that run on the CI will have to wait for the Android project to be evaluated by Gradle, even if it’s not needed at all!

An iOS developer shocked at Gradle spin-up times

Gradle with no sub-projects

The Gradle evaluation issue led us to re-think the whole setup: do we really need to evaluate the Android and iOS projects right away? As a matter of fact, we don’t.

We removed the root settings.gradle file, which completely annihilated the bootstrap times. To run the platform-specific tasks, we now simply make the root project call another Gradle or fastlane instance in the platform directory:

// Android tasks

task('testAndroid', type: Exec, group: 'verification') {
    this.workingDir = './android'
    commandLine './gradlew', 'clean', 'test'
}

// iOS tasks

def fastlane(lane) {
    return ['bundle', 'exec', 'fastlane', lane]
}

task('buildDependentsApple', type: Exec, group: 'build', description: 'Setup the machine with all the iOS dependencies') {
    this.workingDir = './ios'
    commandLine 'bundle', 'install', '--path', 'vendor/bundle'
}

task('testApple', type: Exec, group: 'verification', dependsOn: 'buildDependentsApple') {
    this.workingDir = './ios'
    commandLine fastlane('test')
}

This setup enables us to save lots of configuration time, since the Android project is evaluated by the Android-specific Gradle wrapper, and only when it is actually called. Of course, we can still run both Android and iOS tests with ./gradlew testAndroid testApple.

Although this project setup might seem a bit silly (all of this can be implemented with Bash scripts, after all!), Gradle gives us a better understanding of task definitions and dependencies, and can be easily extended with future team-level tasks. In fact, we have developed tasks for commit message validation and selective tests: stay up to date on Twitter and Google Plus to know when we open source them!

TL;DR

In our experience with developing the same application for both Android and iOS, we found out that:

About Novoda

We plan, design, and develop the world’s most desirable Android products. Our team’s expertise helps brands like Sony, Motorola, Tesco, Channel4, BBC, and News Corp build fully customized Android devices or simply make their mobile experiences the best on the market. Since 2008, our full in-house teams work from London, Liverpool, Berlin, Barcelona, and NYC.

Let’s get in contact