วันพฤหัสบดีที่ 16 ตุลาคม พ.ศ. 2568

Android Build System Practical Guide (with Flutter)

 cr: https://curogom.dev/android-build-system-practical-guide-4ae78c0a36ac

Android Build System Practical Guide (with Flutter)

10 min readJul 8, 2025
Press enter or click to view image in full size

Q. Have you ever experienced your Flutter app suddenly failing to build?

We’ve all been there — an app that built perfectly yesterday suddenly throws build errors today.

“Gradle sync failed”, “Duplicate class found”, “Cannot fit requested classes in a single dex file”… These error messages can leave you feeling helpless.

Gradle and Android SDK version issues are particularly frustrating. Even after scouring Stack Overflow for solutions, if you don’t understand why these problems occur, you’ll encounter them again.

In this article, I’ve compiled the essential Android build system knowledge every Flutter developer needs. Understanding these concepts will enable you to solve over 90% of build errors on your own.

Q. What is Gradle and why is it important?

Gradle is Android’s build tool

Gradle is an automated build tool that converts source code into APK or AAB files. Since Flutter apps are ultimately Android apps, they use Gradle too.

Let’s look at the Android structure in a Flutter project:

android/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin/.../MainActivity.kt
│ │ │ └── res/
│ │ ├── debug/
│ │ └── profile/
│ └── build.gradle # App-level Gradle settings
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties # Gradle version definition
├── build.gradle # Project-level Gradle settings
├── settings.gradle
└── local.properties

Understanding Gradle and AGP (Android Gradle Plugin) versions

This is crucial, yet many developers find it confusing.

  • Gradle: The build tool version itself
  • AGP (Android Gradle Plugin): Plugin version for building Android apps

These two must be compatible!

Check Gradle version in gradle-wrapper.properties:

distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip

Check AGP version in android/build.gradle:

buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0' // AGP version
}
}

Why use Gradle Wrapper?

What’s so special about gradlew (Gradle Wrapper)?

Many developers don’t understand why we use ./gradlew instead of gradle.

Benefits of Gradle Wrapper:

Ensures version consistency

  • All team members use the same Gradle version
  • Prevents “works on my machine” problems

No Gradle installation required

  • Automatically downloads the required Gradle version
  • Works in CI/CD environments without separate installation

Easy version management

  • Just modify the gradle-wrapper.properties file

How to use:

# Mac/Linux
./gradlew clean
./gradlew build
# Windows
gradlew.bat clean
gradlew.bat build
# Wrong way (uses system gradle)
gradle clean # Don't do this!

Upgrading Gradle version

# Upgrade Gradle version through wrapper
# Important: You need to run this command TWICE!
./gradlew wrapper --gradle-version=8.2
./gradlew wrapper --gradle-version=8.2 # Second run
# Or directly modify gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip

Why run it twice? According to Google’s Android developer documentation, to upgrade both Gradle and the Gradle Wrapper itself, you need to run the wrapper command twice. The first run updates the Wrapper itself, and the second run updates Gradle with the new Wrapper.

This ensures everyone who clones the project has the same build environment.

Q. How do I handle Google Play’s API level requirements?

Big changes coming August 31, 2025

Google Play requires yearly target API level increases. Starting August 31, 2025, all new apps and updates must target Android 15 (API level 35) or higher.

Configure in android/app/build.gradle:

android {
compileSdkVersion 35 // SDK used for compilation
    defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21 // Minimum supported Android version
targetSdkVersion 35 // Target Android version (required by Aug 31, 2025)
versionCode 1
versionName "1.0.0"
}
}

Understanding each SDK version

minSdkVersion (Minimum SDK version)

  • Minimum Android version your app can run on
  • Flutter default is 21 (Android 5.0)
  • Lower = more device support, but limited API availability

targetSdkVersion (Target SDK version)

  • Android version your app is tested and optimized for
  • Applies new Android security and performance features
  • Must be updated periodically per Google Play policy

compileSdkVersion (Compile SDK version)

  • Android SDK version used for compilation
  • Usually set equal to or higher than targetSdkVersion
  • Must be raised to use newer APIs

Using Flutter’s automatic SDK version settings

New feature in Flutter 3.16+!

Flutter now automatically sets recommended Android SDK versions for you.

