什么是Compose

Jetpack Compose是谷歌官方推荐的Android UI完成办法,防止了Android传统View在制作、编写、功能等方面的种种缺点,详细运用办法请参阅官方文档。

Compose与插件化

想必我们都触摸过或许了解过插件化开发,没触摸过的小伙伴们要补补课啦。在以往插件化开发进程中,清单文件中要有Activity(宿主中)占位,还要hook、反射手段把宿主的Activity生命周期传递到插件中,这一进程往往因为Android碎片化,机型适配问题严重,对插件化开发形成重重阻止,选用ComposeUI办法,所有视图都是composable-widget(这一点和跨渠道的Flutter很像),ComposeUI给插件化也带来了优化空间,在宿主中加载插件的某一个Composable组件更为灵活,功能更高。留意:Android
View办法也能够完成本文的功能,可是View制作功率低,功能差。运用Compose完成插件化的原理与传统插件化是共同的,主要是省掉了黑科技创立Activity,传递生命周期的进程。

实践

先摆上github源码地址

项目创立

  1. 创立一个项目,没什么新鲜东西,这里就不放图了
  2. 创立插件项目:在项目里新增module,我这里命名为plugin了,在项目根目录gradle.properties文件中新增变量配置runAlone=true用于配置管理plugin是否能够独立运转
  3. 修正plugin模块的gradle配置如下:
plugins {
    id 'org.jetbrains.kotlin.android'
}
def aloneRun = runAlone.toBoolean()
//根据runalone配置,动态增加gradle的plugin
if (aloneRun) {
    plugins.apply('com.android.application')
} else {
    plugins.apply('com.android.library')
}
...
  defaultConfig {
        if (aloneRun) {
            //plugin模块作为application时的applicationId
            applicationId "tech.wcw.compose.plugin"
        }
  }
...
        //plugin模块作为application时增加源文件目录和清单文件,便利开发和测试(compose办法能够经过@Preview预览,按需增加)
        if (aloneRun) {
            sourceSets {
                main.java.srcDirs += 'src/alone/java'
                main.manifest.srcFile 'src/alone/AndroidManifest.xml'
            }
        }

插件完成

  • 办法一:
    此处有坑!!!声明的Composable组件编译后带着(Composer,Int)参数,后一个参数与重组休戚相关。详见Compose完成插件化(二)在调用时假如不能正确传参,可能会产生奇怪的bug。

    经过@Composable注解声明了Composable组件。
@Composable
 fun pluginView(param: String) {
     Log.i(tag, "pluginView v1 重组")
     Box(
         modifier = Modifier
             .background(Color.Red)
             .fillMaxWidth()
             .height(40.dp)
     ) {
         Text(text = "收到宿主传参 $param")
     }
 }
  • 办法二:
    lambda表达式间接调用,即将Composable组件声明为一个函数表达式,在编译后,java中表明为FunctionN(0~23),此处与Composable组件是否传参有关,能够在编译后的文件中查看详细类型
val pluginView: (@Composable () -> Unit) = {
    Log.i(tag, "pluginView v2 重组")
    Box(
        modifier = Modifier
            .background(Color.Blue)
            .fillMaxWidth()
            .height(40.dp)
    )
}

宿主完成

  1. 将plugin打包放入assets中(demo演示用,实践开发应该是从服务器下载)
  2. 获取dex,创立classLoader (demo简略处理,未进行兼并dex)
fun loadPlugin(context: Context) {
    val inputStream = context.assets.open("plugin.apk")
    val filesDir = context.externalCacheDir
    val apkFile = File(filesDir?.absolutePath, "plugin.apk")
    apkFile.writeBytes(inputStream.readBytes())
    val dexFile = File(filesDir, "dex")
    if (!dexFile.exists()) dexFile.mkdirs()
    println("dexPath: $dexFile")
    pluginClassLoader = DexClassLoader(
        apkFile.absolutePath,
        dexFile.absolutePath,
        null,
        this.javaClass.classLoader
    )
}

3.加载插件,经过ClassLoader获取到Class,经过反射得到对应的办法,传参调用即可。
留意:办法一自己传参Composer和changed,后者会对重组有影响,且changed参数与Composable函数参数有关,必定要留意
办法二不必自己传参,与宿主内声明一个Composable函数,再运用的办法共同。

