Android App Bundles: Play Feature Delivery

Learn how to configure your app for Play Feature Delivery which uses advanced capabilities of app bundles, allowing certain features of your app to be delivered conditionally or downloaded on demand. By Harun Wangereka.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Looking at Delivery Types

Dynamic feature modules have the following delivery types:

  1. On-Demand delivery
  2. Install-time delivery
  3. Conditional delivery
  4. Instant delivery

First, take a closer look at On-Demand delivery.

On-Demand Delivery

This method of delivery is for features that aren’t critical when the user first installs the app. The app ships without these features, but users can request them when they need to use them. When the user requests these features, the app downloads them from the Play Core library and installs the module in your app.

Take a look at the manifest configuration for on-demand modules:

<?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"
  package="com.raywenderlich.android.cats">

  <dist:module
    dist:instant="false"
    dist:title="@string/title_cats">
    <dist:delivery>
      <dist:on-demand />
    </dist:delivery>
    <dist:fusing dist:include="true" />
  </dist:module>
</manifest>

Notice the only change you make to make your module on-demand is setting the delivery type to on-demand. However, you need to specify how downloading the modules will happen and handle errors if they occur.

You’ll do that later on in this tutorial.

Install-time Delivery

When you set your module delivery to install-time, your module is on the app when the user first downloads it. This delivery method is useful for modules critical for app functionality, like the onboarding process, sign up and sign in. You can also uninstall these modules since the user won’t use them again on your app.

Your manifest file will look like this:

<dist:module
  dist:instant="false"
  dist:title="@string/title_dogs">
  <dist:delivery>
    <dist:install-time />
  </dist:delivery>
  <dist:fusing dist:include="true" />
</dist:module>

Conditional Delivery

For this type of delivery, you configure the module to be available if the device meets certain conditions. For example, you might develop a feature that isn’t available to users in a specific country. Using dist:conditions, you set the conditions so the module will only be available for the set countries.

The manifest configuration for conditional deliver looks like this:

<dist:module
  dist:instant="false"
  dist:title="@string/title_dogs">
  <dist:delivery>
    <dist:install-time >
      <dist:conditions>
        <dist:user-countries dist:exclude="true">
          <dist:country dist:code="KE"/>
        </dist:user-countries>
      </dist:conditions>
    </dist:install-time>
  </dist:delivery>
  <dist:fusing dist:include="true" />
</dist:module>

Other conditions you can add include:

  • Device API level.
  • Hardware features like camera and augmented reality.

Instant Delivery

For this type of delivery, you let users try your app without installing it. The manifest file looks like this:

<dist:module
  dist:instant="true"
  dist:title="@string/title_cats">
  <dist:delivery>
    <dist:on-demand />
  </dist:delivery>
  <dist:fusing dist:include="true" />
</dist:module>

In a dynamic feature module, you make a module an instant module by setting the dist:instant to true.

There are some things to note with instant feature modules:

  • Your app base module has to be instant-app enabled. In your base module, AndroidManifest.xml, you have to add <dist:module dist:instant="true">. If you use Android Studio to create you module, it will add <dist:module dist:instant="true"> for you.
  • Google Play limits the size of your base module plus instant enabled feature module to at most 10 MB.
  • An instant module can’t have background services or send notifications when running in the background.
  • You can’t have instant modules which are on-demand modules.

Now that you know the different configuration options available for the different delivery types, it’s time to get your hands dirty with the install-time dogs’ module.

Configuring Install-Time Modules

To start, you need to include your shared module in the dogs build.gradle highlighted in the image below.

dogs module gradle file

To do this, add the following code to your dependencies in your dogs module gradle file. Then select Sync Now which appears at the top:

implementation project(":shared")

In your dogs module, create a new file by right-clicking features/dogs/java/com.raywenderlich.android.dogs in Android Studio and selecting New ▸ Kotlin File/Class. Call it DogsActivity.

Place the following code below the package name package com.raywenderlich.android.dogs
and import the corresponding packages by pressing option-return on Mac or Alt+Enter on a PC. If any function has red squiggly lines, re-import its respective package.

import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.raywenderlich.android.shared.R
import com.raywenderlich.android.shared.databinding.ActivityCatsDogsBinding
import com.raywenderlich.android.shared.presentation.adapters.DogsCatsAdapter
import com.raywenderlich.android.shared.presentation.states.UIModel
import com.raywenderlich.android.shared.presentation.states.UIState
import com.raywenderlich.android.shared.presentation.viewmodels.CatsDogViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel

class DogsActivity : AppCompatActivity() {
  // 1
  private val catsDogViewModel: CatsDogViewModel by viewModel()
  private val catsDogsAdapter = DogsCatsAdapter()
  private lateinit var binding: ActivityCatsDogsBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityCatsDogsBinding.inflate(layoutInflater)
    setContentView(binding.root)
    // 2
    catsDogViewModel.getDogs()
    binding.rv.adapter = catsDogsAdapter
    observeDogs()
  }

  // 3
  private fun observeDogs() {
    lifecycleScope.launch {
      catsDogViewModel.dogs.flowWithLifecycle(lifecycle).collect { value: UIState ->
        when (value) {
          is UIState.ShowData<*> -> {
            binding.animationView.cancelAnimation()
            binding.animationView.visibility = View.GONE
            populateData(value.data as List<UIModel>)
          }
          is UIState.Error -> {
            Toast.makeText(applicationContext, value.message, Toast.LENGTH_SHORT).show()
            binding.animationView.cancelAnimation()
            binding.animationView.visibility = View.GONE
          }
          UIState.Loading -> {
            binding.animationView.apply {
              setAnimation(R.raw.dog_animation)
              playAnimation()
              visibility = View.VISIBLE
            }

          }
        }
      }
    }
  }

  // 4
  private fun populateData(data: List<UIModel>) {
    catsDogsAdapter.submitList(data)
  }
}

