Gradle as task runner in Flutter projects

Monday, 30 December 2024

Photo by Ricardo Ferro on Unsplash

I like Gradle. It’s a great, modern build system (not without problems1), and every Flutter app depends on it to create an Android build of the app.

But that’s where usage of Gradle in Flutter ends, which started to baffle me some time ago. A thing I don’t like about Flutter is that is uses a whole zoo of build systems:

Then there’s a whole category of so called *akes, most of them language-specific: Make, Rake (for Ruby), Cake (for C#). There’s no Dake (for Dart) but I’d say Melos can essentially be considered it. There’s also sidekick. I’ve also seen projects that simply use Bash to automate one-off tasks (causing developers on Windows machines to shed a tear).

I’m going to throw another option to the mix – why not use Gradle as a task runner?

Why?

First: you already most likely use Gradle if you target Android, so why make your development workflow depend on yet another tool?

But honestly, I did it kind of just for the sake of it, and because I like Gradle a lot, and don’t like existing tooling (Melos!) – but if I had to come up with more valid of a reason, I’d say standarization.

Standarization

Standarization on a single tool is usually beneficial, because it enables you to make larger, continuous investments in the tooling and your overall broadly understood development workflow. The build system can be viewed as a platform to, ekhem, build on to improve your productivity, a platform that gives you some useful guarantees about code you write. A good example of this is Bazel, which I’m a also a big fan of, but this is not a blogpost about Bazel. It’s a blogpost about Gradle.

Without standarization you use one-off tools for conceptually-similar but quite different use cases – like 5 different build systems in Flutter and various community-built task runners.

I think if the Flutter team at Google refocused their efforts on proper support for, let’s say, Bazel, it would be a win for both Flutter developers and the Flutter and Bazel projects (so a win for Google itself too, I guess?).

That’s it for the “why”, now let’s move on to the “how”.

Define your tasks in Gradle

Below is a simple Gradle script where 2 custom tasks are defined: optimizeAssets and updateL10n. These are stub implementations, but imagine they do something useful. Save it as build.gradle.kts in your Flutter project’s root directory.

import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL

tasks.register("optimizeAssets") {
    group = "Custom"
    description = "Generates optimized variants of /assets"

    doLast {
        logger.quiet("Generating optimized assets")
    }
}

tasks.register("updateL10n") {
    group = "Custom"
    description = "Updates localization files"

    doLast {
        val url = URL("https://jsonplaceholder.typicode.com/todos/1")
        val conn = url.openConnection() as HttpURLConnection
        conn.requestMethod = "GET"

        BufferedReader(InputStreamReader(conn.inputStream)).use { br ->
            var line: String?
            while (br.readLine().also { line = it } != null) {
                println(line)
            }
        }
    }
}

Let me just say: how awesome is this? Kotlin is by far the nicest programming language I know, and now you can use it to create cross-platform tasks to help with your development process. You also can tap into the huge JVM ecosystem.

Problems and how to fix them

Of course, nothing is this easy in the steaming mess of a reality we live in. Some additional plumbing is required, but fortunately I did it so you don’t have to! Read on.

Running and deduplicating Gradle wrappers

How do we even run this thing?

Of course, you can brew install gradle and then:

$ gradle :optimizeAssets

but it’s not a good idea for all the reasons explained here. The way to go is to use the Gradle wrapper, a.k.a the gradlew script that’s usually checked in into your repository (except it isn’t in Flutter apps. Instead it’s generated by Flutter tool during flutter build apk --config-only. But this doesn’t matter for now).

Simply move the /android/gradlew the project’s root directory, and then symlink to it from /android/gradlew:

$ mv android/gradlew .
$ ln -sf ../gradlew android/gradlew

The same has to be done with the /android/gradle directory:

$ mv android/gradle .
$ ln -s ../gradle android/gradle

If you don’t create the /gradle directory with the wrapper in it, then running /gradlew script will crash with:

$ ./gradlew
Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain
Caused by: java.lang.ClassNotFoundException: org.gradle.wrapper.GradleWrapperMain

Now if you run ./gradlew :optimizeAssets, it will work as expected:

$ ./gradlew :optimizeAssets

> Task :optimizeAssets
Generating optimized assets

BUILD SUCCESSFUL in 778ms
5 actionable tasks: 1 executed, 4 up-to-date

You can also build your Android app:

$ ./gradlew :android:app:assembleDebug

BUILD SUCCESSFUL in 2s
54 actionable tasks: 13 executed, 41 up-to-date

Deduplicate .gradle cache directory

We’re not done yet - if you ran commands above, you probably noticed a .gradle directory being created in your project’s root with a bunch of Gradle-owned cruft inside:

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .gradle/

nothing added to commit but untracked files present (use "git add" to track)

Gradle calls this the “project-specific cache directory”. Default value is .gradle/ in the root project directory. The problem is that we already have one project-specific cache directory in the android/ directory. To fix this and make Gradle always use android/.gradle/ as project-specific cache directory, create a gradle.properties file with the following content:

org.gradle.projectcachedir=./android/.gradle

Update .gitignore

So that your changes will not disappear in void:

  1. Remove gradlew from android/.gitignore
  2. Add .gradle/ to .gitignore

Parting words

I flutter created a simple app and made a PR that demonstrates all the steps above. Here it is.

I’ve always enjoyed Flutter, and even though I no longer work as an app developer, I still like the tech a lot and continue contributing to it. Unfortunately this means I don’t really have the opportunity to test this idea of using Gradle as task runner in Flutter. So if this idea piqued your interest, I’d be glad to learn about your experience!

My small hope is that one day, Flutter will standarize on a single build system+task runner tool for all the platforms that it can target. At the same time I’m afraid this ship has sailed quite some time ago. The whole ecosystem and community is now entrenched in the local maximum of using N build systems, and every project seems to adopt a different task runner of some sorts.

But one may hope.

Another problem is that the Flutter tool has quite strong assumptions about the Gradle project structure and that’s why the workarounds above were required to make it work. It’s possible I missed something and now e.g. caching is terribly broken in some cases. In my simple app sample didn’t notice it. If you do, please let me know.


2024-12-30 – Gradle as task runner in Flutter projects
2024-11-12 – Liminal spaces
2024-08-18 – Ditching ngrok for frp
2024-08-16 – Cirrus CI is the best CI system out there
2024-08-14 – Going to Berlin for Droidcon/Fluttercon
2024-06-25 – I was awarded Google Open Source Peer Bonus
2024-06-04 – My journey to Google I/O ’24
2024-05-11 – GitHub Actions beg for a supply chain attack
2024-03-19 – Writing a custom Dart VM service extension (part 1)
2024-02-08 – On using smartphone for things that make sense
2023-11-30 – Semantics in Flutter - under the hood
2023-11-25 – Flutter Engine notes
2023-09-17 – Creating and managing Android Virtual Devices using the terminal
2023-05-27 – Suckless Android SDK setup
2023-05-26 – Let’s start over
2023-05-21 – Short thought on “The Zen of Unix”
2023-05-15 – Notes about “flutter assemble”
2019-01-07 – Google Code-in 2018


  1. Not so great documentation (I say this after going through it a few times), which is further amplified by Gradle introducing breaking changes every two releases (for good reasons, but still), which causes tons of StackOverflow answers and blogposts to become terribly outdated very fast.↩︎