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

Nov 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:

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.