Overview

Google Play Instant enables native Android apps to run without requiring installation. Apps which support Google Play Instant can run in a variety of situations, such as when a user clicks a link in a text message or uses the "Try Now" button from the Google Play Store:

In this codelab, we'll leverage Google Play Instant's ability to load code on-demand via feature modules. You'll start with the "Topeka" app, which supports Google Play Instant. You'll take "Topeka" and modularize it's code to create a multi feature app that also supports Google Play Instant.

Update Android Studio

Ensure you are using Android Studio 3.0. If you are using an older version of Android Studio, download version 3.0 or later.

Fetch Sample Project

To transform "Topeka" you will start with the instant app source code for this app. To get the code, clone the repo using the following command:

$ git clone -b singleModule https://git@github.com/googlecodelabs/android-topeka.git

Alternatively you can download the repository as a Zip file:

Download Zip

Let's open "Topeka" using Android Studio. Run Android Studio 3.0 or higher and choose "Open an existing Android Studio project". Open the Topeka project you fetched in the previous step.

Now let's ensure that everything builds correctly within Android Studio. Go to Build -> Build APK. The project should complete the build without any issues:

Install Instant App SDK

In order to build an app that support Google Play Instant, we need to install the Instant App SDK. Go to Tools -> SDK Manager. Click on the "SDK Tools" tab and install "Instant Apps Development SDK" by checking the box and hitting "Apply"

Install Android API 27 SDK and Tools

Go to Tools -> SDK Manager. Click on the "SDK Platforms" tab. Make sure the Show Package Details checkbox is pressed.

Be sure to install the Android 8.1 system images and tools are installed by checking the following boxes under "SDK Platforms" tab and hitting "Apply" button:

Create an Emulated Device

The code lab will use an Emulator device to run Topeka. To create one, open an Android project and run AVD Manager from Android Studio (Tools -> AVD Manager) and create a new AVD with the following configuration:

Verify the device settings and tap "Finish" to create the emulated device.

Next, we should launch the app in the emulator and start to familiarize ourselves with it:

Running Topeka

Click ‘Run' to start the app. Select the emulator from the connected devices and check "Use same selection for future launches"

Verify the app runs and you get the following screen:

Topeka currently supports Google Play Instant. Because of this, the app is divided into three modules, one for each build process (installed and instant), and one module with all of the code and resources (base):

For this codelab, we'll focus on taking the base module's code and splitting it into multiple modules. These modules are known as feature modules and use the com.android.feature plugin.

Before going further, let's talk about what com.android.feature does and what a feature module is.

If you think about an Android App, it usually has at least one task it lets the user accomplish - for example, taking a quiz in Topeka. Each of these discrete, accomplishable tasks can be thought of as a separate feature. Google Play Instant allows you to create separate modules for the code specific to each feature and to download that code on demand when the user uses the feature.

Base feature module

Each app must define a single module known as the base feature module. In a multi-module app, the base feature module will contain shared code. It is marked with a baseFeature attribute in the module's build.gradle file.

Dividing Topeka

You have an app with one feature module. Now we will divide the code currently in :base into multiple feature modules:

Below is a diagram of the different dependencies between modules. The :instant and :installed modules will depend on all the features. In turn, each feature knows about the :base feature module.

To prepare our app for the new feature modules, let's change how navigation works in our app. Instead of starting activities using class names, we will use the urls we defined.

1. Modify Activity Launch Helper to launch with Urls

Let's update ActivityLaunchHelper to use our new URLs - this allows us to remove references between different parts of our code:

helper/ActivityLaunchHelper

class ActivityLaunchHelper {

