Bundle Tool – A Game Changer

Before we begin our deep dive into the implementation of the Bundle Tool, I hope you have read the introductory blog. If not, please have a look here before proceeding ahead.

So, In this blog post, we will look into the implementation of the Bundle Tool, where we create a Sample Application to demonstrate the functionality of Split APK’s at the runtime.

Our Application has the following structure:

  1. Application(let’s call it base module)- Consist of one Activity (Recyclerview List)
  2. Dynamic Module(which will be downloaded at runtime) – Consist of one Activity( Recyclerview item Details)

In the Activity which rests inside our Application or what we call our base module we create a recycler view with a set of few items and a click listener associated with each item.

When we tap on any item present in the recycler view, we open the detailed activity of that item which is located inside the dynamic module which we will download at run time using the Google Bundle tool.

Firstly, let’s walkthrough First Activity(MainActivity.kt and the Base Activity):

class MainActivity : BaseSplitActivity(), RecyclerviewClickHandler {

    private val TAG = MainActivity::class.java.simpleName
    ......
    .....
    private var currentSessionId = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ......
        init()
        ......
    }

     /**
     * Initialize the Split Install Manager and the Recyclerview 
     */
    fun init() {
        splitInstallManager = SplitInstallManagerFactory.create(this)
        recyclerview.setHasFixedSize(true)
        recyclerview.layoutManager = LinearLayoutManager(this)
        recyclerview.adapter =
            RecyclerViewAdapter(getData(), this)
        uninstall_Button.setOnClickListener {
            requestUninstall()
        }
    }

......
......
/**
 * This base activity calls attachBaseContext as described in:
 */
abstract class BaseSplitActivity : AppCompatActivity() {

    override fun attachBaseContext(newBase: Context?) {
        val ctx = newBase?.let { LanguageHelper.getLanguageConfigurationContext(it) }
        super.attachBaseContext(ctx)
        SplitCompat.install(this)
    }
}

So, the first thing which you need to remember is the requirement you need for your use case. In this demonstration, as soon as the user clicks on an item in Recyclerview, we download our module and launch the activity. In such cases, you need to Enable Split Compact at runtime, which we have achieved in the Base Activity.

You can also enable the Split Compact by extending your Application class with SplitCompactApplication

Remember, when the module is being downloaded it’s always a good experience to let the user know, what is happening behind the scenes. In the sample application, we show a progress dialog when the module is getting downloaded. Let’s have a look at that function:

/**
     * Create a listener to handle different states of the downloading of the dynamic module
     */
    private fun createSplitInstallationListener() {
        // Creates a listener for request status updates.
        listener = SplitInstallStateUpdatedListener { state ->
            if (state.sessionId() == currentSessionId) {
                when (state.status()) {
                    DOWNLOADING -> {
                        //show the progress of the downloading to the user
                        displayLoadingState(state)
                    }
                    INSTALLED -> {
                        //once the module is installed, do your stuff here
                        showHideProgressBar(false)
                        onSuccessfulLoad(mTour)
                    }
                    REQUIRES_USER_CONFIRMATION ->
                        // Displays a dialog for the user to either “Download” or “Cancel” the request.
                        splitInstallManager.startConfirmationDialogForResult(
                            state,
                            this,
                            CONFIRMATION_REQUEST_CODE
                        )
                    else -> {
                        //Log or handle any other scenario here
                    }
                }
            }
        }
    }

When we show a dialog to the user, we always need to ask for permission before proceeding ahead. If approved, show the progress dialog to the user. Once the module is installed we hide the progress bar and navigate the user to the next screen with the data.

You might think what if the user didn’t approve the request? To handle this scenario we need to override the following method and handle it graciously.

   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == CONFIRMATION_REQUEST_CODE) {
            // Handle the user's decision. For example, if the user selects "Cancel",
            // you may want to disable certain functionality that depends on the module.
            if (resultCode == Activity.RESULT_CANCELED) {
                showToastAndLog(getString(R.string.user_cancelled))
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data)
        }
    }

You might be thinking now, what about the request, how can we request to download this dynamic module once the user has clicked on the recycler view item?

/**
     * Persist the data to be sent to the next screen and create a request to install the
     * requested module and initiate it
     */
    override fun onClick(tour: TourData, position: Int) {
        ........
        if (splitInstallManager.installedModules.contains(getString(R.string.module_one))) {
            showToastAndLog(getString(R.string.module_already_installed))
            onSuccessfulLoad(tour)
            return
        }

        val request = SplitInstallRequest
            .newBuilder()
            .addModule(getString(R.string.module_one))
            .build()

        splitInstallManager.startInstall(request).addOnSuccessListener { currentSessionId = it }
            .addOnFailureListener { exception ->
                when ((exception as SplitInstallException).errorCode) {
                    SplitInstallErrorCode.NETWORK_ERROR -> {
                        showToastAndLog(getString(R.string.network_error))
                    }
                    SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> checkForActiveDownloads(
                        currentSessionId
                    )
                }
            }
    }

Before initiating the request to download the module, we need to check whether that module has already been downloaded. If Yes, just start the next activity.

If the module is not installed, create a request to install the module by supplying the module name. Remember always to create a session ID in order to keep track of the status of each downloading module. If due to any issue the module downloading fails, we need a Failure Listener to update the user regarding the same and cancel any pending sessions.

Let’s take a look at the screenshot of our application and go through each flow one by one:

  1. Launch our Application- land on the recycler view Activity
Home
  • Click on any item inside recycler view- A pop up is displayed to download the module
Confirmation Dialog
  • Click Yes- we see the progress dialog
Module Downloading in progress
  • After the download complete progress dialog is hidden
  • Navigate to detail Screen
Downloaded module activity

Another question that pop’s in my mind is what if I need to uninstall this downloaded module? Can I do that as easily as I just downloaded it at run-time?

The Answer is No. If you want to uninstall a downloaded module, all we can do is request the play store to uninstall the downloaded module. The actual uninstallation of the module takes place in the background. We can request for uninstallation of the dynamic module using the following code:

   /** Request uninstall of all features. */
    private fun requestUninstall() {
        val installedModules = splitInstallManager.installedModules.toList()
        if (installedModules.isEmpty()) {
            showToastAndLog(getString(R.string.no_modules_downloaded))
            return
        } else {
            showToastAndLog(
                getString(R.string.uninstall_generic_message)
            )
        }
  splitInstallManager.deferredUninstall(installedModules).addOnSuccessListener {
            showToastAndLog("Uninstalling $installedModules")
        }.addOnFailureListener {
            showToastAndLog("Failed installation of $installedModules")
        }
    }

Bingo! We just saw a very simple example of downloading a dynamic Module at runtime using the Android App Bundle! Although at this point in time, Android App Bundle is not widely used for publishing Apps at play store however, I believe this is the future!

Generally, such kind of architecture where the dynamic modules are downloaded at runtime is limited to use cases where your users might purchase some InApp features at runtime.

You can download the complete source code from here. Happy Coding!