The recent Flutter 3.29 release introduced many new updates to Impeller, Cupertino widgets, DevTools and more. But one big change flew under the radar: new Flutter projects now use the Kotlin DSL for Gradle files by default.
This has some implications for projects that rely on custom Gradle configurations, such as flavors, code signing, and more.
This article breaks down what changed, how it affects you, and how to avoid common pitfalls.
What's changed?
New projects generated with Flutter 3.29 now have .kts
files instead of .gradle
:
android/build.gradle
→android/build.gradle.kts
android/settings.gradle
→android/settings.gradle.kts
android/app/build.gradle
→android/app/build.gradle.kts
Before you panic: both Groovy (old) and Kotlin (new) files are fully supported.
If your project is using Groovy .gradle
files, you don't need to migrate. Everything will still work with Flutter 3.29.
Only brand-new projects start with .kts
files.
That said, if you are working with .kts
, there are a few things you’ll want to know. 👇
The New Kotlin DSL Syntax
Let’s play spot-the-difference between these two snippets:
// Example 1 - is this Groovy or Kotlin?
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"
// Example 2 - is this Groovy or Kotlin?
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")
Both snippets do the same thing, but there are key differences:
- Groovy uses
def
for variables; Kotlin usesval
. - Groovy lets you skip parentheses in method calls; Kotlin requires them.
- Groovy assertions use
assert
; Kotlin usesrequire
.
There are plenty more differences, all neatly documented here:
But what does this actually mean for your project?
Common Gradle Setup Tasks
Here are the typical things you’ll need to adjust:
- Set Android NDK, minSdk, targetSdk, Java version
- Configure code signing
- Add the
com.google.gms.google-services
plugin for apps using Firebase - Set up multiple flavors
Old tutorials and Stack Overflow answers? Mostly outdated now. Here's how to handle it.
1. Setting the Android NDK, minSdk, targetSdk, Java Version
Same settings as before—just with the correct Kotlin syntax (assignment operator, double quotes for string literals):
...
android {
namespace = "com.codewithandrea.flutter_ship_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973" // Updated
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 // Updated
targetCompatibility = JavaVersion.VERSION_17 // Updated
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString() // Updated
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.codewithandrea.flutter_ship_app"
minSdk = 21 // Updated
targetSdk = 34 // Updated
versionCode = flutter.versionCode
versionName = flutter.versionName
}
...
}
...
Pro Tip: updating all these values manually is a pain, and I do it so often that I created a script to automate the process. Here's how it works:
Download it here and add it to your project:
2. Configuring Code Signing
The official Flutter docs about building and releasing an Android app are already updated for the new Kotlin DSL.
Key steps:
Here's a sample code showing the required changes to the android/app/build.gradle.kts
file:
import java.io.FileInputStream
import java.util.Properties
plugins {
...
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
// ...
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = signingConfigs.getByName("release")
}
}
...
}
3. Adding the com.google.gms.google-services plugin (Firebase)
This is handled automatically when you add Firebase to your app with the FlutterFire CLI.
Resulting settings.gradle.kts
:
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
The same plugin will also be listed in android/app/build.gradle.kts
:
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
4. Adding Support for Multiple Flavors
This one is a bit tricky as it requires defining separate product flavors for your app.
The Flutter Flavorizr plugin already handles this for you when you run these processors:
dart run flutter_flavorizr -p android:buildGradle,android:flavorizrGradle,android:androidManifest
If you're doing it manually, open android/app/build.gradle.kts
and add this line at the very bottom:
apply { from("flavors.gradle.kts") }
Then, create a android/app/flavors.gradle.kts
file with these contents:
import com.android.build.gradle.AppExtension
val android = project.extensions.getByType(AppExtension::class.java)
android.apply {
flavorDimensions("flavor-type")
productFlavors {
create("dev") {
dimension = "flavor-type"
applicationId = "com.codewithandrea.flutter_ship_app.dev"
resValue(type = "string", name = "app_name", value = "Flutter Ship Dev")
}
create("stg") {
dimension = "flavor-type"
applicationId = "com.codewithandrea.flutter_ship_app.stg"
resValue(type = "string", name = "app_name", value = "Flutter Ship Stg")
}
create("prod") {
dimension = "flavor-type"
applicationId = "com.codewithandrea.flutter_ship_app"
resValue(type = "string", name = "app_name", value = "Flutter Ship")
}
}
}
Notes:
- Make sure the flavor names match your
flutter run --flavor [name]
command. - Update the
applicationId
and app name for each flavor. - In
AndroidManifest.xml
, set the app label dynamically in theandroid:label
attribute:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="@string/app_name" <-- updated
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
...
</application>
...
</manifest>
There's much more to adding flavors than what I've shown here. For a full guide, check out my Flutter in Production course.
What If You Need to Recreate Your Android Project?
Old Flutter apps sometimes break on new Android tooling. If you’re stuck, it’s often easier to nuke and recreate the Android folder:
# Commit to git before making any changes
git add . && git commit -m "Working copy"
# Delete android folder
rm -rf android
# Create it again with the Flutter CLI
flutter create . --platforms android --org com.yourorgname
# See what's changed
git diff android
# Reapply previous settings, following steps above
# Run again
flutter run
# All good? Commit to git
git add . && git commit -m "Updated Android project"
When you recreate the Android project from scratch, any old settings you had applied will also be lost. Always run
git diff android
to see what's changed, and selectively restore any custom settings as needed.
Conclusion
Starting with Flutter 3.29, new projects use the Kotlin DSL for Gradle.
Honestly? I see little real benefit over Groovy. Yet, our tech overlords have decided this is the way, and we have to keep up and learn a new syntax. 🤷♂️
My advice:
- If you're not forced to migrate, stick with Groovy.
- If you start fresh or need Kotlin DSL, this guide has you covered.
Need more help shipping your app? Keep reading.
Flutter in Production
Shipping and maintaining apps in production takes more than just writing code:
- Preparing for release: splash screens, flavors, environments, error reporting, analytics, force update, privacy, T&Cs.
- App Submissions: metadata, screenshots, compliance, testing vs distribution tracks, dealing with rejections.
- Release automation: CI workflows, environment variables, custom build steps, code signing, uploading to the stores.
- Post-release: monitoring crashes, fixing bugs, OTA updates, feature flags & A/B testing.
My latest course will help you ship faster and with fewer headaches. Learn more here. 👇