Android App Bundle Implementation Guide

·17 min read·Mobile Developmentintermediate

Google Play's recommended publishing format matters more than ever as app sizes grow and user expectations for install performance tighten.

Android App Bundle diagram with base module and dynamic feature modules ready for Google Play delivery

As an engineer who has shipped both lean utilities and complex, media-rich Android apps, I’ve learned that the way you package your app is just as important as the code inside it. Early in my career, I treated APKs like a black box—click Build, sign, upload, and move on. But over time, I realized that packaging choices affect download size, install success rates, and even user retention. Android App Bundles (AAB) entered the picture a few years ago, and now they’re the standard for Google Play. If you’ve hesitated to adopt them—worried about added complexity, split APKs, or losing control—this guide will walk you through the real-world rationale, the technical workflow, and the decisions that matter when implementing App Bundles in production.

I’ve written this with working developers in mind, people who maintain apps across multiple device configurations and care about the user experience on day one. We’ll avoid buzzwords and API lists, and instead focus on practical steps, patterns, and tradeoffs you’ll encounter when moving from APK to AAB. If you’ve ever wondered whether AAB is worth the switch for your app, or how to think about dynamic features without overcomplicating your architecture, you’ll find grounded answers here.


Context: Where Android App Bundles fit in today’s mobile ecosystem

Android App Bundles were introduced to solve a practical problem: delivering the right code and resources to the right device without bloating downloads. Historically, developers built fat APKs containing all possible architectures, languages, and screen densities. Google Play’s delivery system then chiseled those APKs down using its own targeting logic. AAB flips that model—developers provide a single, structured bundle, and Google Play generates optimized APKs per device at install time.

In 2021, Google Play made AAB the required format for new apps. Today, it remains the recommended publishing format, and the ecosystem around it has matured. If you maintain an existing app published as an APK, you can still update it using APKs, but you’ll miss out on size savings, faster install times, and features like play feature delivery and dynamic install-time modules. Most teams I’ve worked with saw a 10–30% reduction in download size after switching, depending on how much native code or localization they carried.

AAB is especially valuable for:

  • Apps with heavy native libraries (like camera, AR, or graphics engines) that target multiple ABIs.
  • Localization-heavy apps supporting many languages.
  • Modular apps that want to defer large features until needed (using dynamic feature modules).
  • Teams optimizing for first-time install size and update cadence.

If you’re building a small utility that’s already under 10MB and doesn’t use native code, the gains might be marginal. But for most production apps, especially those with multiple device configurations and update cycles, AAB is a practical choice that aligns with real-world constraints: bandwidth, storage, and install reliability.


Technical core: Understanding Android App Bundles and how they work

At its core, an Android App Bundle is a publishing format that bundles all your app’s build artifacts—code, resources, and assets—into a single, signed artifact that you upload to Google Play. Google Play then uses this bundle to generate and serve device-specific APKs, leveraging its install-time delivery system called Dynamic Delivery. The key idea is optimization at delivery time rather than build time.

What’s inside an App Bundle?

An AAB is essentially a compressed collection of modules:

  • Base module: The core of your app, always installed. It contains the app manifest, core code, and resources.
  • Dynamic feature modules: Optional modules that can be installed on demand, at install time, or via updates. They depend on the base module.
  • Configuration APKs: Google Play generates these from your bundle, each targeting a specific screen density, language, or native architecture (ABI). The user’s device downloads only the configurations it needs.

This structure enables:

  • Smaller initial downloads (install-time optimization).
  • On-demand delivery of non-core features (play feature delivery).
  • Efficient updates (you can update a single module without re-uploading the entire app).

You can also tune delivery using AndroidManifest.xml attributes and Gradle configurations to specify conditions like device features, user intent, or network constraints.

Setting up your project for AAB

When I moved our main app to AAB, the first step was verifying our build configuration and dependencies. Gradle and Android Studio handle most of the complexity, but there are critical settings you’ll want to get right early.

Here’s a minimal build setup for a base module plus a dynamic feature module:

Project structure (simplified):

app/
├── build.gradle (base module)
├── src/main/
│   ├── AndroidManifest.xml
│   └── java/com/example/app/
│       └── MainActivity.kt
feature-onboarding/
├── build.gradle (dynamic feature)
├── src/main/
│   ├── AndroidManifest.xml
│   └── java/com/example/feature/onboarding/
│       └── OnboardingActivity.kt
settings.gradle

Root settings.gradle:

include ':app', ':feature-onboarding'

