我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Jetpack Compose 中的动态加载、插件化技术探索

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Jetpack Compose 中的动态加载、插件化技术探索

在传统的 Android 开发模式中,由于界面过分依赖于 ActivityFragment这样的组件,一个业务模块中往往会存在着大量的 Activity 类,因此诞生了很多的插件化框架,这些插件化框架基本都是想方设法的使用各种Hook/反射手段来解决使用未注册的组件问题。在进入 Jetpack Compose 的世界以后,Activity 的角色被淡化了,由于一个 Composable 组件就可以承担一个屏幕级的显示,因此我们的应用中不再需要那么多的 Activity 类,只要你喜欢,你甚至可以打造一个单 Activity 的纯 Compose 应用。

本文主要尝试探索几种可以在 Jetpack Compose 中实施插件化/动态加载的可行性方案。

以 Activity占坑的方式访问插件中的 Composable 组件

这种方式其实传统 View 开发也可以做,但是由于 Compose 中我们可以只使用一个Activity,而其余页面均使用 Composable 组件来实现,感觉更加适合它。因此主要的思路就是在宿主应用的 AndroidManifest.xml 中注册一个占坑的 Activity类,该 Activity实际存在于插件中,然后在宿主中加载插件中该 Activity的Class,启动插件中的该Activity并传递不同的参数,以显示不同的 Composable 组件。说白了就是借助一个空壳 Activity 来做跳板去展示不同的 Composable 。

首先在工程中新建一个 module 模块,将 build.gradle 中的 'com.android.library' plugins配置改为 'com.android.application',因为这个模块是当成一个 application 模块开发的,最终以 apk 的形式提供插件。然后在其中新建一个 PluginActivity 作为跳板 Activity,并新建两个测试的 Composable 页面。

在这里插入图片描述

PluginActivity 的内容如下:

class PluginActivity: ComponentActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        val type = intent.getStringExtra("type") ?: "NewsList"        setContent {            MaterialTheme {                if (type == "NewsList") {                    NewsList()                } else if (type == "NewsDetail") {                    NewsDetail()                }            }        }    }}

这里就是简单的根据 intent 读取的 type 类型来判断,如果是 NewsList 就显示一个新闻列表的 Composable 页面, 如果是 NewsDetail 就显示一个新闻详情的 Composable 页面。

NewsList 内容如下:

@Composablefun NewsList() {    LazyColumn(        Modifier.fillMaxSize().background(Color.Gray),        contentPadding = PaddingValues(15.dp),        verticalArrangement = Arrangement.spacedBy(10.dp)    ) {        items(50) { index ->            NewsItem("我是第 $index 条新闻")        }    }}@Composableprivate fun NewsItem(    text : String,    modifier: Modifier = Modifier,    bgColor: Color = Color.White,    fontColor: Color = Color.Black,) {    Card(        elevation = 8.dp,        modifier = modifier.fillMaxWidth(),        backgroundColor = bgColor    ) {        Box(            Modifier.fillMaxWidth().padding(15.dp),            contentAlignment = Alignment.Center        ) {            Text(text = text, fontSize = 20.sp, color = fontColor)        }    }}

NewsDetail 内容如下:

@Composablefun NewsDetail() {    Column {        Text(text = "我是插件中的新闻详情页面".repeat(100))    }}

执行 assembleDebug,将生成的 apk 文件拷贝到宿主 app 模块的 assets 目录下,以便在应用启动后从其中拷贝到存储卡(实际项目中应当从服务器下载)。

在这里插入图片描述

然后在宿主app模块的 AndroidManifest.xml 中注册插件中定义的 PluginActivity 进行占坑,这里爆红也没有关系,不会影响打包。

在这里插入图片描述

然后在app模块中定义一个 PluginManager 类,主要负责加载插件中的 Class