Use in android/app/build.gradle:

android {
// Use Flutter's recommended versions instead of hardcoding
compileSdkVersion flutter.compileSdkVersion
    defaultConfig {
applicationId "com.example.myapp"
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode 1
versionName "1.0.0"
}
}

Benefits

  • Automatically applies latest recommended versions when upgrading Flutter
  • Automatically responds to Google Play policy changes
  • No need to manually manage versions

When manual configuration is needed

android {
// Specify directly if you need specific versions
compileSdkVersion 35
    defaultConfig {
// If you need a lower minimum version
minSdkVersion 19 // Lower than Flutter default
// Set directly for Google Play requirements
targetSdkVersion 35
}
}

Check current Flutter version defaults

# Check Flutter SDK default values
cat $FLUTTER_ROOT/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy | grep -E "(compileSdk|minSdk|targetSdk)"

This way you can use stable SDK versions tested by the Flutter team while still having the flexibility to customize when needed.

Considerations when updating to API 35

Check new permission models

  • Especially changes to notification, location, and photo permissions

Background work restrictions

  • Stricter limitations due to battery optimization

Testing is essential

  • Test on actual Android 15 devices or emulators

Q. How do I use Build Variants and Product Flavors?

For separating development/staging/production environments

Many developers use development and production servers, but manually modifying code for each build is risky.

Add flavors to android/app/build.gradle:

android {
flavorDimensions "environment"
    productFlavors {
dev {
dimension "environment"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
// Define development server URL with buildConfigField
buildConfigField "String", "API_URL", '"https://dev-api.example.com"'
}
staging {
dimension "environment"
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
buildConfigField "String", "API_URL", '"https://staging-api.example.com"'
}
prod {
dimension "environment"
buildConfigField "String", "API_URL", '"https://api.example.com"'
}
}
}

Flavor-specific folder structure and AndroidManifest.xml usage

You can create separate folders for each flavor to configure different resources:

android/app/src/
├── main/
│ ├── AndroidManifest.xml # Base Manifest
│ ├── res/
│ │ ├── mipmap-hdpi/
│ │ │ └── ic_launcher.png
│ │ └── values/
│ │ └── strings.xml
│ └── kotlin/
├── dev/
│ ├── AndroidManifest.xml # Dev-specific Manifest (optional)
│ └── res/
│ ├── mipmap-hdpi/
│ │ └── ic_launcher.png # Dev icon
│ └── values/
│ └── strings.xml # Dev app name
├── staging/
│ └── res/
└── prod/
└── res/

Changing app name per flavor

main/res/values/strings.xml (default):

<resources>
<string name="app_name">My App</string>
</resources>

dev/res/values/strings.xml (development):

<resources>
<string name="app_name">My App (Dev)</string>
</resources>

staging/res/values/strings.xml (staging):

<resources>
<string name="app_name">My App (Staging)</string>
</resources>

Changing icons per flavor

Place different icons in each flavor’s res/mipmap-* directories for automatic application:

  • dev/res/mipmap-hdpi/ic_launcher.png → Development icon
  • staging/res/mipmap-hdpi/ic_launcher.png → Staging icon

Using flavor-specific AndroidManifest.xml

Add specific permissions or settings with dev/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Debugging permission needed only in dev environment -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <application>
<!-- Allow HTTP only in development -->
<meta-data
android:name="android.network-security-config"
android:resource="@xml/network_security_config_dev" />
</application>
</manifest>

Understanding merge rules

AndroidManifest.xml files are merged in this order:

  1. main/AndroidManifest.xml (base)
  2. <flavor>/AndroidManifest.xml (flavor-specific)
  3. Build type Manifest (debug/release)

When conflicts occur, use tools:replace to override.

Run with Flutter:

# Run in development environment (uses dev icon and name)
flutter run --flavor dev
# Build for production (uses prod settings)
flutter build apk --flavor prod

To access from Dart code, additional setup is needed. Usually the flutter_flavor package is used.

Q. How do I set and change the Package Name?

Package Name is your app’s unique identifier

Package Name (Application ID) is the unique ID that identifies your app on Google Play Store. Once published, it can never be changed, so choose carefully.

Check default Package Name:

// android/app/build.gradle
defaultConfig {
applicationId "com.example.myapp" // This is the Package Name
}