private fun applyPluginV1() {
    val plugin = PluginManager.loadClass("tech.wcw.compose.plugin.PluginV1")
    plugin?.let {
        val method: Method =
            plugin.getDeclaredMethod(
                "pluginView",
                String::class.java,
                Composer::class.java,
                Int::class.java
            )
        method.isAccessible = true
        pluginV1Obj = plugin.newInstance()
        pluginV1Method = method
        applyV1Success = true
    }
}
private fun applyPluginV2() {
    val plugin = PluginManager.loadClass("tech.wcw.compose.plugin.PluginV2")
    plugin?.let {
        val method: Method = plugin.getDeclaredMethod("getPluginView")
        method.isAccessible = true
        val obj = plugin.newInstance()
        pluginV2Compose = method.invoke(obj) as (@Composable () -> Unit)
        applyV2Success = true
    }
}

弥补内容

获取到插件内组件的method(办法一)或许插件内Composable(办法二)的引用后,插件内组件的运用与普通办法是几乎共同的,我的忽略,没有把github源码中的运用代码贴出来。此外demo源码中弥补了SideEffect、LauncherEffect的代码块,用于验证Compose组件重组进程。
文章内容见下文:

宿主内运用办法

办法一

选用办法一获取到的method,需求我们自己传参composer:Composer和changed:Int,特别是后者,与Composable组件声明时的函数参数有关,且会影响重组进程,后续独自写一篇关于这个参数的总结。在前文中,我们已经获取到pluginV1Method和pluginV1Obj,直接运用反射办法invoke在宿主内的组件中运用即可

if (applyV1Success) {
    pluginV1Method!!.invoke(pluginV1Obj, param, currentComposer, 0b000)
}

办法二

办法二获取到的pluginV2Compose是一个Composable的表达式,这个运用办法就简略了,与声明在宿主内组件的运用办法共同

if (applyV2Success) {
    pluginV2Compose()
}

Composse插件化对Effect的影响

在插件内的Effect是否能正常运转呢?在插件的组件中增加LauncherEffect、SideEffect后是能正常履行的。详细运转情况能够下载demo或许自己完成下。

//声明effect
@Composable
fun pluginView2() {
    Log.i(tag, "pluginView2 重组")
    val ret = remember {
        mutableStateOf(System.currentTimeMillis())
    }
    Button(onClick = {
        ret.value = System.currentTimeMillis()
    }) {
        Text(text = "插件内组件 点击自更新 ${ret.value}")
        LaunchedEffect(ret.value) {
            Log.i(tag, "pluginView2 LaunchedEffect打印")
        }
        SideEffect {
            Log.i(tag, "pluginView2 SideEffect内打印")
        }
    }
//打印结果
2023-08-04 10:43:32.730 13207-13207 PluginV1                tech.wcw.compose.plugin.demo         I  pluginView2 SideEffect内打印
2023-08-04 10:43:32.750 13207-13207 PluginV1                tech.wcw.compose.plugin.demo         I  pluginView2 LaunchedEffect打印

功能

两种办法都运用了反射,毫无疑问是对功能肯定是有必定影响的,在运用时要防止重复反射。现在的设备功能都还不错,在不滥用反射,合理运用资源的情况下,对功能的影响是微乎其微的。
获取class和反射newInstance、获取method、Comppose组件增加了相关打印,demo中都有,小伙伴们能够自己运转下试试。

2023-08-04 10:52:32.501 13662-13662 MainActivity            tech.wcw.compose.plugin.demo         I  applyPluginV1: PluginV1 class加载耗时 1
2023-08-04 10:52:32.504 13662-13662 MainActivity            tech.wcw.compose.plugin.demo         I  applyPluginV1: PluginV1 method 加载耗时 2
2023-08-04 10:52:32.533 13662-13662 MainActivity            tech.wcw.compose.plugin.demo         I  applyPluginV1: PluginV1 newInstance 耗时 28
2023-08-04 10:52:32.534 13662-13662 MainActivity            tech.wcw.compose.plugin.demo         I  applyPluginV1: PluginV1 从加载到newInstance总耗时 34

最终

假如文内或源码内有过错,欢迎我们指正和批判。
ps:走过路过的朋友,动动你们发财的小手,点赞支撑,点点关注啊。