Major painpoints of android development as a side activity - Part 1 Gradle

·

7 min read

Cover Image for Major painpoints of android development as a side activity - Part 1 Gradle

I have been doing Android development as hobby for many years now and I have applications that have millions of installs. However, Android development has been painful for me. This may not be a big deal for large orgs but as a small developer who writes code only one weekends this is a different experience.

Gradle - a bit of Frankenstein’s monster.

I come from a web background. So when I think of UI, I mostly think of HTML/React/Angular etc. The idea in web ui is very simple. HTML renders the UI and you fetch data from a remote server which does a lot of complicated actions. I am also familiar with Java SWING api that was used in past to design UI.

Unfortunately that mental model did not translate well to Android development for me. It was made even complicated by extremely terrible documentation and sometimes outright wrong information by Google.

Android Java development was broken to begin with. Firstly, Android operating system had its own problems as it had many versions and the android SDK changed quiet a bit from version to another. But that is a problem which would need another post.

Android java development’s problems started with its completely weird programming model that scattered things into 4 different types of files.

Gradle and Build System

The Gradle build system was an improvement over anything else we had seen in past except may be maven. But it requires you to understand Groovy a language that apparently changes its syntax every second Sunday for no rhyme or reason. I never had time to sit down and ready Groovy syntax. But it would not have helped as the language changed frequently.

But there is more complexity here. Gradle files are not exactly Groovy either. They are groovy based DSL (Domain specific language). Frankly I have no idea what that really means. Sure I can spend some time in mastering that but clearly it would not have helped as Android has now moved to Kotlin based DSL.

A build system is supposed to simplify build process so that the developer can focus on writing code. A build is supposed to contain everything that the primary code needs and as long as you have the basic underlying system then everything just builds magically and works.

Gradle is exact opposite of this. When you read a gradle file you will have no clue what that file is about and what it is supposed to do. An android project has three gradle files of consequence

settings.gradle

This file is supposed to be project level file. You wont understand much about this file. Which is fine. As long as it works and you rarely have to touch it. But you would be wrong. For some projects the order of respositories would matter. For some not.

Notice how same google(), mavenCentral() are repeated without a clear indication of what it means. What the pluginManagement blocks talks about is where should the build system find the plugins. What are plugins ? This is something totally opaque to an Android application developer. You never have to learn anything about them until and unless the build system breaks.

This is followed by dependencyResolutionManagement. This tells your build system where to find the application dependencies and in what order.

Then the file describes the project name and what modules are under that project.

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = 'MyAndroidApp' // Name of your project
include ':app'             // Include the 'app' module

Project level build.gradle

But the settings file is totally different from the project level build file. Note that here too you will specify the repositories which are also present in settings file. So this stuff is now repeated at three places. Next time when someone on stackoverflow tells you that you need to change something here to fix your build, you will have no clue where to make those changes. Also, does the order of repositories matter in this file ? Nobody knows.

Notice that there is dependencies block. But this dependencies have nothing to do with you actually android application but these are dependencies of the gradle build system itself.

Oh wait, it does not stop there. For some reason we again need to have the same “repositories” block again with google() and mavenCentral() mentioned there. How awesome. Why does it exist ? Nobody knows. Does the order matter here ? Nobody knows.

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.4.2' // Specify the Android Gradle Plugin version
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

As an android developer the expectation is that you will rarely touch these two files. Though you will oftne find that things would break and you will have to touch these files far too foten.

The real build file however is build.gradle inside the module.

plugins {
    id 'com.android.application' // Apply the Android Application plugin
    id 'org.jetbrains.kotlin.android' // Apply the Kotlin Android plugin
    id 'kotlin-kapt'  // Enable Kotlin Annotation Processing Tool (KAPT)
}

android {
    namespace 'com.example.myapp' // Package name of your app
    compileSdk 34

    defaultConfig {
        applicationId 'com.example.myapp' // Unique application ID
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled true  // Enable code shrinking for release builds
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' // ProGuard rules
        }
        debug {
            debuggable true
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true // Enable Jetpack Compose
        viewBinding true
        dataBinding true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.5.10' // Or the latest version
    }
    packagingOptions {
        resources {
            exclude 'META-INF/LICENSE*' // Exclude license files to reduce APK size
        }
    }
}

dependencies {
    def nav_version = "2.7.5"
    def room_version = "2.6.1"
    def lifecycle_version = "2.7.0"

    // Core KTX
    implementation "androidx.core:core-ktx:1.12.0"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

    // UI
    implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "com.google.android.material:material:1.11.0"
    implementation "androidx.constraintlayout:constraintlayout:2.1.4"

    // Jetpack Compose
    implementation "androidx.activity:activity-compose:1.8.2"
    implementation platform("androidx.compose:compose-bom:2023.10.01")
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.ui:ui-graphics"
    implementation "androidx.compose.ui:ui-tooling-preview"
    implementation "androidx.compose.material3:material3"
    debugImplementation "androidx.compose.ui:ui-tooling"
    debugImplementation "androidx.compose.ui:ui-test-manifest"

    // Navigation
    implementation "androidx.navigation:navigation-compose:$nav_version"
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

    // Room
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

    // Networking (Retrofit)
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0" // Gson converter

    // Dependency Injection (Dagger/Hilt) - Hilt
    implementation "com.google.dagger:hilt-android:2.51"
    kapt "com.google.dagger:hilt-compiler:2.51"
    implementation "androidx.hilt:hilt-navigation-compose:1.1.0"

    // Testing
    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.5"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}

This file’s syntax has changed over years by a lot so I will not even go into explaining this file line by line but will try to point out only few pain points.

Android Gradle Plugin aka AGP.

You see gradle can be used as a build system for different type of projects. It is used for say SpringBoot projects as well. The build process for Android is different from say SpringBoot. It is the plugins that tell gradle how to actually build.

That one line actually is enabled gradle do a lot of tasks one by one to produce an APK in the end.

Note that the plugin is not actually downloaded right away. The gradle file specifies the plugin which is dynamically fetched from internet during the build process. What this means is that if the gradle plugin is updated somehwere your gradle system will fetch the latest version. if you want to prevent that you have to specify the specific version number but no one generally does that.

Gradle version and Android Plugin versions need to be compatible for them to work together well. But they are both developed independently and fetching logic is not smart enough to figure out which version of the plugin must be used. A lot of issues caused by this.

Broader issues with Gradle and groovy.

Another issue I have with Gradle and groovy is that they use what appears to be a json like syntax. We generally think of json or xml as static content that needs to be understood as a whole. But Groovy is a scripting language. What you see is actually a script which is executed line by line. This creates the maximum level of confusion. The underlying mechanics of how Gradle works are often hidden by the DSL, making it difficult to grasp the build process intuitively.

There is a lot of stuff that is happening to build the android APK and you have no idea how the gradle file is achieving it. The Android build system is constantly evolving. Newer versions of Android Studio often come bundled with or recommend newer versions of Gradle and the Android Gradle Plugin. These updates can introduce breaking changes, deprecate old configurations, or enforce stricter rules that your old project's build files might not comply with.

Solution ?

I think things have gotten better but still not fast enough. It might be a good idea to simply hide the details of Gradle from average developers and move to more Docker like system where everything is frozen in time so old projects can continue to build even if you update your android studio. Perhaps Android Studio can simply take care of the build configuration through a much more simplistic UI.