How to change Package Name

  1. Modify build.gradle
defaultConfig {  applicationId "com.mycompany.awesomeapp"  // Change to desired name }

Change MainActivity package path

  • android/app/src/main/kotlin/com/example/myapp/ → android/app/src/main/kotlin/com/mycompany/awesomeapp
1. Modify MainActivity.kt ```kotlin package com.mycompany.awesomeapp // Change package name

2. import io.flutter.embedding.android.FlutterActivity
3. class MainActivity: FlutterActivity() { }
4. **No need to modify AndroidManifest.xml**
- If it shows `.MainActivity`, it automatically follows applicationId
### Package Name naming conventions- Reverse domain format recommended: `com.companyname.appname`
- Use lowercase only
- No special characters (underscore allowed)
- No reserved words (java, android, etc.)
## Q. How do I resolve Multidex errors?### "Cannot fit requested classes in a single dex file" errorAndroid apps can only contain 65,536 methods by default. Exceeding this requires Multidex.**Good news: Recent Flutter versions handle this automatically!**
Flutter 2.10+ automatically enables Multidex when minSdkVersion is 21 or higher.
If you still encounter issues, manually configure in **android/app/build.gradle**:
```gradle
android {
defaultConfig {
minSdkVersion 21 // Automatic multidex support for 21+
multiDexEnabled true // Explicitly enable multidex
}
}
dependencies {
implementation 'androidx.multidex:multidex:2.0.1' // Only needed for minSdk < 21
}

Additional setup needed for minSdkVersion below 21:

// MainActivity.kt
import androidx.multidex.MultiDexApplication
class MyApplication : MultiDexApplication() {
// ...
}

Q. What is Desugaring and when do I need it?

Using modern Java features on older Android versions

Desugaring is a technology that allows you to use Java 8+ features on older Android versions.

Get CuroGom’s stories in your inbox

Join Medium for free to get updates from this writer.

Why is it needed?

  • Limited Java 8 feature support on Android below 7.0 (API 24)
  • Cannot use useful features like LocalDateTimeStream API
  • Many modern libraries require Java 8+ features

Enabling Desugaring

Add to android/app/build.gradle:

android {
compileOptions {
// Enable Java 8 support
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
        // Enable Desugaring
coreLibraryDesugaringEnabled true
}
}
dependencies {
// Add Desugaring library
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}

Now available features

// Native plugins using Java 8 date/time APIs 
// now work properly in Dart too
// Example: In native code
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
LocalDateTime now = LocalDateTime.now();
String formatted = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);

Considerations

  • APK size increases slightly (usually 100–200KB)
  • Unnecessary if minSdkVersion is 21+ and you don’t use Java 8 features
  • May require some ProGuard/R8 rule adjustments

Q. How do I manage Android permissions?

Two types of permissions

Android classifies permissions into two categories:

Normal Permissions

  • Granted automatically at install
  • Internet, vibration, etc.

Dangerous Permissions

  • Require user consent at runtime
  • Camera, location, storage, etc.

Declare permissions in AndroidManifest.xml:

<manifest>
<!-- Normal permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
    <!-- Dangerous permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- New permissions for Android 13+ -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
</manifest>

Android 13+ permission changes

Important changes from Android 13 (API 33):

  1. Notification permission now requires runtime permission
  • Before: Notifications allowed automatically
  • Now: Must explicitly request user permission
  1. Media permissions are more granular
  • Before: READ_EXTERNAL_STORAGE for all file access
  • Now: Separate permissions for photos, videos, and audio

Handle in Flutter with permission_handler package:

// Request notification permission (Android 13+)
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
final status = await Permission.notification.request();
if (status.isDenied) {
// Handle permission denial
}
}
}

Q. How do I manage Keystore and app signing?

Keystore is your app’s identity

Keystore is a certificate used to sign your app. If lost, you cannot update your app, so keep it safe.

Creating a Keystore

keytool -genkey -v -keystore ~/upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias upload

Create key.properties file

android/key.properties (Don’t add to Git!):

storePassword=<password>
keyPassword=<password>
keyAlias=upload
storeFile=<keystore file path>

Configure signing in build.gradle

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}

Q. How do I solve Google Play App Signing and SHA-1 issues?

This really troubles many developers

