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

news2024/9/29 23:29:58

在传统的 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 内容如下:

@Composable
fun NewsList() {
    LazyColumn(
        Modifier.fillMaxSize().background(Color.Gray),
        contentPadding = PaddingValues(15.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        items(50) { index ->
            NewsItem("我是第 $index 条新闻")
        }
    }
}

@Composable
private 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 内容如下:

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

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

在这里插入图片描述

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

在这里插入图片描述

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

import android.annotation.SuppressLint
import android.content.Context
import dalvik.system.DexClassLoader
import java.io.File
import java.lang.reflect.Array.newInstance
import java.lang.reflect.Field

class 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
        }

        /**
         * 合并DexElement数组: 宿主新dexElements = 宿主原始dexElements + 插件dexElements
         * 1、创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。
         * 2、获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
         * 3、合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。
         * 4、最后通过反射将新的 Element[] 赋值给宿主的 dexElements。
         */
        @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)
@Composable
fun 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<Composer, Integer, Unit>,它其实对应的就是 Kotlin 中的 @Composable () -> Unit函数类型,因为在 Java 的世界中没有所谓的函数类型,取而代之的是使用类似函数的接口类型来对应Kotlin 中的函数类型(Function0...Function22,最多有22个)。

因此我们可以认为在编译期, Function2<Composer, Integer, Unit>@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 测试代码如下:

@Composable
fun HostScreen(viewModel: PluginViewModel = viewModel()) {
    CommonLayout(viewModel) {
        Button(onClick = { viewModel.loadPluginComposables() }) {
            Text(text = "点击加载插件中的 Composables")
        }
        // 加载成功后调用插件中的Composable函数
        if (viewModel.isLoadPluginComposablesSuccess) {
            viewModel.pluginComposable1()
            viewModel.pluginComposable2()
        }
    }
}
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
private 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 可以通过如下方式:

@Composable
fun 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,因此我们可以这样调用:

@Composable
fun 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 测试代码如下:

@Composable
fun 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")
            }
        }
    }
}

@Composable
private fun <T: View> SimpleAndroidView(factory: (Context) -> T) {
    AndroidView(
        factory = { context -> factory(context) },
        modifier = Modifier.wrapContentSize()
    )
}

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
private 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 提供给宿主使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/398633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++11auto

autoc11中auto并不代表一种实际的数据类型&#xff0c;它只是一个类型声明的占位符&#xff0c;auto也并不是再所有场景下都能推导出变量的实际类型&#xff0c;使用auto不需要进行初始化&#xff0c;让编译器推导出它的实际类型&#xff0c;再编译阶段将auto占位符替换为真正的…

没有比这更详细的-压测工具Jmeter介绍及使用了

一、压测工具选型 1.1、前言 压力测试是每一个Web应用程序上线之前都需要做的一个测试&#xff0c;他可以帮助我们发现系统中的瓶颈问题&#xff0c;减少发布到生产环境后出问题的几率&#xff1b;预估系统的承载能力&#xff0c;使我们能根据其做出一些应对措施。所以压力测…

【Linux基础篇】一、Linux入门基础命令

一、Linux基础命令 1、Linux的目录结构 /&#xff0c;根目录是最顶级的目录了Linux只有一个顶级目录&#xff1a;/路径描述的层次关系同样适用/来表示/home/itheima/a.txt&#xff0c;表示根目录下的home文件夹内有itheima文件夹&#xff0c;内有a.txt 2、ls命令 功能&#…

数字孪生GIS智慧风场Web3D可视化运维系统

随着国家双碳目标的实施&#xff0c;新能源发电方式逐渐代替了污染大气层的火力发电&#xff0c;其中风力发电相比于光伏发电具有能量密度高、发电小时数长、生命周期达20-25年之久等独特的优势。风能取之不尽、用之不竭&#xff0c;在新型能源互联网下&#xff0c;风力发电有可…

三、GC算法垃圾回收器

文章目录&#xff08;持续更新中... ...CMS and G1&#xff09;概述如何判断对象存活引用计数法根可达算法GC算法(垃圾回收算法)分代收集理论标记-清除算法复制算法标记-整理算法垃圾回收器概述Serial收集器Parallel收集器Parallel Old收集器CMS收集器G1收集器常用的收集器组合…

STM32实战项目-串口打印

前言&#xff1a; 本小结主要实现串口打印功能&#xff0c;主要将上一结的状态机运行次数&#xff0c;通过串口在串口终端上打印出来&#xff0c;硬件电路上主要是TTL转USB驱动电路&#xff0c;软件上主要有状态机函数&#xff0c;串口发送函数等调试工具是串口助手。 目录 1…

机器学习学习记录1:假设空间

我们可以把学习过程看作一个在所有假设组成的空间中进行搜索的过程&#xff0c;搜索目标是找到与训练集"匹配" 的假设&#xff0c;即能够将训练集中的瓜判断正确的假设.假设的表示一旦确定&#xff0c;假设空间及其规模大小就确定了.对于西瓜问题&#xff0c;这里我们…

[MySQL索引]2.索引的底层原理(一)