    companion object {

        const val EXTRA_EDIT = "EDIT"

        private const val URL_BASE = "https://topeka.instantappsample.com"
        private const val URL_SIGNIN = "$URL_BASE/signin"
        private const val URL_CATEGORIES = "$URL_BASE/categories"
        private const val URL_QUIZ_BASE = "$URL_BASE/quiz/"

        fun launchCategorySelection(activity: Activity, options: ActivityOptionsCompat? = null) {
            val starter = categorySelectionIntent(activity)
            if (options == null) {
                activity.startActivity(starter)
            } else {
                ActivityCompat.startActivity(activity, starter, options.toBundle())
            }
        }

        fun launchSignIn(activity: Activity, edit: Boolean = false) {
            ActivityCompat.startActivity(activity,
                    signInIntent(activity, edit),
                    ActivityOptionsCompat.makeSceneTransitionAnimation(activity).toBundle())
        }

        fun categorySelectionIntent(context: Context? = null) = baseIntent(URL_CATEGORIES, context)

        fun quizIntent(category: Category, context: Context? = null) =
                baseIntent("$URL_QUIZ_BASE${category.id}", context)

        fun signInIntent(context: Context? = null, edit: Boolean = false): Intent =
                baseIntent(URL_SIGNIN, context).putExtra(EXTRA_EDIT, edit)

        private fun baseIntent(url: String, context: Context? = null): Intent {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                    .addCategory(Intent.CATEGORY_DEFAULT)
                    .addCategory(Intent.CATEGORY_BROWSABLE)
            if (context != null) {
                intent.`package` = context.packageName
            }
            return intent
        }
    }
}

Note:

Let's introduce the multi feature structure to Topeka by adding the :category module.

1. Add :category feature module

From File->New Module...

Choose "Feature Module" and click "Next."

Choose the following:

Click "Next".

In the final wizard screen, choose "Add No Activity" and click Finish.

2. Review what changed in the gradle files

Make sure to let the gradle sync complete.

There were a number of things that were automatically added to your project when you created this new :categories feature module. This includes:

3. Add the Kotlin library to :categories

There are a few more things you need to add yourself. Since we're using Kotlin in Topeka, you'll need to add the Kotlin plugin to the :categories gradle file.

categories/build.gradle

apply plugin: 'com.android.feature'
apply plugin: 'kotlin-android' // Make sure you have this plugin at the top of your gradle file

4. Move libraries specific to :categories

If there's a library that only one of your features will use, it should be moved to that feature's gradle file. For example, remove the RecyclerView library from :base :

base/build.gradle

dependencies {
    // shared libraries
    implementation "com.android.support:recyclerview-v7:$supportLibVersion" // remove this
}

And add it to :categories:

categories/build.gradle

dependencies {
    implementation project(':base')
    implementation "com.android.support:recyclerview-v7:$supportLibVersion" // add this
}

5. Make :installed and :instant build :categories

Your :instant and :installed modules must both know about the new :categories module so that they both can build it.

installed/build.gradle

dependencies {
    implementation project(':base')
    implementation project(':categories') // Add this line
}

instant/build.gradle

dependencies {
    implementation project(':base')
    implementation project(':categories') // Add this line
}

6. Update base.gradle

One of the functions that your :base feature gradle file provides is a list of the libraries that will be used by all modules, including the Support Library and Kotlin standard library. To make these accessible by all feature modules, make these API dependencies instead of implementation dependencies.

base/build.gradle

dependencies {
    // Already important, but an important part of multi-feature instant apps
    application project(':installed') 

    // ... project associations should be api
    api "com.android.support:appcompat-v7:$supportLibVersion"
    api "com.android.support:design:$supportLibVersion"
    api "com.android.support:support-v4:$supportLibVersion"
    api 'com.google.android.instantapps:instantapps:1.1.0'
    api "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    api "com.android.support.test.espresso:espresso-idling-resource:$espressoVersion"
    //... non-shared libraries
}

7. Run your app!

At this point you should be able to build your app. You haven't divided up the code yet, but you've create a module for your new feature to go and write up the build processes. Go ahead and run your app.

8. Build the app to ensure Google Play Instant ZIP is generated

If you app runs, now let's see what is built and what a multi-feature app which supports Google Play Instant looks like:

Rebuild your app using Build -> Build APK(s)

Switch to Project mode and navigate to Topeka/instant/build/outputs/apk/debug/instant-debug.zip:

As you can see, this is the zip file containing two feature APKs, for each feature :base and :categories.

:base still has all of the code, and there's not much in :categories which is why the sizes are 5.5MB vs 2.8KB respectively. In the next step we'll move our category specific code over to the categories module.

Your app builds a Google Play Instant zip file, but there's nothing in :categories! Time to change that.

1. Move over Category classes

The following classes specific to the category feature:

You can use the Refactor->Move tool to do this.

First, select the CategorySelectionActivity class and right click. Then go to Refactor -> Move:

Then from the Move tool window, you'll want to change the Destination directory:

Choose the categories/src/main directory:


And change the To package to: com.google.samples.apps.topeka.categories.activity

Allow it to create the new package by selecting Yes:

Ignore the issues and select Continue:

Repeat for the other classes, make sure to give the correct packages. The end result should look like this; all three classes in their appropriate package, in the categories feature module:

2. Move over the activity tag in the Android Manifest


Open the :base AndroidManifest.xml and cut the activity tag for CategorySelectionActivity.

Move it to the :categories AndroidManifest.xml.

categories/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.samples.apps.topeka.categories" >
    <application>
        <activity
            android:name="com.google.samples.apps.topeka.categories.activity.CategorySelectionActivity"
            android:theme="@style/Topeka.CategorySelectionActivity">
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="http"
                    android:host="topeka.instantappsample.com"
                    android:pathPrefix="/categories" />
            </intent-filter>
            <intent-filter  android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="https"
                    android:host="topeka.instantappsample.com"
                    android:pathPrefix="/categories" />
            </intent-filter>
        </activity>
    </application>
</manifest>

At this point you should be able to build and run your app. If your app isn't running, try cleaning your project first, using Build -> Clean Project.

3. Move over associated image resources

All of the resources labeled image_category_* and icon_category_* are specific to the :categories module and so should be moved from :base to :categories. This is over 100+ images, so don't do this individually! Instead, copy the whole drawable folder contents from :base over to :categories.

Then in categories/src/main/res/drawable/, delete all files that don't start with image_category_* and icon_category_*. You will end up deleting 156 files.

Now in base/src/main/res/drawable/, delete all of the files that do start with image_category_* and icon_category_*. When you see this screen, select "Delete Anyway"

You will end up deleting 133 files.

4. Move over layout files

Move the following layout files from :base to :categories:

  1. activity_categories_selection.xml
  2. fragment_categories.xml

You'll need to make a new layout folder

Then cut and paste both layouts into the categories/src/main/res/layout folder:

5. Move over v21 Transitions files

You can cut and paste the transitions files starting with the world "category":

Create a new resource directory with transition as the title, and minimum API level 21.

Cut and move transition files over into the new directory categories/src/main/res/transitions-v21/ .

6. Move over styles

In base/src/main/res/values-21/styles move over Topeka.CateogrySelectionActivity style to categories/src/main/res/values-21/styles . You'll need to create a new values-21 resource directory for the styles.

Cut the entire Topeka.CateogrySelectionActivity tag:

And move it to :categories :

categories/src/main/res/values-v21/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Topeka.CategorySelectionActivity">
        <item name="android:windowSharedElementEnterTransition">
            @transition/category_shared_enter
        </item>
        <item name="android:windowEnterTransition">@transition/category_enter</item>
        <item name="android:windowExitTransition">@transition/category_exit</item>
        <item name="android:windowBackground">@color/topeka_blank</item>
    </style>
</resources>

7. Create the sign out menu for categories

Make a new menu resource file:

And create a menu specific to :categories called menu_category.xml with the following code for a sign-out button:

categories/src/main/res/menu/menu_category.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/sign_out"
        app:showAsAction="never"
        android:title="@string/sign_out" />
</menu>

8. Fix import issues

Moving resources between modules will almost always cause resource related issues when you refer to these resources in your code. The location of a module's R file is determined by the package given in the AndroidManifest. Here are the two packages for the R files in your app, as defined by the respective Android Manifests:


Your code is only importing :base's R file. Most of your classes will have an import that looks like this:

import com.google.samples.apps.topeka.base.R

For your classes in :categories, start by changing the important statement for R such that it imports from :categories' R file instead. In CategorySelectionActivity, CategoryAdapter, CategorySelectionFragment make the following change:

// Replace
//import com.google.samples.apps.topeka.base.R
// with
import com.google.samples.apps.topeka.categories.R
import com.google.samples.apps.topeka.base.R as RBase

Note the last line still important :base's R file with the name RBase. This is because you'll still need to reference a few resources in the shared base. Namely:

CategorySelectionActivity.kt

// Replace references to
//R.string.x_points
// with
RBase.string.x_points

CategorySelectionFragment.kt

// Replace references to
// R.id.category_title
// R.dimen.spacing_nano
// R.id.solved
// R.string.transition_toolbar
// with
RBase.id.category_title
RBase.dimen.spacing_nano
RBase.id.solved
RBase.string.transition_toolbar

9. Build your new Google Play Instant APKS

Alright, moment of truth! Run your newly modularized app as an instant app. If that works, go ahead and rebuild your app using Build -> Build APK(s).

Because of all your hard work moving files, you should see that the feature module size is now more evenly split. The base module has 3.2MB and the categories modules has 2.8MB.

Congratulations!

You've created a multi-feature Android app supported by Google Play Instant! Splitting your code this way means faster instant app downloads for your users. At this point we've covered the majority of the content for the codelab and you should be well on your way to turning your own app into an Android Instant app. At this point you can do one of the following:

  1. Optionally split out the quiz feature into its' own feature module. We won't be covering new material in this step, but it's good practice and an example of what a more "real world" app would look like.
  2. Review the final code state of Topeka on Github.
  3. Read additional Google Play Instant resources and documentation.

In this optional step, you'll also split out the :quiz module

1. Add quiz module

From File->New Module... Choose "Feature Module" and click "Next."

Choose the following:

Click "Next".

On the final wizard screen, choose "Add No Activity" and click Finish.

2. Make :installed and :instant build :quiz

As with :categories, update the gradle files so that they both build :quiz.

installed/build.gradle

dependencies {
    implementation project(':base')
    implementation project(':categories')
    implementation project(':quiz') // Add this line
}

instant/build.gradle

dependencies {
    implementation project(':base')
    implementation project(':categories')
    implementation project(':quiz') // Add this line
}

3. Add the Kotlin library to categories

You'll also need to add the Kotlin plugin to the :quiz gradle file

quiz/build.gradle

apply plugin: 'com.android.feature'
apply plugin: 'kotlin-android' // Make sure you have this plugin at the top of your gradle file

4. Move over Quiz classes

You'll want to move over the following classes to :quiz with the same directory structure.

5. Move over the activity tag in the Android Manifest

You'll also need to move over the associated Android Manifest tags from :base's manifest file to the new :quiz Android Manifest.

quiz/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.samples.apps.topeka.quiz" >
    <application>
        <activity
            android:name="com.google.samples.apps.topeka.activity.QuizActivity"
            android:launchMode="singleTop"
            android:windowSoftInputMode="adjustPan"
            android:theme="@style/Topeka.QuizActivity">
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="http"
                    android:host="topeka.instantappsample.com"
                    android:pathPrefix="/quiz" />
            </intent-filter>
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="https"
                    android:host="topeka.instantappsample.com"
                    android:pathPrefix="/quiz" />
            </intent-filter>
        </activity>
    </application>
</manifest>

6. Move and make copies of image resources

Move of the following resources from :base to :quiz

Make copies of the following :base resources in :quiz

7. Move over layout files

Move of the following layouts from :base to :quiz

8. Move over animator, transition and array resources

Move of the following resources from :base to :quiz:

9. Move v21 style.xml

In base/src/main/res/values-21/styles move over Topeka.QuizActivity style to quiz/src/main/res/values-21/styles . You'll need to create a new values-21 resource directory for the styles.