Here’s what’s happening in this class:

  1. First, you define your top level variables.
  2. Then you call getDogs() from CatsDogViewModel to fetch a list of dog images.
  3. You observe the state of the network call and handle each state.
  4. Finally, you submit the list of dog images to DogsCatsAdapter.

Your dog’s module is ready to show some cute dog images! Grrrrrr!

But first, don’t forget to do something almost all developers forget: Adding your activity in the manifest file!

Open your dogs/AndroidManifest.xml and add DogsActivity inside the manifest element:

<application>
  <activity
    android:name="com.raywenderlich.android.dogs.DogsActivity"
    android:label="@string/title_dogs"
    android:parentActivityName="com.raywenderlich.android.playfeaturedelivery.MainActivity"/>
  </application>

Next, inside MainActivity, replace TODO - Add Dogs Card Click Listener with the following code and import the package for Intent.

binding.dogsCard.setOnClickListener {
  val intent = Intent()
  intent.setClassName(BuildConfig.APPLICATION_ID, "com.raywenderlich.android.dogs.DogsActivity")
  startActivity(intent)
}

As you can see, you create a new Intent and specify the full class name for your DogsActivty. If you don’t do this, the app won’t find your activity since it’s in a different module.

Build and run. Tap DOGS and you’ll see this loading animation:

Dog Images Loading Screen

When the loading completes, you’ll see:

Dogs Images

This image shows an example of an install-time module. See how it displayed the cute dogs instantly!

What if you need to show cute images of cats on-demand? You’ll learn how to do that in the next section.

Configuring On-Demand Delivery Modules

You’ll follow the same steps you did to create your dogs module. Create a new cats module by going to File ▸ New ▸ New Module as shown below:

Creating New Dynamic Feature Module

Click Next. This time, on your final step, select the Do not include module at install-time (on-demand only) option.

Configuring Delivery Type

Now, your modules will look like this:

All Modules Structure

Inside your features package, you’ll find two dynamic feature modules: One is on-demand, and the other one is install-time.

Next, add the functionality to display cat images. Add the shared module to cats build.gradle:

implementation project(":shared")

Do a Gradle sync to see your changes.

Inside cats, create a new file by right-clicking features/cats/java/com.raywenderlich.android.cats in Android Studio and selecting New ▸ Kotlin File/Class. Name it CatsActivity.

In CatsActivity, below the package name package com.raywenderlich.android.cats, add:

import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.google.android.play.core.splitcompat.SplitCompat
import com.raywenderlich.android.shared.R
import com.raywenderlich.android.shared.databinding.ActivityCatsDogsBinding
import com.raywenderlich.android.shared.presentation.adapters.DogsCatsAdapter
import com.raywenderlich.android.shared.presentation.states.UIModel
import com.raywenderlich.android.shared.presentation.states.UIState
import com.raywenderlich.android.shared.presentation.viewmodels.CatsDogViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel

class CatsActivity : AppCompatActivity() {
  private val catsDogViewModel: CatsDogViewModel by viewModel()
  private val catsDogsAdapter = DogsCatsAdapter()
  private lateinit var binding: ActivityCatsDogsBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityCatsDogsBinding.inflate(layoutInflater)
    setContentView(binding.root)
    catsDogViewModel.getCats()
    binding.rv.adapter = catsDogsAdapter
    observeCats()

  }

  private fun observeCats() {
    lifecycleScope.launch {
      catsDogViewModel.cats.flowWithLifecycle(lifecycle).collect { value: UIState ->
        when (value) {
          is UIState.ShowData<*> -> {
            binding.animationView.cancelAnimation()
            binding.animationView.visibility = View.GONE
            populateData(value.data as List<UIModel>)
          }
          is UIState.Error -> {
            binding.animationView.cancelAnimation()
            binding.animationView.visibility = View.GONE
            Toast.makeText(applicationContext, value.message, Toast.LENGTH_SHORT).show()
          }
          UIState.Loading -> {
            binding.animationView.apply {
              setAnimation(R.raw.cat_animation)
              playAnimation()
              visibility = View.VISIBLE
            }
          }
        }
      }
    }
  }

  private fun populateData(data: List<UIModel>) {
    catsDogsAdapter.submitList(data)
  }

  override fun attachBaseContext(base: Context?) {
    super.attachBaseContext(base)
    SplitCompat.install(this)
  }
}

This class is similar to DogsActivity with one slight difference: The CatsActivity overrides attachBaseContext(base: Context?). To access modules code and resources, you must enable SplitCompat in your app which is why you have the SplitCompat.install(this) line.

Don’t forget to add your activity to cats/AndroidManifest.xml:

  <application>
    <activity
      android:name=".CatsActivity"
      android:label="@string/title_cats"
      android:parentActivityName="com.raywenderlich.android.playfeaturedelivery.MainActivity"/>
  </application>

Build and run. Tap CATS. Nothing happens because this is an on-demand module. The app needs to download the module first before you can view it on the app.

You’ll add the logic to download this module in the next section.