Исследование динамической загрузки и подключаемых модулей в Jetpack Compose

В традиционной модели разработки Android, поскольку интерфейс чрезмерно зависит от Activityтаких Fragmentкомпонентов, как , в бизнес-модуле часто бывает большое количество Activityклассов, поэтому появилось много фреймворков плагинов, и эти фреймворки плагинов в основном пытаются используйте различные методы A Hook/reflection для решения проблемы использования незарегистрированных компонентов. После входа в мир Jetpack Compose Activityроль класса приуменьшается. Поскольку Composableкомпонент может выполнять отображение на уровне экрана, нам больше не нужно так много Activityклассов в нашем приложении. Пока вы хотите, вы даже можете создать один ActivityPure Compose. приложение.

В этой статье в основном делается попытка изучить несколько возможных решений, которые могут реализовать подключаемый модуль/динамическую загрузку в Jetpack Compose.

Получите доступ к компоненту Composable в плагине путем занятия Activity

На самом деле, этот метод также можно использовать в традиционной разработке View, но, поскольку мы можем использовать только одно действие в Compose, а остальные страницы реализованы с использованием компонентов Composable, он кажется более подходящим для него. Поэтому основная идея состоит в том, чтобы прописать в хост AndroidManifest.xml-приложении класс-пит , который реально существует в плагине, а затем загрузить Класс в плагине в хосте, запустить класс в плагине и передавать разные параметры для отображения разных составных компонентов. Проще говоря, это использование пустой оболочки Activity в качестве трамплина для отображения различных Composables.ActivityActivityActivityActivity

Во-первых, создайте новый модуль модуля в проекте и 'com.android.library'измените конфигурацию плагинов в build.gradle 'com.android.application', потому что этот модуль разработан как модуль приложения, и, наконец, предоставьте плагины в виде apk. Затем создайте новую PluginActivityактивность в качестве трамплина и создайте две составные страницы для тестирования.

вставьте сюда описание изображения

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

Вот простое суждение, основанное на типе, прочитанном намерением.Если это NewsList, будет отображаться составная страница списка новостей.Если это NewsDetail, будет отображаться составная страница сведений о новостях.

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-файл в каталог assets модуля хост-приложения, чтобы его можно было скопировать на карту памяти после запуска приложения (его следует загрузить с сервера в реальном проекте).

вставьте сюда описание изображения

AndroidManifest.xmlЗатем зарегистрируйте определенный в плагине в модуле хост-приложения, PluginActivityчтобы занять яму, Неважно, станет ли он популярным здесь, и это не повлияет на упаковку.

вставьте сюда описание изображения

Затем определите класс в модуле приложения 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плагина ; метод заключается в использовании ClassLoader для загрузки и возврата в соответствии с указанным Метод заключается в объединении массива в плагине с массивом хоста, чтобы загруженный плагин мог быть распознан хост-приложением.ClassDexClassLoaderloadClass()classNameClassmergeDexElement()dexElementsdexElementsClass

Затем определите один , вызовите и обработайте три метода, описанных PluginViewModelвыше, соответственно , и выставьте соответствующее состояние: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)
            }
        }
    }
}

Наконец, есть тестовая страница для использования, которая определяет страницу, 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, а затем страница переключалась между разными представлениями, я боюсь, что это будет сумасшествие , Потерянный. Но теперь все по-другому.Компонуемые компоненты могут самостоятельно отвечать за отображение контента на уровне экрана, а также иметь функцию навигации, независимую от Activity.Он может быть только лидером, поэтому в основном нет необходимости в слишком большом количестве классов Activity, и количество активностей, которые должны занимать место в хосте. Естественно, их очень мало.

Однако этот метод не обязательно удовлетворяет всем сценариям. Его преимущества также являются его недостатками. Просто представьте, что каждый плагин должен предоставлять заполнитель Activity. Когда есть много плагинов, может быть большое количество классов Activity. .Есть также Серьезная проблема в том, что этот метод может отображаться только при открытии новой страницы в виде "прыжка", потому что он использует Activity как Composable контейнер, то есть если я хочу отображать в определенной странице текущей страницы Отображение области исходит из компонента Composable в плагине, что невозможно таким образом.

Непосредственно загружать составные компоненты в плагины

Для того, чтобы отображать компоненты из плагина в локальной области текущей страницы в хосте Composable, мы не можем взять Activityметод занятия ямы в качестве трамплина, мы можем рассмотреть возможность удаления Активности в плагине, то есть оставьте только чистый составной код компонента (чистый kotlinкод), а затем превратите его в apkподключаемый модуль для загрузки хоста.Поскольку классы в подключаемом модуле могут быть загружены в хост, легко напрямую вызвать функцию Composable в плагине через отражение.

Поскольку код должен быть переведен в соответствующий код, прежде чем kotlinон будет окончательно скомпилирован в файл , а мы знаем, что не существует такого понятия, как функция верхнего уровня , каждый файл должен соответствовать независимому классу и имя файла должно быть таким же, как у класса. Имя остается прежним. Так что в каком бы файле ни был написан наш Composable компонент , он в конечном итоге будет транслирован в класс, а затем мы загружаем класс в хост и вызываем метод в классе .DEXJavaJavaClassJavaClassJavaxx.ktJavaJavaComposable

