Gradle as task runner in Flutter projects
Monday, 30 December 2024
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:
- Gradle for Android
- CMake for Linux
- xcodebuild for iOS/macOS
- some Visual Studio build system for Windows (I don’t use it)
- custom tooling for Web
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
.register("optimizeAssets") {
tasks= "Custom"
group = "Generates optimized variants of /assets"
description
{
doLast .quiet("Generating optimized assets")
logger}
}
.register("updateL10n") {
tasks= "Custom"
group = "Updates localization files"
description
{
doLast val url = URL("https://jsonplaceholder.typicode.com/todos/1")
val conn = url.openConnection() as HttpURLConnection
.requestMethod = "GET"
conn
(InputStreamReader(conn.inputStream)).use { br ->
BufferedReadervar line: String?
while (br.readLine().also { line = it } != null) {
(line)
println}
}
}
}
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:
- Remove
gradlew
fromandroid/.gitignore
- Add
.gradle/
to.gitignore
Parting words
I flutter create
d 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
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.↩︎