quiz/src/main/res/values-v21/styles.xml

<resources>
    <style name="Topeka.QuizActivity">
        <item name="android:windowSharedElementEnterTransition">@transition/quiz_shared_enter</item>
    </style>

</resources>

10. Make a new style.xml

Also make a new style.xml file, and add the following:

quiz/src/main/res/values/styles.xml

<resources>

    <style name="Topeka.CompoundButton" parent="Widget.AppCompat.Button.Borderless">
        <item name="android:background">@drawable/selector_button</item>
        <item name="android:button">@null</item>
    </style>

    <style name="Topeka.CompoundButton.Radio" parent="Topeka.CompoundButton">
        <item name="android:background">@drawable/selector_checkable</item>
    </style>

    <style name="Topeka.CompoundButton.True" parent="Topeka.CompoundButton">
        <item name="android:background">@drawable/selector_true</item>
    </style>

    <style name="Topeka.CompoundButton.False" parent="Topeka.CompoundButton">
        <item name="android:background">@drawable/selector_false</item>
    </style>

    <style name="Topeka.TextAppearance" />

    <style name="Topeka.TextAppearance.CategoryItem" parent="TextAppearance.AppCompat.Small">
        <item name="android:textSize">@dimen/category_item_text_size</item>
    </style>

    <style name="Topeka.TextAppearance.Title" parent="TextAppearance.AppCompat.Title">
        <item name="android:textColor">@color/topeka_primary</item>
    </style>

    <style name="Topeka.TextAppearance.Title.Inverse" parent="TextAppearance.AppCompat.Title">
        <item name="android:textColor">@android:color/white</item>
    </style>

    <style name="Topeka.TextAppearance.Title.Picker" parent="Topeka.TextAppearance.Title">
        <item name="android:textSize">96sp</item>
    </style>

    <style name="Topeka.TextAppearance.Subhead" parent="TextAppearance.AppCompat.Subhead">
        <item name="android:textColor">@color/topeka_primary</item>
    </style>

    <style name="Topeka.TextAppearance.ListItem" parent="TextAppearance.AppCompat.Subhead" />
    <style name="Topeka.TextAppearance.ListItemSecondary" parent="TextAppearance.AppCompat.Body1" />

</resources>

Delete the following from :base:

base/src/main/res/values/styles.xml

<resources>
  //...
  //Delete these style tags

    <style name="Topeka.CompoundButton.True" parent="Topeka.CompoundButton">
        <item name="android:background">@drawable/selector_true</item>
    </style>

    <style name="Topeka.CompoundButton.False" parent="Topeka.CompoundButton">
        <item name="android:background">@drawable/selector_false</item>
    </style>

   //...

    <style name="Topeka.TextAppearance.ListItem" parent="TextAppearance.AppCompat.Subhead" />
    <style name="Topeka.TextAppearance.ListItemSecondary" parent="TextAppearance.AppCompat.Body1" />
</resources>

11. Fix import issues

As with :categories, there will be import issues. We've moved a lot more classes this time around, so when you build, you'll see a lot of Unresolved reference errors.

In the affected classes, add the following import statements.

// Replace
//import com.google.samples.apps.topeka.base.R
// with
import com.google.samples.apps.topeka.categories.R
import com.google.samples.apps.topeka.base.R as RBase

Then for each resource that's in base, use RBase as before.

12, Run and build your app

Run your newly modularized app as an instant app. If that works, go ahead and rebuild your app using Build -> Build APK(s).

You've created a true multi-feature instant app!

You're done!

Great work! You've successfully taken a real world Android app and have gone through the process of modularizing it and preparing it for Google Play Instant. If this were your own app, the final step would be to publish on the Play Store. If you have time, glance through this guide to get an idea of the final steps!

Make sure to take a look at the final code with multiple modules on Github.

If you want to go deeper on Google Play Instant, here are some links to get you started:

Main Docs

Code Samples

UX Guidelines

FAQ