Android App Bundle: Load Dynamic Feature Modules with ViewModel and Helper Class

November 18, 2019

Based on Android App Bundle: Launch Activity from Dynamic Feature Modules, I shall reorganize the code in a more reusable manner via ViewModel and helper class.

ViewModel

  • Install Dynamic Modules
class DailyQuoteViewModel(app: Application) : AndroidViewModel(app) {
    companion object {
        const val QUOTE_MAKER_MODULE = "quotemaker"
    }

    private val splitInstallManager = SplitInstallManagerFactory.create(getApplication())
    private var sessionId = 0
    var needSplitCompatInstall = !isQuoteMakerInstalled()

    private val listener = SplitInstallStateUpdatedListener { state ->
        if (state.sessionId() == sessionId) {
            when (state.status()) {
                SplitInstallSessionStatus.FAILED -> {
                    Timber.d("Module install failed with ${state.errorCode()}")
                    processCallbacks(false)
                }
                SplitInstallSessionStatus.INSTALLED -> {
                    processCallbacks(true)
                }
                else -> Timber.d("Status: ${state.status()}")
            }
        }
    }

    init {
        splitInstallManager.registerListener(listener)
    }

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

    private fun isQuoteMakerInstalled() = splitInstallManager.installedModules.contains(QUOTE_MAKER_MODULE)

    private fun requestQuoteMakerInstall() {
        val request =
            SplitInstallRequest
                .newBuilder()
                .addModule(QUOTE_MAKER_MODULE)
                .build()

        splitInstallManager
            .startInstall(request)
            .addOnSuccessListener { id ->
                sessionId = id
            }
            .addOnFailureListener { exception ->
                processCallbacks(false)
                when ((exception as SplitInstallException).errorCode) {
                    SplitInstallErrorCode.NETWORK_ERROR -> {
                        Timber.e(exception, "splitInstallManager failed - network error")
                    }
                    else -> {
                        Timber.e(exception, "splitInstallManager failed")
                    }
                }
            }
    }

    private val callbacks = ArrayDeque<(success: Boolean) -> Unit>()

    private fun processCallbacks(success: Boolean) {
        while (callbacks.isNotEmpty()) {
            callbacks.remove().invoke(success)
        }
    }

    fun getQuoteMakerModule(callback: (success: Boolean) -> Unit) {
        if (isQuoteMakerInstalled()) {
            callback(true)
        }
        else {
            if (callbacks.size == 0) {
                requestQuoteMakerInstall()
            }
            callbacks.add(callback)
        }
    }
}

ActivityFactory

  • Launch Dynamic Modules’s Activity
interface AddressableActivity {
    val className: String
}

object ActivityFactory {
    const val PACKAGE_NAME = "com.luasoftware"
    const val QUOTE_MAKER_PACKAGE = "$PACKAGE_NAME.quotemaker"

    fun createIntent(addressableActivity: AddressableActivity): Intent {
        return Intent(Intent.ACTION_VIEW).setClassName(
            BuildConfig.APPLICATION_ID,
            addressableActivity.className)
    }

    object QuoteMakerX:  AddressableActivity {
        override val className: String
            get() = "${QUOTE_MAKER_PACKAGE}.view.QuoteMakerXActivity"

        const val EXTRA_QUOTE_ID = "quote_id"
        const val EXTRA_CONTENT = "content"
        const val EXTTRA_SOURCE = "source"

        fun createIntent(quoteId: String? = null, content: String? = null, source: String? = null): Intent {
            return createIntent(this).apply {
                putExtra(EXTRA_QUOTE_ID, quoteId)
                putExtra(EXTRA_CONTENT, content)
                putExtra(EXTTRA_SOURCE, source)
            }
        }
    }
}

Usage in Activity

class DailyQuoteActivity : AppCompatActivity() {
    private val viewModel by viewModels<DailyQuoteViewModel>()

    override fun attachBaseContext(base: Context) {
        // if need to access language resources from dynamic modules
        /*
        val configuration = Configuration()
        configuration.setLocale(Locale.forLanguageTag("en"))
        val context = base.createConfigurationContext(configuration)
        super.attachBaseContext(context)
         */
        super.attachBaseContext(base)
        // required to access resources (e.g. drawable) from dynamic modules
        SplitCompat.install(this)
    }

    fun startModuleActivity() {
        viewModel.getQuoteMakerModule { success ->
            if (success) {
                val intent = ActivityFactory.QuoteMakerX.createIntent(quoteId = "ID", content = "CONTENT", source = "SOURCE")
                startActivity(intent)
            }
        }
    }

    fun loadDrawableFromModule() {
        viewModel.getQuoteMakerModule { success ->
            if (success) {
                val moduleResourceId = ...

                if (viewModel.needSplitCompatInstall) {
                    Timber.d("SplitCompat.install")

                    // you can access drawable via applicationContext without SplitCompat.install
                    val d = AppCompatResources.getDrawable(applicationContext, moduleResourceId)

                    // need this to access drawable from module during first install
                    SplitCompat.install(context)
                    viewModel.needSplitCompatInstall = false
                }

                // access drawable via activity context require SplitCompat.install
                // , else Resources.NotFoundException exception is thrown
                val d = AppCompatResources.getDrawable(context, moduleResourceId)
                imageView.setImageResource(moduleResourceId)

            }
        }
    }
}

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.