索引的底层原理&#xff08;一&#xff09;B-树索引B树索引tips: ​ 通过使用malloc/new来申请4字节的内存&#xff0c;但是操作系统不是说每一次用户申请4字节内存&#xff0c;我就只分配4字节&#xff0c;这样申请次数多了就要涉及频繁的用户态和内核态的切换&#xff0c;开销…

AM5728(AM5708)开发实战之安装Debian 10桌面操作系统

一 环境搭建 准备一个SD卡启动卡&#xff0c;能够正常引导板卡启动&#xff0c;后续会把Debian 10镜像安装到SD卡ext4分区 准备两个U盘&#xff0c;一个格式化成fat32文件系统&#xff0c;另一个格式化成ext4文件系统 下载Debian 10镜像&#xff0c;镜像名字为debian-10.4.0-a…

【前端】一个更底层库React,附教程

Reat是什么&#xff1f;React在UI里最火的也是最好的库&#xff0c;React是一个做UI的库&#xff0c;具体来说是做UI组件的库&#xff0c;专注于做mvc中的v&#xff0c;它是一个更底层的工具库。为你应用的每一个状态设计简洁的视图&#xff0c;当数据变动时 React 能高效更新并…

Linux发行版本与发行版的简单的介绍

Linux linux下有很多发行的版本&#xff0c;或者称之为魔改版本。以下介绍一些常见的版本&#xff0c;以避免名词的混淆。 linux是提供了一个内核&#xff0c;就像是谷歌的内核一样&#xff0c;QQ浏览器就是使用的谷歌的内核&#xff0c;也算是一个发行版本。 Ubuntu&#x…

课程项目简介

一、深度学习模型入门 1&#xff0c;什么是机器学习&#xff1f; Study of algorithms that improve their performance P at some task T with experience E. 研究在某个任务T上用经验E提高其性能P的算法。 well-defined learning task:<P,T,E> 明确的学习任务&#…

手把手开发一门程序语言JimLang (2)

根据爱因斯坦的相对论&#xff0c;物体的质量越大&#xff0c;时间过得越快&#xff0c;所以托更对于我的煎熬&#xff0c;远远比你们想象的还要痛苦…今天给大家来盘硬菜&#xff0c;也是前些时日预告过的JimLang的开发过程… Let’s go !!! 语法及解析 JimLang.g4 这里我们…

轮毂要怎么选?选大还是选小?

随着改装车的越来越火爆&#xff0c;汽车轮毂可选择的款式也越来越多&#xff0c;90%的人换轮毂&#xff0c;首先选的就是外观。大轮毂的款式多&#xff0c;外形大气好看&#xff0c;运动感十足&#xff0c; 那是不是选大轮毂就可以呢&#xff1f;不是的&#xff0c;汽车轮毂要…

全面吃透Java Stream流操作,让代码更加的优雅

文章目录1 认识Stream流1.1 什么是流1.2 流与集合1.2.1 流只能遍历一次1.2.2 外部迭代和内部迭代1.3 流操作1.3.1 中间操作1.3.2 终端操作1.3.3 使用流2 学会使用Stream流2.1 筛选和切片2.1.1 用谓词筛选2.1.2 筛选各异的元素2.1.3 截短流2.1.4 跳过元素2.2 映射2.2.1 map方法2…

Java经典面试题——对比 Vector、ArrayList、LinkedList 有何区别?

典型回答 这三者都是实现集合框架中的 List &#xff0c;也就是所谓的有序集合&#xff0c;因此具体功能也比较近似&#xff0c;比如都提供按照位置进行定位、添加或者删除的操作&#xff0c;都提供迭代器以遍历其内容等。但因为具体的设计区别&#xff0c;在行为、性能、线程…

详解CRC原理以及C语言实现

CRC检验原理 CRC&#xff08;Cyclic Redundancy Check&#xff09;校验是一种常用的数据校验方法&#xff0c;它通过计算数据的校验码来检测数据在传输过程中是否出现了错误。 CRC校验的基本原理是将数据按照一定的规则进行计算&#xff0c;得到一个固定长度的校验码&#xf…

JavaScript内改变this指向

之前我们说的都是代码内 this 的默认指向 今天我们要来说一下如何能改变 this 指向 也就是说, 你指向哪我不管, 我让你指向哪, 你就得指向哪 开局 在函数的原型( Function.prototype ) 上有三个方法 call apply bind 既然是在函数的原型上, 那么只要是函数就可以调用这三个方…

React(七):Router基本使用、嵌套路由、编程式导航、路由传参、懒加载

React&#xff08;七&#xff09;一、React-Router的基本使用1.安装和介绍2.路由的配置和跳转3.Navigate的使用4.如果找不到对应的路由路径&#xff1f;二、嵌套路由的用法三、编程式路由导航1.类组件中使用useNavigate2.函数式组件中使用useNavigate四、路由跳转传参1.设置好路…

小白学Pytorch 系列--Torch API(1)

小白学Pytorch 系列–Torch API Torch version 1.13 Tensors TORCH.IS_TENSOR 如果obj是PyTorch张量&#xff0c;则返回True。 注意&#xff0c;这个函数只是简单地执行isinstance(obj, Tensor)。使用isinstance 更适合用mypy进行类型检查&#xff0c;而且更显式-所以建议使…