How to migrate a simple Android app from Maven to Gradle

Recently Android Studio popped up a notification recommending that one of the projects I am working on be migrated from Maven to Gradle. The build files for that project had initially been crafted by hand and used Maven for build automation.

The project is a simple Android app used for learning purposes. The build has a single apk target and a couple of Maven and jar dependencies (no tests, shame on me). What follows is a description of the migration process from Maven to Gradle.

Why switch to Gradle?

Maven is the default build tool for the Android Development Tools (ADT) on Eclipse, which used to be the IDE of choice for Android development. That is until Google announced Android Studio, an IDE based on IntelliJ, at the Google I/O conference in May 2013.

While Android Studio works just fine with Maven-based projects, Google is pushing developers to switch to Gradle. Presumably they will focus their efforts on integrating Android with Gradle rather than Maven.

Given the simplicity of my project I was quite happy with Maven and didn’t need any Gradle-specific features. But switching early probably pays off in terms of tool support later on.

The original pom.xml

Gradle 1.6 introduced a (beta) feature called Build Setup Plugin which tries to initialize a new Gradle build. If a pom.xml file exists in the same folder it will try to automatically migrate from Maven to Gradle. Unfortunately that didn’t work in my case:

$ gradle setupBuild
[...]
FAILURE: Build failed with an exception.

Now that we’re stuck with manual migration, let’s have a look at the pom.xml file to understand its structure and how to port it to Gradle. Here is the verbatim pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.thoughtworks.healthgraphexplorer</groupId>
    <artifactId>HealthGraphExplorer</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>apk</packaging>
    <name>HealthGraphExplorer</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <platform.version>4.1.1.4</platform.version>
        <android.plugin.version>3.6.0</android.plugin.version>
        <robospice.version>1.4.6</robospice.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.google.android</groupId>
            <artifactId>android</artifactId>
            <version>${platform.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.github.kevinsawicki</groupId>
            <artifactId>http-request</artifactId>
            <version>5.2</version>
        </dependency>
        <dependency>
            <groupId>org.roboguice</groupId>
            <artifactId>roboguice</artifactId>
            <version>2.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>com.octo.android.robospice</groupId>
            <artifactId>robospice</artifactId>
            <version>${robospice.version}</version>
        </dependency>
        <dependency>
            <groupId>com.octo.android.robospice</groupId>
            <artifactId>robospice-retrofit</artifactId>
            <version>${robospice.version}</version>
        </dependency>
        <dependency>
            <!--
                 Install into local repository by running the following:
                 > curl https://raw.github.com/jjoe64/GraphView/master/public/graphview-3.1.jar -o /tmp/graphview-3.1.jar
                 > mvn install:install-file -Dfile=/tmp/graphview-3.1.jar -DgroupId=com.jjoe64 -DartifactId=graphview -Dversion=3.1 -Dpackaging=jar
            -->
            <groupId>com.jjoe64</groupId>
            <artifactId>graphview</artifactId>
            <version>3.1</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                    <artifactId>android-maven-plugin</artifactId>
                    <version>${android.plugin.version}</version>
                    <extensions>true</extensions>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>com.jayway.maven.plugins.android.generation2</groupId>
                <artifactId>android-maven-plugin</artifactId>
                <configuration>
                    <sdk>
                        <platform>16</platform>
                    </sdk>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

The file is basically made up of three parts: a few variables and properties, a list of dependencies and finally some build instructions.

Two things worth pointing out:

  • The build step is magically handled by the third-party plugin com.jayway.maven.plugins.android.generation2. I assume this is what caused gradle setupBuild to fail.

  • There is a dependency on a jar which is not available on Maven Central and must be manually added to the local repository before running the build. This setup seems fragile and prone to failures in case the remote file is deleted or renamed. So let’s make the dependency part of our project in the libs/ folder:

$ mkdir libs
$ curl https://raw.github.com/jjoe64/GraphView/master/public/graphview-3.1.jar -o libs/graphview-3.1.jar

Gradle skeleton

Starting from the Gradle guide for Android results in the following initial build.gradle file:

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.5.+'
    }
}
apply plugin: 'android'

android {
    compileSdkVersion 18
    buildToolsVersion "18.1.0"

    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 16
    }
}