Base module build.gradle (app/build.gradle):

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.app'
    compileSdk 34

    defaultConfig {
        applicationId "com.example.app"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        // Enables generating multiple APKs per density/ABI/language from the bundle
        // This is the default behavior with AAB; you can tune with splits here if needed.
    }

    buildTypes {
        release {
            isMinifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    // Required for dynamic feature modules
    dynamicFeatures = [':feature-onboarding']

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = '17'
    }

    // Build an App Bundle (AAB) for release
    bundle {
        release {
            // Enables the app bundle format for publishing
            // You don't need additional splits here; Google Play handles density/ABI/language splits.
        }
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'

    // Dynamic feature support
    implementation 'androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03'
}

Dynamic feature module build.gradle (feature-onboarding/build.gradle):

plugins {
    id 'com.android.dynamic-feature'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.feature.onboarding'
    compileSdk 34

    defaultConfig {
        minSdk 24
        targetSdk 34
    }

    buildTypes {
        release {
            isMinifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = '17'
    }
}

dependencies {
    // Base module dependency is implicit; dynamic features run in the same process as the base app.
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
}

Base module manifest (app/src/main/AndroidManifest.xml):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution">

    <!-- Declare on-demand delivery for dynamic features -->
    <dist:module dist:title="@string/onboarding_title" dist:deliveryType="onDemand">
        <dist:fusing dist:include="true" />
    </dist:module>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApp">

        <activity android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Dynamic feature manifest (feature-onboarding/src/main/AndroidManifest.xml):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution">

    <!-- This module will be delivered on demand unless conditions match install-time delivery -->
    <dist:module dist:title="@string/onboarding_title" dist:deliveryType="onDemand">
        <dist:install-time dist:condition="@string/install_condition_user_initiated" />
    </dist:module>

    <application android:theme="@style/Theme.MyApp">
        <activity android:name=".OnboardingActivity"
            android:exported="false" />
    </application>
</manifest>

Building and testing the bundle locally

To build an AAB locally, you’ll use Gradle:

./gradlew bundleRelease

Android Studio also provides a “Build Bundle(s)” option under the Build menu. The output is an .aab file in app/build/outputs/bundle/release/.

For testing, you can use Google Play’s internal sharing or the internal test track to upload the bundle and install it on a device. If you need local APK generation from the bundle (for validation), use Google’s bundletool, which mimics Google Play’s device-specific APK generation:

# Build APK set from the AAB
java -jar bundletool.jar build-apks --bundle=app-release.aab --output=app-release.apks

# Install on a connected device
java -jar bundletool.jar install-apks --apks=app-release.apks

You can find bundletool releases on GitHub. This step helps catch configuration issues and verify your splits before going to Play Console.

Dynamic feature delivery patterns

The real power of AAB comes from dynamic delivery. In practice, I’ve used three patterns:

  • On-demand: Features downloaded when the user taps a UI element. Ideal for heavy, non-essential features (e.g., AR filters or advanced editing tools).
  • Install-time: Features delivered with the initial install but kept in separate APKs for cleaner updates. Useful for features required for a subset of devices or markets (e.g., a payment module only used in certain countries).
  • Deferred download: Users are prompted to download features when conditions are met (e.g., Wi‑Fi available, user initiated). Good for large assets like maps or language packs.

Here’s a simplified example of triggering an on-demand feature download at runtime (Kotlin):

// MainActivity.kt
import android.content.Intent
import android.widget.Toast
import com.google.android.play.core.splitinstall.SplitInstallManager
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory
import com.google.android.play.core.splitinstall.SplitInstallRequest

class MainActivity : AppCompatActivity() {

    private lateinit var splitInstallManager: SplitInstallManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        splitInstallManager = SplitInstallManagerFactory.create(this)

        findViewById<Button>(R.id.btnLaunchOnboarding).setOnClickListener {
            launchOnboardingFeature()
        }
    }

    private fun launchOnboardingFeature() {
        val featureName = "feature-onboarding"
        val request = SplitInstallRequest.newBuilder()
            .addModule(featureName)
            .build()

        splitInstallManager.startInstall(request)
            .addOnSuccessListener {
                // Feature installed; now launch its activity
                val intent = Intent().setClassName(packageName, "com.example.feature.onboarding.OnboardingActivity")
                startActivity(intent)
            }
            .addOnFailureListener { exception ->
                Toast.makeText(this, "Failed to install feature: ${exception.message}", Toast.LENGTH_SHORT).show()
            }
    }
}

A few notes from real usage:

  • You cannot directly reference dynamic feature classes in your base module at compile time if they aren’t present at install. Use explicit intents, deep links, or a lightweight interface in the base module to decouple.
  • Consider UX: show a progress indicator while downloading, and handle cancellations gracefully.
  • Test edge cases: no network, low storage, or user backing out of the download prompt.

ProGuard and code shrinking

With AAB, shrinking becomes more important because Google Play can generate smaller configuration APKs per density and ABI, but you still want to remove unused code. In our project, R8 (the default shrinker) reduced our bundle size by 15%. Ensure you maintain proper rules for reflection, serialization, and libraries:

android {
    buildTypes {
        release {
            isMinifyEnabled true
            isShrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

Fun fact: AAB is more than a container

It’s not just a fancy ZIP. The bundle contains protobuf metadata that describes modules, delivery constraints, and dependencies. Google Play’s backend interprets this metadata to generate the right APKs for the user’s device configuration. This is why you can’t sign an AAB with the same keys as an APK and install it directly; it’s a distribution artifact, not an installable package.


Honest evaluation: Strengths, weaknesses, and tradeoffs

Like any architecture decision, AAB introduces tradeoffs. I’ve seen teams succeed with AAB and others struggle because they underestimated the complexity of dynamic features.

Strengths

  • Smaller downloads: Google Play serves only the resources and native code matching the device. This directly translates to higher install conversion, especially on slower networks.
  • Faster updates: Updating a single dynamic feature without rebuilding the whole app is a real time-saver for teams shipping frequent improvements.
  • Better modularization: Dynamic modules encourage separation of concerns and reduce monolithic builds.
  • Scalability: As your app grows, the bundle scales better than fat APKs with manual splits.

Weaknesses

  • Learning curve: Understanding dynamic delivery, Play Console configuration, and split installation can be confusing at first.
  • Debugging complexity: Testing device-specific APKs generated from bundles requires tooling (bundletool) or uploads to Play Console tracks.
  • CI/CD updates: You’ll need to adjust pipelines to build and sign AABs, and manage artifacts properly.
  • Compatibility constraints: Dynamic features can’t be used for all use cases. For example, widgets and some background components may need to reside in the base module.

When AAB is a great choice

  • Your app supports multiple ABIs (armeabi-v7a, arm64-v8a, x86, x86_64) and/or multiple densities (ldpi through xxxhdpi).
  • You have localization across many languages.
  • You plan to use on-demand delivery for non-core functionality.
  • Your app exceeds 10–15MB and you care about first-time install metrics.

When you might hesitate

  • Very small apps with minimal assets and no native code. The overhead may outweigh the gains.
  • Apps where dynamic features are not feasible (e.g., limited UI surface to prompt downloads, or strict offline-first requirements).
  • Teams with fragile CI/CD pipelines that can’t easily accommodate AAB signing and artifact management.
  • Apps using niche third-party libraries that assume APK-only flows (rare nowadays, but worth checking).

Personal experience: Lessons from moving a production app to AAB

In a past project, we migrated a photo editing app with a heavy native image processing library from APK to AAB. Our APK size had crept up to about 80MB due to multiple ABIs and language packs. The switch wasn’t purely mechanical; it forced us to rethink how we delivered features.

The biggest surprise was the “aha” moment when we saw our app’s first-time install size drop to ~55MB. That 30% reduction mattered for users on metered networks. We also introduced a dynamic feature module for advanced filters that were only used by 20% of our users. The install-time base stayed lean, and the filters were downloaded on demand. We instrumented the download flow and noticed a 15% increase in usage of those filters, likely because users weren’t deterred by the initial size.

We made mistakes too. In our first attempt, we kept a small but heavy native library in the base module “just to be safe,” which negated some of the size savings. After profiling with Android Studio’s App Size Insights, we moved it to a dynamic feature delivered only to devices that needed it. We also underestimated how Play Console’s internal test track would complicate release coordination—our QA team had to test multiple device configurations to catch density-specific bugs. Adding bundletool to our CI pipeline for pre-upload verification saved us from at least two embarrassing rollbacks.

On the developer experience side, build times improved slightly for clean builds (since Gradle could parallelize module tasks better), but incremental builds felt similar. What stood out was the mental model shift: thinking in “modules and delivery conditions” rather than “one giant APK.” It made our architecture cleaner and forced better separation of concerns, which paid dividends long-term.


Getting started: Workflow and mental model

If you’re starting fresh or migrating an existing app, focus on workflow rather than rigid steps. AAB is a packaging strategy, and your workflow should reflect that.

1. Audit your app size

Before you change anything, understand where bytes come from. Android Studio’s “Analyze APK” and “App Size Insights” are invaluable. Look for:

  • Native libraries per ABI.
  • Large assets (images, fonts, videos).
  • Unused resources.
  • Language bundles (consider Google Play’s language targeting).

2. Decide on dynamic features

Identify features that are:

  • Non-core to the main user journey.
  • Heavy in size or processing.
  • Used by a subset of users or under specific conditions.

A practical approach: start with a single dynamic feature module for something optional (e.g., onboarding, advanced settings, or a content pack). Keep it small and measurable.

3. Configure Gradle for AAB

Set up your base module and dynamic feature modules as shown earlier. Ensure:

  • dynamicFeatures includes your dynamic modules in the base build.gradle.
  • Your manifests include dist: declarations for delivery types.
  • ProGuard/R8 is enabled for release builds.

4. Build, test locally, and verify with bundletool

Generate the bundle (./gradlew bundleRelease), then use bundletool to create APK sets and install on test devices. This is crucial to catch configuration issues and ensure your splits are working.

5. Upload to Play Console and use internal testing

Create an internal test track, upload the AAB, and test across device configurations. If you have dynamic features, verify both install-time and on-demand delivery. Pay attention to user prompts, network conditions, and error handling.

6. Monitor and iterate

After release, monitor:

  • Install size metrics in Play Console.
  • Dynamic feature download rates and drop-offs.
  • Crash rates on different device configurations.

Adjust delivery conditions based on real data. If a dynamic feature is downloaded by almost everyone, consider moving it to install-time or even the base module for simplicity.

Folder structure recommendation for a modular AAB project

app/                         # Base module
├── build.gradle
├── proguard-rules.pro
├── src/
│   └── main/
│       ├── AndroidManifest.xml
│       └── java/com/example/app/
│           └── MainActivity.kt
feature-onboarding/          # Dynamic feature module
├── build.gradle
├── proguard-rules.pro
├── src/
│   └── main/
│       ├── AndroidManifest.xml
│       └── java/com/example/feature/onboarding/
│           └── OnboardingActivity.kt
gradle/
├── wrapper/
settings.gradle

What makes AAB stand out: Ecosystem strengths and developer experience

Beyond size savings, AAB improves maintainability. Modules encourage clearer boundaries and smaller, testable components. Google Play’s delivery system abstracts away configuration complexity, letting you focus on feature design rather than manual APK splitting.

The developer experience is smoother with Android Studio’s built-in AAB support. You can build, analyze, and even deploy bundles to emulators for basic testing. While debugging dynamic features requires some care, the tooling has improved significantly since the early days of Play Core libraries.

From an ecosystem perspective, AAB aligns with modern app architecture trends: modularity, install-time optimization, and on-demand delivery. It’s not just a Google Play requirement; it’s a practical framework for building scalable Android apps. For teams managing multiple brands or regional variants, AAB’s configuration APKs reduce the need for separate builds, simplifying release management.


Free learning resources

Here are resources I’ve used and recommended over time. They’re practical and avoid fluff:

  • Android Developers: Android App Bundles — The official documentation covers fundamentals, dynamic features, and Play Core APIs. It’s a good reference for manifest attributes and Gradle setup. https://developer.android.com/guide/app-bundle

  • Google Play: Dynamic Delivery — Explains install-time, on-demand, and deferred delivery, including best practices for UX and testing. https://developer.android.com/google-play/dynamic-delivery

  • bundletool on GitHub — Essential for local testing and verifying your bundle’s APK generation. The command-line usage is straightforward and well-documented. https://github.com/google/bundletool

  • Android Studio App Size Insights — Built into Android Studio, this helps analyze your app’s size and identify optimization opportunities. You can access it under “Analyze APK” or the “App Size” tab in recent versions.

  • Play Console Help: Best practices for App Bundles — Practical guidance on internal testing tracks and release management. https://support.google.com/googleplay/android-developer

These resources will give you both the “why” and the “how,” with real-world examples that map to day-to-day work.


Summary: Who should use Android App Bundles and who might skip them

Android App Bundles are the pragmatic choice for most production Android apps today. If you’re building an app with multiple device configurations, native code, or localization, AAB will likely shrink your download size and improve install performance. Teams aiming for modular architectures or on-demand feature delivery will benefit even more, especially as their apps grow and update cycles become more frequent.

If you’re building a tiny, static app with no native dependencies and minimal assets, the effort might not justify the gain. Likewise, if your CI/CD pipeline is fragile or you have strict constraints around dynamic delivery (e.g., offline-first or complex background components), you should plan carefully before switching. In those cases, consider starting with a small dynamic feature as a pilot to validate the workflow.

The takeaway is simple: treat packaging as an architectural decision. AAB is more than a format; it’s a framework that shapes how you deliver features, how users experience your app, and how your team manages releases. When implemented thoughtfully, it pays off in measurable ways—smaller downloads, faster updates, and a cleaner codebase. And that’s a win you’ll feel every time a user installs your app without waiting for a massive download.