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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android App Bundles: Play Feature Delivery
30 mins
- Getting Started
- Understanding Play Feature Delivery
- Enabling Play Feature Delivery
- Creating a Dynamic Feature Modules
- Specifying Delivery Type
- Modularizing Your Android App
- Manifest Configurations
- Looking at Delivery Types
- On-Demand Delivery
- Install-time Delivery
- Conditional Delivery
- Instant Delivery
- Configuring Install-Time Modules
- Configuring On-Demand Delivery Modules
- Downloading an On-Demand Module
- Initiating Download of On-Demand Modules
- Deploying On-Demand Modules
- Testing On-Demand Modules Locally
- Configuring Instant Apps
- Deploying Instant Apps to a Physical Device
- Deploying Instant Apps to Google Play Store
- Creating a Play Instant Release
- Where To Go From Here?
Looking at Delivery Types
Dynamic feature modules have the following delivery types:
- On-Demand delivery
- Install-time delivery
- Conditional delivery
- 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.
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:
- First, you define your top level variables.
- Then you call
getDogs()
fromCatsDogViewModel
to fetch a list of dog images. - You observe the state of the network call and handle each state.
- 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:
When the loading completes, you’ll see:
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:
Click Next. This time, on your final step, select the Do not include module at install-time (on-demand only) option.
Now, your modules will look like this:
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.