How to use PSPDFKit for Android with Dynamic Delivery

Dynamic delivery or on-demand delivery was introduced to Android to allow developers to ship only the necessary features of the app while allowing optional features to be downloaded as needed by the user. These on-demand features (aka “dynamic feature modules (DFM)”) are downloaded automatically during runtime rather than download/install time. Note that these features are downloaded only once. In the case of our PSPDFKit SDK, this feature becomes useful when the user wants to view a PDF only.

Using PSPDFKit for Android in a Dynamic Feature Module in Android Studio

  1. Follow the steps listed here to add a dynamic feature module. Choose the settings that apply to you as explained on that page.

  2. Next add the following dependencies to your app level build.gradle file.

    api 'com.pspdfkit:pspdfkit:2024.1.1'
    api 'com.google.android.play:feature-delivery:2.0.1'
    api 'com.google.android.play:feature-delivery-ktx:2.0.1'

    Note the usage of the api configuration because these libraries will be used by all modules in the test application. If you have more than two modules, it makes more sense to add the libraries individually in each module using the implementation configuration. You have to add these dependencies in the base module as well as the module containing the PSPDFKit library. Read more on why this is happening on this issue.

  3. Follow the steps listed on this page to add a simple implementation of PSPDFKit SDK. In this example, store your test PDF file inside the base module’s assets folder. In an actual project, this would be replaced by the user picking a specific file to display from the device or another usecase that does not necessarily involve packaging a PDF file in the app.

  4. Add the following lines of code at the top of the MainActivity file of the base module

    private var mySessionId = 0
    private lateinit var splitInstallManager: SplitInstallManager
    private val stateLiveData = MutableLiveData<DownloadState>()
    
    companion object {
    // The test pdf file is stored in the main assets folder and is called pdfquick.pdf
    const val FILE_NAME = "file:///android_asset/pdfquick.pdf" 
    }
    
    // Creates a listener for request status updates.
    private val listener = SplitInstallStateUpdatedListener { state ->
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.CANCELED -> {
                stateLiveData.value = DownloadState(state = "CANCELED")
            }
            SplitInstallSessionStatus.CANCELING -> {
                stateLiveData.value = DownloadState(state = "CANCELING")
            }
            SplitInstallSessionStatus.DOWNLOADED -> {
                stateLiveData.value = DownloadState(state = "DOWNLOADED")
            }
            SplitInstallSessionStatus.DOWNLOADING -> {
                stateLiveData.value = DownloadState(state = "DOWNLOADING")
            }
            SplitInstallSessionStatus.FAILED -> {
                stateLiveData.value = DownloadState(state = "FAILED")
            }
            SplitInstallSessionStatus.INSTALLED -> {
                val modules = state.moduleNames()
                stateLiveData.value = DownloadState(modules.contains("pspdfDynamicFeature"), "INSTALLED")
            }
            SplitInstallSessionStatus.INSTALLING -> {
                stateLiveData.value = DownloadState(state = "PENDING")
            }
            SplitInstallSessionStatus.PENDING -> {
                stateLiveData.value = DownloadState(state = "PENDING")
            }
            SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> {
                stateLiveData.value = DownloadState(state = "REQUIRES_USER_CONFIRMATION")
            }
            SplitInstallSessionStatus.UNKNOWN -> {
                stateLiveData.value = DownloadState(state = "UNKNOWN error")
            }
        }
    }
    }
    
    data class DownloadState(
    val isDownloaded: Boolean = false,
    val state: String
    )

With this code, there is a way to track the session id of the operation to download the DFM. There is also a LiveData to track the download state and a way to update the user of the status. In a production application, this would be replaced by a progress indicator or something similar. The splitInstallManager handles the requesting and installation of split APKs. The session id will be initialised in the next step.

  1. Next the splitInstallManager needs to be initialised and the above listener attached to the download request. Initialise the manager in onCreate as follows: splitInstallManager = SplitInstallManagerFactory.create(this), attach a click listener to a button to trigger the download, and then observe the livedata variable. The onCreate then becomes:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    
        splitInstallManager = SplitInstallManagerFactory.create(this)
    
        val stateText = findViewById<TextView>(R.id.text_state)
        findViewById<Button>(R.id.btn_download_show).setOnClickListener {
            downloadDynamicModule()
        }
    
        stateLiveData.observe(this) { downloadState ->
            val builder = StringBuilder()
            builder.appendLine(downloadState.state)
            stateText.text = builder
    
            if (downloadState.isDownloaded) {
                val intent = Intent().setClassName(
                    BuildConfig.APPLICATION_ID, "com.pspdfkitexample.pspdfdynamicfeature.PDFDynamicActivity"
                )
                startActivity(intent)
            }
        }
    }

The LiveData observes the state, updates the UI in the state TextView, and triggers navigation the moment the value for isDownloaded is true.

NOTE: Make sure that your dynamic module has the same package and activity names if you wish to use the exact navigation code above, otherwise the app will crash once the attempt to trigger navigation happens. If the package names are different, modify the intent as well.

The method downloadDynamicModule is as below:

private fun downloadDynamicModule() {
    val request = SplitInstallRequest.newBuilder().addModule("pspdfDynamicFeature").build()

    splitInstallManager.registerListener(listener)

    splitInstallManager
        // Submits the request to install the module through the
        // asynchronous startInstall() task. Your app needs to be
        // in the foreground to submit the request.
        .startInstall(request)
        // You should also be able to gracefully handle
        // request state changes and errors.
        .addOnSuccessListener { sessionId ->
            mySessionId = sessionId
        }.addOnFailureListener { exception ->
            Toast.makeText(
                this,
                "Request to download dynamic module has failed with the error ${exception.message}", Toast.LENGTH_LONG
            ).show()
        }
}
  1. Finally, unregister the splitInstallManager in your onStop, or when it is no longer needed as follows.

    override fun onStop() {
        super.onStop()
        splitInstallManager.unregisterListener(listener)
    }

Testing the app

Testing the app can be done in one of two ways:

  1. Uploading a release application to the Google Playstore in the internal test track, and then installing in a device to confirm the downloading works as expected.

  2. Using bundletool by Google locally. Once this tool has been installed, navigate to the folder with the generated debug bundle file named app_debug.aab and start a terminal there. Now run the following two bundletool commands from the terminal. This will simulate a download of the DFM.

    bundletool build-apks --local-testing --bundle app_debug.aab --output my_app.apks
    
    bundletool install-apks --apks my_app.apks

The first command builds the apks file needed with the local-testing flag while the second installs the apk on the device assuming there is only one devices connected/running.