To test the file run gradle clean build. It will complain about the missing SDK and recommends setting sdk.dir. This is solved by specifying the path to the Android SDK in a local.properties file (adjust to your environment, the following should work on Mac OS X):

# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
sdk.dir=/Applications/Android Studio.app/sdk

Alternatively you could set the ANDROID_HOME environment variable, though I haven’t verified how well that plays with Android Studio.

Update the directory structure

At this point you might run into errors like "Main Manifest missing" or "No resource found that matches the given name". They are caused by Gradle expecting a slightly different directory structure than your previous build setup.

There are two options to resolve this: either configure Gradle for your directory structure, or move your project files and folders into the following structure:

.
├── build            # auto-generated
│   └── [...]
├── build.gradle
├── libs             # additional libs
│   └── graphview-3.1.jar
├── local.properties
├── src
│   └── main
│       ├── AndroidManifest.xml
│       ├── java
│       │   └── [...]
│       └── res
│           └── [...]
└── target           # auto-generated
    └── [...]

Note the locations of AndroidManifest.xml and res/. In my case I had to move them to src/main/.

Add the Maven dependencies

Let’s now focus on getting the Maven dependencies into Gradle. Fortunately Gradle is able to fetch from Maven repositories, better yet it knows about Maven Central. We just need to add the following lines to enable that:

repositories {
    mavenCentral()
}

The shortcut form to idenfity Maven artifacts has the following format: "$groupId:$artifactId:$version". You could also go with the more explicit definition but I will use the shortcut identifiers in this article.

Each dependency in the original pom.xml file needs to be converted from the verbose Maven format to a shortcut definition. Note that the dependency on com.google.android should be ignored since it is already provided by the skeleton described earlier.

For example the following dependency in Maven format:

<dependency>
    <groupId>org.roboguice</groupId>
    <artifactId>roboguice</artifactId>
    <version>2.0</version>
</dependency>

becomes "org.roboguice:roboguice:2.0" in Gradle. The dependencies are added to build.gradle with the following block:

dependencies {
    compile (
        'com.github.kevinsawicki:http-request:5.2',
        'org.roboguice:roboguice:2.0',
        'com.google.code.gson:gson:2.2.4',
        'com.squareup.retrofit:retrofit:1.2.2',
        'com.octo.android.robospice:robospice:1.4.7',
        'com.octo.android.robospice:robospice-retrofit:1.4.7',
    )
}

Add the jar dependencies

As mentioned earlier there is also a dependency on a jar file in folder libs/. This dependency must be added explicitly as well using the following snippet instead of an artifact identifier: files("$path_to_jar").

Adding the jar file to the end of the list of compile dependencies in build.gradle yields the following block:

dependencies {
    compile (
        'com.github.kevinsawicki:http-request:5.2',
        'org.roboguice:roboguice:2.0',
        'com.google.code.gson:gson:2.2.4',
        'com.squareup.retrofit:retrofit:1.2.2',
        'com.octo.android.robospice:robospice:1.4.7',
        'com.octo.android.robospice:robospice-retrofit:1.4.7',
        files("libs/graphview-3.1.jar"),
    )
}

Note that compile dependencies are also automatically added as runtime dependencies. There is no need to list them explicitly. The Maven and jar dependencies will automatically be part of the final apk artifact.

It’s alive!

Putting it all together results in the following build.gradle file:

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.5.+'
    }
}
apply plugin: 'android'

android {
    compileSdkVersion 18
    buildToolsVersion "18.1.0"

    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 16
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile (
        'com.github.kevinsawicki:http-request:5.2',
        'org.roboguice:roboguice:2.0',
        'com.google.code.gson:gson:2.2.4',
        'com.squareup.retrofit:retrofit:1.2.2',
        'com.octo.android.robospice:robospice:1.4.7',
        'com.octo.android.robospice:robospice-retrofit:1.4.7',
        files("libs/graphview-3.1.jar"),
    )
}

which finally builds successfully:

$ gradle clean build
:clean
:preBuild UP-TO-DATE
:preDebugBuild UP-TO-DATE
[...]
:assembleRelease
:assemble
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Total time: 44.86 secs

At this point the old pom.xml can safely be removed. Restart Android Studio, give it a few minutes to refresh and compile. You should now see your project compiling fine, including libraries and Maven dependencies.

Have fun!


More posts here.

Comments