When using Google Play App Signing, Google re-signs the final APK. This changes the SHA-1 fingerprint, causing issues with Kakao login, Google login, and other services.

Types of SHA-1 keys

  1. Debug certificate SHA-1: Used during development
  2. Upload certificate SHA-1: Used when uploading to Play Console
  3. App Signing certificate SHA-1: Final SHA-1 after Google re-signs

How to check each SHA-1

Check Debug SHA-1:

keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android -keypass android

Check Upload SHA-1:

keytool -list -v -keystore upload-keystore.jks \
-alias upload

Check App Signing SHA-1:

  1. Access Play Console
  2. Select app → Setup → App integrity
  3. Check SHA-1 in “App signing” tab

Configuring social login

Each service requires SHA-1 registration:

Kakao Developer Console:

  • Platform → Android → Register key hashes
  • Add Debug, Upload, and App Signing SHA-1s

Google Firebase Console:

  • Project settings → Android app
  • Add all to SHA certificate fingerprints

This ensures proper operation in development/test/production environments.

Q. How do I resolve AndroidManifest.xml conflicts?

When plugins conflict

Conflicts occur when multiple plugins declare the same permissions or settings.

Common conflict resolution:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
    <!-- Override with tools:replace -->
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
tools:replace="android:label,android:icon">
<!-- Specify merge rules with tools:node -->
<activity
android:name=".MainActivity"
tools:node="merge">
</activity>
<!-- Remove specific elements -->
<uses-library
android:name="org.apache.http.legacy"
android:required="false"
tools:node="remove" />
</application>
</manifest>

Merge rule options

  • tools:replace: Override the attribute
  • tools:remove: Remove the element
  • tools:node="merge": Merge (default)
  • tools:node="remove": Remove
  • tools:node="removeAll": Remove all elements of same type

Q. How do I configure Firebase?

The location of google-services.json is crucial

To use Firebase, the google-services.json file must be in the correct location.

android/
└── app/
├── src/
└── google-services.json // Here!

build.gradle configuration

android/build.gradle (project level):

buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}

android/app/build.gradle (app level):

apply plugin: 'com.google.gms.google-services'  // Add at the bottom
dependencies {
implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation 'com.google.firebase:firebase-analytics'
}

Additional setup for Push notifications

AndroidManifest.xml:

<service
android:name=".MyFirebaseMessagingService"
android:exported="false">

<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- Default notification icon and color -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/colorAccent" />

Q. How do I solve common build errors?

1. “Gradle sync failed” error

Cause: Usually version compatibility issues

Solution:

# 1. Clear cache
cd android
./gradlew clean
# 2. Clean Flutter cache
flutter clean
flutter pub get
# 3. Completely delete Gradle cache (last resort)
rm -rf ~/.gradle/caches/

2. “Duplicate class” error

Cause: Different versions of same library included multiple times

Solution:

// android/app/build.gradle
android {
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt'
}
}
// Exclude specific libraries
configurations {
all {
exclude group: 'com.google.guava', module: 'listenablefuture'
}
}

3. “SDK location not found” error

Cause: Missing local.properties file

Solution:

# Create android/local.properties
sdk.dir=/Users/username/Library/Android/sdk # Mac
sdk.dir=C:\\Users\\username\\AppData\\Local\\Android\\sdk # Windows

4. “Namespace not specified” error

Cause: Namespace required in AGP 7.0+

Solution:

// android/app/build.gradle
android {
namespace 'com.example.myapp' // Same as applicationId
}

Q. To summarize?

The Android build system may seem complex, but understanding key concepts allows you to solve most problems.

Key points to remember

  • Check Gradle and AGP version compatibility
  • targetSdkVersion 35 required by August 31, 2025
  • Choose Package Name carefully (cannot be changed)
  • Keep Keystore safe
  • Manage SHA-1 when using Google Play App Signing
  • Handle Android 13+ permission changes

Now you can systematically solve build errors without panic. Make it a habit to read error messages carefully and check version compatibility first.

Coming next iOS Build System Practical Guide — From Xcode Project Structure to App Store Builds

Please share your Android build problems and solutions in the comments to help others.

Series: Essential Native Development Knowledge for Flutter Developers #2

ไม่มีความคิดเห็น:

แสดงความคิดเห็น