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 an app while allowing optional features to be downloaded as needed by the user. These on-demand features — known as 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 a user only wants to view a PDF.
Using PSPDFKit for Android in a Dynamic Feature Module in Android Studio
-
Follow the steps listed here to add a dynamic feature module. Choose the settings that apply to you as explained on the page.
-
Next, add the following dependencies to your app-level
build.gradle
file:api 'com.pspdfkit:pspdfkit:2024.5.1' api 'com.google.android.play:feature-delivery:2.0.1' api 'com.google.android.play:feature-delivery-ktx:2.0.1'
Note the use 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.
-
Follow the steps listed on this page to add a simple implementation of the
PSPDFKit
SDK. In this example, store your test PDF file inside the base module’sassets
folder. In an actual project, this would be replaced by the user picking a specific file to display from the device, or another use case that doesn’t necessarily involve packaging aPDF
file in the app. -
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’s a way to track the session ID of the operation to download the DFM. There’s also a LiveData
variable to track the download state and update the user of the status. In a production application, this would be replaced by a progress indicator or something similar. splitInstallManager
handles the requesting and installation of split APKs, and the session ID will be initialized in the next step.
-
splitInstallManager
needs to be initialized, and the listener above needs to be attached to the download request. InitializesplitInstallManager = SplitInstallManagerFactory.create(this)
inonCreate
, attach a click listener to a button to trigger the download, and then observe theLiveData
variable.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
variable observes the state, updates the UI TextView
, and triggers navigation the moment the value for isDownloaded
is true
.
![]()
Make sure 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:
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() } }
-
Finally, unregister the
splitInstallManager
in youronStop
, or when it’s no longer needed:override fun onStop() { super.onStop() splitInstallManager.unregisterListener(listener) }
Testing the App
Testing the app can be done in one of two ways:
-
Uploading a release application to the Google Play Store in the internal test track, and then installing on a device to confirm the downloading works as expected.
-
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 twobundletool
commands from the terminal. This will simulate a download of the DFM.```shell 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 necessary apk file with the local-testing
flag, while the second installs the apk on the device, assuming there’s only one device connected/running.