import android.annotation.SuppressLintimport android.content.Contextimport dalvik.system.DexClassLoaderimport java.io.Fileimport java.lang.reflect.Array.newInstanceimport java.lang.reflect.Fieldclass PluginManager private constructor() {    companion object {        var pluginClassLoader : DexClassLoader? = null        fun loadPlugin(context: Context) {            val inputStream = context.assets.open("news_lib.apk")            val filesDir = context.externalCacheDir            val apkFile = File(filesDir?.absolutePath, "news_lib.apk")            apkFile.writeBytes(inputStream.readBytes())            val dexFile = File(filesDir, "dex")            if (!dexFile.exists()) dexFile.mkdirs()            println("输出dex路径: $dexFile")            pluginClassLoader = DexClassLoader(apkFile.absolutePath, dexFile.absolutePath, null, this.javaClass.classLoader)        }        fun loadClass(className: String): Class<*>? {            try {                if (pluginClassLoader == null) {                    println("pluginClassLoader is null")                }                return pluginClassLoader?.loadClass(className)            } catch (e: ClassNotFoundException) {                println("loadClass ClassNotFoundException: $className")            }            return null        }                @SuppressLint("DiscouragedPrivateApi")        fun mergeDexElement(context: Context) : Boolean{            try {                val clazz = Class.forName("dalvik.system.BaseDexClassLoader")                val pathListField: Field = clazz.getDeclaredField("pathList")                pathListField.isAccessible = true                val dexPathListClass = Class.forName("dalvik.system.DexPathList")                val dexElementsField = dexPathListClass.getDeclaredField("dexElements")                dexElementsField.isAccessible = true                // 宿主的 类加载器                val pathClassLoader: ClassLoader = context.classLoader                // DexPathList类的对象                val hostPathListObj = pathListField[pathClassLoader]                // 宿主的 dexElements                val hostDexElements = dexElementsField[hostPathListObj] as Array<*>                // 插件的 类加载器                val dexClassLoader = pluginClassLoader ?: return false                // DexPathList类的对象                val pluginPathListObj = pathListField[dexClassLoader]                // 插件的 dexElements                val pluginDexElements = dexElementsField[pluginPathListObj] as Array<*>                val hostDexSize = hostDexElements.size                val pluginDexSize = pluginDexElements.size                // 宿主dexElements = 宿主dexElements + 插件dexElements                // 创建一个新数组                val newDexElements = hostDexElements.javaClass.componentType?.let {                    newInstance(it, hostDexSize + pluginDexSize)                } as Array<*>                System.arraycopy(hostDexElements, 0, newDexElements, 0, hostDexSize)                System.arraycopy(pluginDexElements, 0, newDexElements, hostDexSize, pluginDexSize)                // 赋值 hostDexElements = newDexElements                dexElementsField[hostPathListObj] = newDexElements                return true            } catch (e: Exception) {                println("mergeDexElement: $e")            }            return false        }    }}

这里的原理就不多介绍了,网上相关的文章已经有很多了,如有不了解的可以自行搜索。这里我使用的代码基本上也是从其他地方搬过来参考的。上面 PluginManager 类中定义了三个方法:loadPlugin() 方法负责将 assets 中的 apk 拷贝到外置存储卡中应用的缓存目录,并定义一个用于加载插件中的 ClassDexClassLoaderloadClass() 方法就是使用该ClassLoader根据指定的className进行加载并返回 Class的;mergeDexElement() 方法则是将插件中的dexElements数组合并到宿主的dexElements数组中,以便加载的插件Class能被宿主应用识别。

接下来定义一个 PluginViewModel ,分别针对上面 PluginManager 中的三个方法进行调用处理,并向 Composable 公开相应的状态:

class PluginViewModel: ViewModel() {    private val _isPluginLoadSuccess = MutableStateFlow(false)    val isPluginLoadSuccess = _isPluginLoadSuccess.asStateFlow()    private val _isMergeDexSuccess = MutableStateFlow(false)    val isMergeDexSuccess = _isMergeDexSuccess.asStateFlow()    var pluginActivityClass by mutableStateOf<Class<*>?>(null)        private set    fun loadPlugin(context: Context) {        viewModelScope.launch {            withContext(Dispatchers.IO) {                PluginManager.loadPlugin(context)                if (PluginManager.pluginClassLoader != null) {                    _isPluginLoadSuccess.value = true                }            }        }    }    fun mergeDex(context: Context) {        viewModelScope.launch {            withContext(Dispatchers.IO) {                if (PluginManager.mergeDexElement(context)) {                    _isMergeDexSuccess.value = true                }            }        }    }    fun loadClass(name: String) {        viewModelScope.launch {            withContext(Dispatchers.IO) {                pluginActivityClass = PluginManager.loadClass(name)            }        }    }}

最后就是一个用于的测试页面,定义一个 HostScreen 作为宿主中的页面进行展示:

const val PluginActivityClassName = "com.fly.compose.plugin.news.PluginActivity"@OptIn(ExperimentalLifecycleComposeApi::class)@Composablefun HostScreen(viewModel: PluginViewModel = viewModel()) {    val context = LocalContext.current    Column(        horizontalAlignment = Alignment.CenterHorizontally,        verticalArrangement = Arrangement.spacedBy(20.dp)    ) {        Text(text = "当前是宿主中的Composable页面")        Button(onClick = { viewModel.loadPlugin(context) }) {            Text(text = "点击加载插件Classloader")        }        val isLoadSuccess = viewModel.isPluginLoadSuccess.collectAsStateWithLifecycle()        Text(text = "插件Classloader是否加载成功:${isLoadSuccess.value}")        if (isLoadSuccess.value) {            Button(onClick = { viewModel.mergeDex(context) }) {                Text(text = "点击合并插件Dex到宿主中")            }            val isMergeDexSuccess = viewModel.isMergeDexSuccess.collectAsStateWithLifecycle()            Text(text = "合并插件Dex到宿主是否成功:${isMergeDexSuccess.value}")            if (isMergeDexSuccess.value) {                Button(onClick = { viewModel.loadClass(PluginActivityClassName) }) {                    Text(text = "点击加载插件中的 PluginActivity.Class")                }                if (viewModel.pluginActivityClass != null) {                    Text(text = "加载插件中的 PluginActivity.Class 的结果:\n${viewModel.pluginActivityClass?.canonicalName}")                    val intent = Intent(context, viewModel.pluginActivityClass)                    Button(onClick = {                        context.startActivity(intent.apply { putExtra("type", "NewsList") })                    }) {                        Text(text = "点击显示插件中的 NewsList 页面")                    }                    Button(onClick = {                        context.startActivity(intent.apply { putExtra("type", "NewsDetail") })                    }) {                        Text(text = "点击显示插件中的 NewsDetail 页面")                    }                }            }        }    }}

运行效果:

在这里插入图片描述

可以看到,这种方式是完全可行的,几乎毫无压力。

对于这种占坑的方式,它的好处是每个插件只需要提供一个用于在宿主中占位的 PluginActivity 即可,这是相对于以前传统的View开发而言的,因为以前 View 并不能承担屏幕级内容展示的角色而且也没有独立的导航功能,所以需要借助大量的 Activity 类,如果在以前传统的开发中只允许你用一个Activity 类,然后页面在不同的View之间切来切去,恐怕要疯掉了。但是现在不同了,Composable 组件可独立负责屏幕级内容的展示而且也具备独立于Activity的导航功能,它可以独挑大梁,所以说基本不需要太多的 Activity 类,需要在宿主中占位的 Activity 数量自然也就很少。

但是这种方式并不一定能满足所有的场景,它的优点也是它的缺点,试想每一个插件都需要提供一个占位的Activity,插件多的情况下还是有可能出现大量的Activity类,还有一个严重的问题是,这种方式只能以 “跳转” 的形式打开新的页面来展示,因为借助的是一个Activity来当做 Composable的容器,也就是说,假如我想在当前页面的某个区域显示来自插件中的某个Composable组件,这种方式就无法实现。

直接加载插件中的 Composable 组件

为了能在宿主中当前页面的某个局部区域显示来自插件中的Composable组件,就不能采取占坑Activity做跳板的这种方式了,我们可以考虑去掉插件中的这个Activity,也就是说每个插件中只保留纯 Composable 的组件代码(纯kotlin代码),然后打成apk插件给宿主加载,既然宿主中都可以加载插件中的类了,那应该可以很轻松地通过反射直接调用插件中的Composable函数。

因为 kotlin 代码在最终被编译成DEX文件之前,要先翻译成对应的Java代码,而我们知道在Java当中是没有顶层函数这种概念的,每一个Class文件都必须对应一个独立的Java类且Class文件的名称必须和Java类的名称保持一致。因此不管我们的 Composable 组件是写在哪个 xx.kt 文件当中,它最终都会被翻译成一个 Java 类,然后我们在宿主中加载该 Java 类并调用该类中的 Composable 方法不就可以了。

这个想法似乎很完美,但是事情并没有想象中的那样简单,很快我就发现了一个残酷的现实,我们知道,Compose 编译器会在编译过程中对 Composable 函数施加一些 “黑魔法”,它会篡改编译期的 IR,因此最终的 Composable 函数会被添加一些额外的参数。例如,前面代码中的 NewsList.ktNewsDetail.kt,使用反编译工具查看它们最终的形态是长下面这样:

在这里插入图片描述

在这里插入图片描述

这里可以看到 Compose 编译器为每个 Composable 函数注入了一个 $composer 参数(用于重组)和一个 $changed 参数(用于参数比较和跳过重组),也就是说即便一个无参的 Composable 函数也会被注入这两个参数,那么这就有问题了,即便我们能在宿主中加载该类并通过反射获取了 Composable 函数的句柄引用,但是我们却无法调用它们,因为我们无法提供 $composer$changed 参数,只有 Compose runtime 才知道如何提供这些参数。

这就很尴尬了,这相当于我们想调用一个只有上帝才知道该如何去调用的方法。

那么这样难道就没有办法了吗?其实我们想要的就是在宿主中调用 Composable 函数而已,可以换一种思路,既然直接调用不行,那就间接地调用。

首先,我们可以通过在一个类中定义 Composable lambda 类型的属性来存储 Composable 函数,也就是提供一个 Composable 函数类型的属性成员。例如可以这样写:

class ComposeProxy {    val content1 : (@Composable () -> Unit) = {        MyBox(Color.Red, "我是插件中的Composable组件1")    }    val content2 : (@Composable () -> Unit) = {        MyBox(Color.Magenta, "我是插件中的Composable组件2")    }    @Composable    fun MyBox(color: Color, text: String) {        Box(            modifier = Modifier.requiredSize(150.dp).background(color).padding(10.dp),            contentAlignment = Alignment.Center        ) {            Text(text = text, color = Color.White, fontSize = 15.sp)        }    }}

这里 ComposeProxy 类中的成员属性 content1content2 的类型都是 Composable 函数类型,即 @Composable () -> Unit,实际上就是定义了两个 Composable lambda。在其 lambda block 块中可以调用真正的 Composable 业务组件,因为这本质上还是在Composable中调用另一个Composable。至于为什么要这样写,可看下面编译后的结果:

在这里插入图片描述

可以看到,翻译成 Java 代码之后,ComposeProxy 中的 content1content2 变成了两个 public 方法 getContent1()getContent2(),而这两个方法是没有参数的,因此我们就可以通过加载类后反射调用它们。注意到它们返回的类型是 Function2,它其实对应的就是 Kotlin 中的 @Composable () -> Unit函数类型,因为在 Java 的世界中没有所谓的函数类型,取而代之的是使用类似函数的接口类型来对应Kotlin 中的函数类型(Function0...Function22,最多有22个)。

因此我们可以认为在编译期, Function2@Composable () -> Unit 二者是等价的,因为后者会被翻译成前者。

其实我们不必关心返回的到底是 Function 几,因为我们最终会通过 Java 的反射API来调用 getContent1()getContent2()方法,也就是执行 Method.invoke(),它返回的是一个 Object 对象(如果是用 kotlin 代码来写那就是返回一个 Any 类型的对象),因此我们可以在编写加载插件代码的时候将这个 Object (Any) 对象强转成 @Composable () -> Unit 函数类型。然后我们就在宿主中得到了一个 @Composable () -> Unit 函数类型的对象,那么我们就可以调用该函数对象的 invoke 方法了(即调用了 Composable 函数)。

下面修改一下 PluginViewModel ,在其中添加如下代码:

class PluginViewModel: ViewModel() {// ...省略其它无关代码val composeProxyClassName = "com.fly.compose.plugin.news.ComposeProxy"    var pluginComposable1 by mutableStateOf<@Composable () -> Unit>({})    var pluginComposable2 by mutableStateOf<@Composable () -> Unit>({})    var isLoadPluginComposablesSuccess by mutableStateOf(false)    fun loadPluginComposables() {        viewModelScope.launch {            withContext(Dispatchers.IO) {                val composeProxyClass = PluginManager.loadClass(composeProxyClassName)                composeProxyClass?.let { proxyClass ->                    val getContent1Method: Method = proxyClass.getDeclaredMethod("getContent1")                    val getContent2Method: Method = proxyClass.getDeclaredMethod("getContent2")                    val obj = proxyClass.newInstance()                    pluginComposable1 = getContent1Method.invoke(obj) as (@Composable () -> Unit)                    pluginComposable2 = getContent2Method.invoke(obj) as (@Composable () -> Unit)                    isLoadPluginComposablesSuccess = true                }            }        }    }}

修改 HostScreen 测试代码如下:

@Composablefun HostScreen(viewModel: PluginViewModel = viewModel()) {    CommonLayout(viewModel) {        Button(onClick = { viewModel.loadPluginComposables() }) {            Text(text = "点击加载插件中的 Composables")        }        // 加载成功后调用插件中的Composable函数        if (viewModel.isLoadPluginComposablesSuccess) {            viewModel.pluginComposable1()            viewModel.pluginComposable2()        }    }}@OptIn(ExperimentalLifecycleComposeApi::class)@Composableprivate fun CommonLayout(    viewModel: PluginViewModel = viewModel(),    content: @Composable () -> Unit) {    val context = LocalContext.current    Column(        horizontalAlignment = Alignment.CenterHorizontally,        verticalArrangement = Arrangement.spacedBy(20.dp)    ) {        Text(text = "当前是宿主中的Composable页面")        Button(onClick = { viewModel.loadPlugin(context) }) {            Text(text = "点击加载插件Classloader")        }        val isLoadSuccess = viewModel.isPluginLoadSuccess.collectAsStateWithLifecycle()        Text(text = "插件Classloader是否加载成功:${isLoadSuccess.value}")        if (isLoadSuccess.value) {            Button(onClick = { viewModel.mergeDex(context) }) {                Text(text = "点击合并插件Dex到宿主中")            }            val isMergeDexSuccess = viewModel.isMergeDexSuccess.collectAsStateWithLifecycle()            Text(text = "合并插件Dex到宿主是否成功:${isMergeDexSuccess.value}")            if (isMergeDexSuccess.value) {                content()            }        }    }}

重新打包 news_lib 模块生成 apk,并拷贝到 app 模块中的 assets 目录,然后运行 app,查看效果:

在这里插入图片描述

可以看到,我们成功的在宿主的 Composable 界面中直接加载了来自插件中的 Composable 组件,非常完美。这也意味着我们可以在宿主中任意的 Activity/FragmentComposable 中调用来自插件中的 Composable。也就是说我们的插件中可以不保留任何 Activity 类,只保留 Composable 组件及与其相关的业务逻辑代码即可。

相比传统的插件化方式,这种方式再也不用考虑如何去绕开 AMS 对 Activity 的校验,无需在 Manifest 中占位 Activity,也无需在系统源码中寻找各种Hook点通过动态代理的方式偷换 Activity ,因为插件中压根就不需要 Activity 了。从此以后,插件化可以走上非常纯净的道路。

关于 Compose 的插件化技术探索到这里还没完,最后还有一种方式来尝试一下。

以俄罗斯套娃模式加载插件中的 Composable 组件

这种方式的灵感来自于 Composable 和 Android View 的互操作性。例如,要在 Composable 中显示一个 Android 的 View 可以通过如下方式:

@Composablefun SomeComposable() {    AndroidView(factory = { context ->       // android.webkit.WebView        WebView(context).apply {            settings.javaScriptEnabled = true            webViewClient = WebViewClient()            loadUrl("https://xxxx.com")        }    }, modifier = Modifier.fillMaxSize())}

其中 AndroidView 是一个 Composable 函数,可在其内部嵌套 Android 原生 View 组件。

另一方面,可以通过调用 ComposeViewsetContent{} 来设置其 Composable 内容,这是因为 ActivitysetContent{ } 方法内部其实就是创建了一个 ComposeView 来执行 setContent 的:

在这里插入图片描述

ComposeView 是一个公开的类,也就是说我们也可以这样调用:

ComposeView(context).apply {    setContent {        ComposableExample()    }}

由于 ComposeView 是一个标准的 Android View,因此我们可以这样调用:

@Composablefun SomeComposable() {    AndroidView(factory = { context ->        ComposeView(context).apply {            setContent {                ComposableExample()            }        }    }, modifier = Modifier.fillMaxSize())}

于是我们的俄罗斯套娃版本的插件化方案就诞生了:

在这里插入图片描述

插件只需向宿主提供 ComposeView 的获取方式即可,宿主获取到来自插件的 ComposeView 后可以借助 AndroidView 这个 Composable 来嵌入到宿主的界面中。而插件内部的 Composable 无需向宿主暴露,包裹在 ComposeView 的内部即可。

跟前面类似的,在插件中定义一个 ComposeViewProxy 类,然后在其中定义一个类型为 Context.(String) -> ComposeView 函数类型的成员属性:

class ComposeViewProxy {    val pluginView: (Context.(String) -> ComposeView) = { name ->        ComposeView(this).apply {            setContent {                if (name == "content1") {                    MyBox(Color.Red, "我是插件中的Composable组件1")                } else if (name == "content2") {                    MyBox(Color.Magenta, "我是插件中的Composable组件2")                }            }        }    }    @Composable    fun MyBox(color: Color, text: String) {        Box(            modifier = Modifier.requiredSize(150.dp).background(color).padding(10.dp),            contentAlignment = Alignment.Center        ) {            Text(text = text, color = Color.White, fontSize = 15.sp)        }    }}

将生成的插件 apk 使用反编译工具查看:

在这里插入图片描述

接下来只需要在宿主中加载 ComposeViewProxy 类,然后反射调用 getPluginView() 方法,拿到返回结果即可在宿主的 AndroidView 中使用了。

修改 PluginViewModel ,在其中添加如下代码:

class PluginViewModel: ViewModel() {// ...省略其它无关代码val composeViewProxyClassName = "com.fly.compose.plugin.news.ComposeViewProxy"    var pluginView by mutableStateOf<Context.(String) -> ComposeView>({ComposeView(this)})    var isLoadPluginViewSuccess by mutableStateOf(false)    fun loadPluginView() {        viewModelScope.launch {            withContext(Dispatchers.IO) {                val composeViewProxyClass = PluginManager.loadClass(composeViewProxyClassName)                composeViewProxyClass?.let { proxyClass ->                    val getPluginViewMethod: Method = proxyClass.getDeclaredMethod("getPluginView")                    val obj = proxyClass.newInstance()                    pluginView = getPluginViewMethod.invoke(obj) as (Context.(String) -> ComposeView)                    isLoadPluginViewSuccess = true                }            }        }    }}

修改 HostScreen 测试代码如下:

@Composablefun HostScreen(viewModel: PluginViewModel = viewModel()) {    CommonLayout(viewModel) {        Button(onClick = { viewModel.loadPluginView() }) {            Text(text = "点击加载插件中的 ComposeView")        }        // 加载成功后调用插件中的ComposeView        if (viewModel.isLoadPluginViewSuccess) {            SimpleAndroidView { context ->                viewModel.pluginView(context, "content1")            }            SimpleAndroidView { context ->                viewModel.pluginView(context, "content2")            }        }    }}@Composableprivate fun <T: View> SimpleAndroidView(factory: (Context) -> T) {    AndroidView(        factory = { context -> factory(context) },        modifier = Modifier.wrapContentSize()    )}@OptIn(ExperimentalLifecycleComposeApi::class)@Composableprivate fun CommonLayout(    viewModel: PluginViewModel = viewModel(),    content: @Composable () -> Unit) {    val context = LocalContext.current    Column(        horizontalAlignment = Alignment.CenterHorizontally,        verticalArrangement = Arrangement.spacedBy(20.dp)    ) {        Text(text = "当前是宿主中的Composable页面")        Button(onClick = { viewModel.loadPlugin(context) }) {            Text(text = "点击加载插件Classloader")        }        val isLoadSuccess = viewModel.isPluginLoadSuccess.collectAsStateWithLifecycle()        Text(text = "插件Classloader是否加载成功:${isLoadSuccess.value}")        if (isLoadSuccess.value) {            Button(onClick = { viewModel.mergeDex(context) }) {                Text(text = "点击合并插件Dex到宿主中")            }            val isMergeDexSuccess = viewModel.isMergeDexSuccess.collectAsStateWithLifecycle()            Text(text = "合并插件Dex到宿主是否成功:${isMergeDexSuccess.value}")            if (isMergeDexSuccess.value) {                content()            }        }    }}

运行效果:

在这里插入图片描述

运行效果跟前一种方案相同,这种方式的代码逻辑也跟前一种几乎一样,但相比前一种,这种方案插件无需向宿主直接暴露 Composable 组件,而只提供 ComposeView

实际上这种方式开发的插件可以同时混合使用原生 ViewComposable,例如在项目改造的过程中,有可能界面中的一部分还没来得及改造完毕就需要发布应用版本,此时这部分可仍然使用原来的 View 实现,而另一部分使用 Composable 全新实现的部分可通过 ComposeView 嵌入到整个页面中,然后将整个页面再作为一个单独的 View 提供给宿主使用。

来源地址:https://blog.csdn.net/lyabc123456/article/details/128755247

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Jetpack Compose 中的动态加载、插件化技术探索

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Jetpack Compose 中的动态加载、插件化技术探索

在传统的 Android 开发模式中,由于界面过分依赖于 Activity、Fragment这样的组件,一个业务模块中往往会存在着大量的 Activity 类,因此诞生了很多的插件化框架,这些插件化框架基本都是想方设法的使用各种Hook/反
2023-08-30

编程热搜

目录