Эта идея кажется идеальной, но все не так просто, как предполагалось, и вскоре я обнаружил жестокую реальность, мы знаем, что компилятор Compose применит некоторую «черную магию» к функции Composable в процессе компиляции, она IR времени компиляции, поэтому финальная функция Composable будет иметь некоторые дополнительные параметры. NewsList.ktНапример, и в предыдущем коде NewsDetail.ktиспользуйте инструмент декомпиляции, чтобы просмотреть их окончательный вид следующим образом:

вставьте сюда описание изображения

вставьте сюда описание изображения

Здесь вы можете видеть, что компилятор Compose вводит $composerпараметр (для реорганизации) и $changedпараметр (для сравнения параметров и пропуска реорганизации) для каждой Composable функции, то есть даже Composable функция без параметров будет внедрена в эти два параметра, тогда есть проблема, даже если мы можем загрузить класс в хост и получить ссылку на дескриптор функции Composable через отражение, но мы не можем их вызвать, потому что мы не можем предоставить и параметры, знает только среда выполнения $composerCompose $changedКак предоставить эти параметры.

Это смущает, и это эквивалентно желанию вызвать метод, который только Бог знает, как вызвать.

Так нет ли способа сделать это? На самом деле, мы хотим вызвать функцию 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в классе являются типами функций, то есть фактически определены два . Настоящий Composable бизнес-компонент может быть вызван в его лямбда-блоке, потому что это, по сути, вызов другого Composable в Composable. Что касается того, почему вы хотите написать это так, вы можете увидеть скомпилированный результат ниже:content1content2Composable@Composable () -> UnitComposable lambda

вставьте сюда описание изображения

Видно, что после трансляции в код Java, и ComposeProxyв стали два публичных метода и , причем эти два метода не имеют параметров, поэтому мы можем вызвать их отражением после загрузки класса. Обратите внимание, что тип, который они возвращают, на самом деле соответствует типу функции в Kotlin, потому что в мире Java нет так называемого типа функции , и вместо этого используется тип интерфейса, подобный функции, для соответствия типу функции в Kotlin. Kotlin ( , не более 22).content1content2getContent1()getContent2()Function2<Composer, Integer, Unit>@Composable () -> UnitFunction0...Function22

Таким образом, мы можем думать, что во время компиляции Function2<Composer, Integer, Unit>и @Composable () -> Unitэквивалентны, потому что последнее будет преобразовано в первое.

На самом деле, нам не нужно заботиться о том, что возвращается , потому что мы в конечном итоге будем вызывать и метод Functionчерез API отражения Java , то есть выполнять , и он возвращает объект (если он написан в коде kotlin, он возвращает объект типа), поэтому мы можем принудительно преобразовать этот (объект . Затем мы получаем объект типа функция в хосте, затем мы можем вызвать метод объекта функции (то есть вызвать функцию Composable).getContent1()getContent2()Method.invoke()ObjectAnyObjectAny@Composable () -> Unit@Composable () -> Unitinvoke

Измените его ниже 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, и скопируйте его в каталог assets в модуле приложения, затем запустите приложение, чтобы увидеть результат:

вставьте сюда описание изображения

Видно, что мы успешно загрузили компоненты Composable из плагина прямо в интерфейс Composable хоста, что идеально. Activity/FragmentЭто также означает, что мы можем вызывать что угодно Composableиз плагина в хосте Composable. Иными словами, нам не нужно сохранять какой-либо класс Activity в нашем плагине, а нужно хранить только компоненты Composable и связанные с ними коды бизнес-логики.

По сравнению с традиционным методом подключаемых модулей, этот метод больше не требует рассмотрения того, как обойти проверку действия с помощью AMS, не требует занятия действия в манифесте и не требует поиска различных точек подключения в системе. исходный код через метод динамического прокси Заменить Activity тайно, т.к. Activity в плагине вообще не нужно. С этого момента плагин может пойти по очень чистому пути.

Изучение технологии подключаемых модулей Compose на этом не заканчивается, и, наконец, есть еще один способ попробовать ее.

Загружать составные компоненты в плагины в режиме матрешки

Этот подход был вдохновлен совместимостью 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.

С другой стороны, вы можете установить его содержимое, вызвав ComposeViewof , потому что метод of фактически создает объект для выполнения :setContent{}ComposableActivitysetContent{ }ComposeViewsetContent

вставьте сюда описание изображения

ComposeViewявляется общедоступным классом, что означает, что мы также можем называть его так:

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

Поскольку ComposeViewэто стандартный Android View, мы можем назвать его так:

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

Так родилась наша русская матрешка версии подключаемого решения:

вставьте сюда описание изображения

Плагину нужно только предоставить хосту метод получения, и хост может использовать его для встраивания в интерфейс хоста после ComposeViewполучения от плагина . Внутреннюю часть плагина не нужно показывать хосту, ее можно обернуть внутри .ComposeViewAndroidViewComposableComposableComposeView

Аналогично предыдущему, определите класс в плагине ComposeViewProxy, а затем определите в нем атрибут-член типа function type 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воздействию host, а только предоставляет их ComposeView.

На самом деле, разработанные таким образом плагины можно смешивать с нативными Viewи Composableодновременно, например, в процессе трансформации проекта возможно, что часть интерфейса нужно освободить до завершения трансформации. В это время эта часть все еще может использовать исходную реализацию View. , а другая часть, использующая Composableвновь реализованную часть, может быть ComposeViewвстроена во всю страницу через , а затем вся страница предоставляется Viewхосту как отдельный .

Supongo que te gusta

Origin blog.csdn.net/lyabc123456/article/details/128755247#comments_27327499
Recomendado
Clasificación