Dynamic loading and plug-in technology exploration in Jetpack Compose

In the traditional Android development model, because the interface is overly dependent on Activitysuch Fragmentcomponents as , there are often a large number of Activityclasses in a business module, so many plug-in frameworks have been born, and these plug-in frameworks basically try to use various A Hook/reflection method to solve the problem of using unregistered components. After entering the world of Jetpack Compose, Activitythe role of is downplayed. Since a Composablecomponent can undertake a screen-level display, we no longer need so many Activityclasses in our application. As long as you like, you can even create a Activitysingle Pure Compose application.

This article mainly tries to explore several feasible solutions that can implement plug-in/dynamic loading in Jetpack Compose.

Access the Composable component in the plug-in in the way of Activity occupation

In fact, this method can also be done in traditional View development, but since we can only use one Activity in Compose, and the rest of the pages are implemented using Composable components, it feels more suitable for it. Therefore, the main idea is AndroidManifest.xmlto register a pit-occupied Activityclass in the host application, which Activityactually exists in the plug-in, and then load the ActivityClass in the plug-in in the host, start the class in the plug-in Activityand pass different parameters to display different Composable components. To put it bluntly, it is to use an empty shell Activity as a springboard to display different Composables.

First, create a new module module in the project, and 'com.android.library'change the plugins configuration in build.gradle 'com.android.application', because this module is developed as an application module, and finally provide plugins in the form of apk. Then create a new PluginActivityActivity as a springboard, and create two Composable pages for testing.

insert image description here

PluginActivityThe content is as follows:

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()
                }
            }
        }
    }
}

Here is a simple judgment based on the type read by the intent. If it is NewsList, a composable page of news list will be displayed. If it is NewsDetail, a composable page of news details will be displayed.

NewsListThe content is as follows:

@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)
        }
    }
}

NewsDetailThe content is as follows:

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

Execute assembleDebug, copy the generated apk file to the assets directory of the host app module, so that it can be copied to the memory card after the application starts (it should be downloaded from the server in the actual project).

insert image description here

AndroidManifest.xmlThen register the defined in the plug-in in the host app module PluginActivityto occupy the pit. It doesn't matter if it becomes popular here, and it will not affect the packaging.

insert image description here

Then define a class in the app module PluginManager, which is mainly responsible for loading the plug-in 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
        }
    }
}

I won’t introduce much about the principle here. There are already many related articles on the Internet. If you don’t understand it, you can search it yourself. The code I use here is basically moved from other places for reference. The above PluginManagerclass defines three methods: loadPlugin()the method is responsible for copying assetsthe in the external memory card to the cache directory of the application, and defines a for loading the apkin the plug-in ; the method is to use the ClassLoader to load and return according to the specified ; The method is to merge the array in the plug -in into the array of the host, so that the loaded plug-in can be recognized by the host application.ClassDexClassLoaderloadClass()classNameClassmergeDexElement()dexElementsdexElementsClass

Next, define one , call and process the three methods in PluginViewModelthe above respectively , and expose the corresponding state to :PluginManagerComposable

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

Finally, there is a test page for use, which defines a HostScreenpage to be displayed as a page in the host:

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 页面")
                    }
                }
            }
        }
    }
}

running result:

insert image description here

It can be seen that this method is completely feasible and almost stress-free.

For this way of occupying pits, its advantage is that each plug-in only needs to provide one for occupying a place in the host PluginActivity, which is compared with the previous traditional View development, because View could not bear the screen in the past. Level content display and no independent navigation function, so you need to use a lot of Activityclasses. If you were only allowed to use one class in the traditional development before Activity, and then the page was switched between different Views, I am afraid it will be crazy. Lost. But now it is different. Composable components can be independently responsible for the display of screen-level content and also have a navigation function independent of the Activity. It can be the leader alone, so basically there is no need for too many Activity classes, and the number of Activities that need to occupy space in the host Naturally, there are very few.

However, this method does not necessarily satisfy all scenarios. Its advantages are also its disadvantages. Just imagine that each plug-in needs to provide a placeholder Activity. When there are many plug-ins, there may still be a large number of Activity classes. There are also A serious problem is that this method can only be displayed by opening a new page in the form of "jump", because it uses an Activity as a Composable container, that is, if I want to display in a certain page of the current page The area display comes from a Composable component in the plugin, which is not possible in this way.

Directly load Composable components in plugins

In order to display the components from the plug-in in a local area of ​​the current page in the host Composable, we cannot take Activitythe method of occupying the pit as a springboard. We can consider removing the Activity in the plug-in, that is to say, only Keep the pure Composable component code (pure kotlincode), and then make it into apka plug-in for the host to load. Since the classes in the plug-in can be loaded in the host, it should be easy to directly call the Composable function in the plug-in through reflection.

Because the code must be translated into the corresponding code before kotlinit is finally compiled into a file , and we know that there is no such concept as a top-level function , each file must correspond to an independent class and the name of the file must be the same as that of the class The name remains the same. So no matter which file our Composable component is written in , it will eventually be translated into a class, and then we load the class in the host and call the method in the class .DEXJavaJavaClassJavaClassJavaxx.ktJavaJavaComposable

This idea seems to be perfect, but things are not as simple as imagined, and soon I discovered a cruel reality, we know that the Compose compiler will apply some "black magic" to the Composable function during the compilation process, it will Tampering with the IR at compile time, so some extra parameters will be added to the final Composable function. NewsList.ktFor example, and in the previous code NewsDetail.kt, use the decompilation tool to view their final form as follows:

insert image description here

insert image description here

Here you can see that the Compose compiler injects a $composerparameter (for reorganization) and a $changedparameter (for parameter comparison and skip reorganization) for each Composable function, that is to say, even a Composable function without parameters will be injected into this Two parameters, then there is a problem, even if we can load the class in the host and get the handle reference of the Composable function through reflection, but we can't call them, because we can't provide and parameters, only the Compose $composerruntime $changedknows How to provide these parameters.

This is embarrassing, and it's equivalent to wanting to call a method that only God knows how to call.

So is there no way to do this? In fact, what we want is to call the Composable function in the host. We can change the way of thinking, since the direct call is not possible, then call it indirectly.

First, we can store Composable functions by defining properties of Composable lambda type in a class , that is, provide a property member of Composable function type . For example, you can write:

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

Here the types of the member properties and ComposeProxyin the class are both function types, that is , two are actually defined . The real Composable business component can be called in its lambda block, because this is essentially calling another Composable in a Composable. As for why you want to write it like this, you can see the compiled result below:content1content2Composable@Composable () -> UnitComposable lambda

insert image description here

It can be seen that after being translated into Java code, and ComposeProxyin become two public methods and , and these two methods have no parameters, so we can call them by reflection after loading the class. Notice that the type they return is that it actually corresponds to the function type in Kotlin, because there is no so-called function type in the Java world , and instead, a function-like interface type is used to correspond to the function type in Kotlin ( , at most There are 22).content1content2getContent1()getContent2()Function2<Composer, Integer, Unit>@Composable () -> UnitFunction0...Function22

So we can think that at compile time, Function2<Composer, Integer, Unit>and @Composable () -> Unitare equivalent, because the latter will be translated into the former.

In fact, we don't have to care about what is returned , because we will eventually call and method Functionthrough Java's reflection API , that is, execute , and it returns an object (if it is written in kotlin code, it returns an object of type) , so we can force this ( ) object into a function type when writing the code to load the plug-in . Then we get an object of function type in the host, then we can call the method of the function object (that is, call the Composable function).getContent1()getContent2()Method.invoke()ObjectAnyObjectAny@Composable () -> Unit@Composable () -> Unitinvoke

Modify it below PluginViewModeland add the following code to it:

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

Modify HostScreenthe test code as follows:

@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()
            }
        }
    }
}

Repackage the news_lib module to generate apk, and copy it to the assets directory in the app module, then run the app to see the effect:

insert image description here

It can be seen that we have successfully loaded the Composable components from the plug-in directly in the Composable interface of the host, which is perfect. Activity/FragmentThis also means that we can call anything Composablefrom the plugin in the host Composable. That is to say, we do not need to keep any Activity class in our plug-in, but only keep Composable components and their related business logic codes.

Compared with the traditional plug-in method, this method no longer needs to consider how to bypass the verification of the Activity by AMS, does not need to occupy the Activity in the Manifest, and does not need to find various Hook points in the system source code through the dynamic proxy method Replace the Activity secretly, because the Activity is not needed in the plug-in at all. From then on, plug-in can take a very pure path.

The exploration of Compose's plug-in technology is not finished here, and finally there is another way to try it.

Load Composable components in plugins in Matryoshka mode

This approach was inspired by interoperability Composablewith Android . ViewFor example, to Composabledisplay an Android in Viewcan be done as follows:

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

Among them AndroidViewis a Composablefunction that can nest Android native components inside it View.

On the other hand, you can set its content by calling of ComposeViewof , because the method of of actually creates a to execute :setContent{}ComposableActivitysetContent{ }ComposeViewsetContent

insert image description here

ComposeViewis a public class, which means we can also call it like this:

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

Since ComposeViewis a standard Android View, we can call it like this:

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

So our Russian Matryoshka version of the plug-in solution was born:

insert image description here

The plug-in only needs to provide the host with the acquisition method, and the host can use this to embed into the host interface after ComposeViewobtaining the from the plug-in . The inside of the plug-in does not need to be exposed to the host, it can be wrapped inside of .ComposeViewAndroidViewComposableComposableComposeView

Similar to the previous, define a class in the plug-in ComposeViewProxy, and then define a member attribute of type function type in it 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)
        }
    }
}

Use the decompilation tool to view the generated plugin apk:

insert image description here

Next, you only need to load the class in the host ComposeViewProxy, then reflectively call the method, and get the returned result and use it getPluginView()in the host .AndroidView

Modify PluginViewModel, add the following code in it:

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

Modify HostScreenthe test code as follows:

@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()
            }
        }
    }
}

running result:

insert image description here

The running effect is the same as the previous solution, and the code logic of this method is almost the same as the previous one, but compared with the previous one, the plug-in of this solution does not need to directly expose Composablecomponents to the host, but only provides them ComposeView.

In fact, the plug-ins developed in this way can be mixed with native Viewand Composableat the same time. For example, in the process of project transformation, it is possible that part of the interface needs to be released before the transformation is completed. At this time, this part can still use the original implementation View. , and another part that uses Composablethe newly implemented part can be ComposeViewembedded into the entire page through , and then the entire page is Viewprovided to the host as a separate .

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/128755247#comments_27327499