В традиционной модели разработки Android, поскольку интерфейс чрезмерно зависит от Activity
таких Fragment
компонентов, как , в бизнес-модуле часто бывает большое количество Activity
классов, поэтому появилось много фреймворков плагинов, и эти фреймворки плагинов в основном пытаются используйте различные методы A Hook/reflection для решения проблемы использования незарегистрированных компонентов. После входа в мир Jetpack Compose Activity
роль класса приуменьшается. Поскольку Composable
компонент может выполнять отображение на уровне экрана, нам больше не нужно так много Activity
классов в нашем приложении. Пока вы хотите, вы даже можете создать один Activity
Pure Compose. приложение.
В этой статье в основном делается попытка изучить несколько возможных решений, которые могут реализовать подключаемый модуль/динамическую загрузку в Jetpack Compose.
Получите доступ к компоненту Composable в плагине путем занятия Activity
На самом деле, этот метод также можно использовать в традиционной разработке View, но, поскольку мы можем использовать только одно действие в Compose, а остальные страницы реализованы с использованием компонентов Composable, он кажется более подходящим для него. Поэтому основная идея состоит в том, чтобы прописать в хост AndroidManifest.xml
-приложении класс-пит , который реально существует в плагине, а затем загрузить Класс в плагине в хосте, запустить класс в плагине и передавать разные параметры для отображения разных составных компонентов. Проще говоря, это использование пустой оболочки Activity в качестве трамплина для отображения различных Composables.Activity
Activity
Activity
Activity
Во-первых, создайте новый модуль модуля в проекте и '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 для загрузки и возврата в соответствии с указанным Метод заключается в объединении массива в плагине с массивом хоста, чтобы загруженный плагин мог быть распознан хост-приложением.Class
DexClassLoader
loadClass()
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
, а затем страница переключалась между разными представлениями, я боюсь, что это будет сумасшествие , Потерянный. Но теперь все по-другому.Компонуемые компоненты могут самостоятельно отвечать за отображение контента на уровне экрана, а также иметь функцию навигации, независимую от Activity.Он может быть только лидером, поэтому в основном нет необходимости в слишком большом количестве классов Activity, и количество активностей, которые должны занимать место в хосте. Естественно, их очень мало.
Однако этот метод не обязательно удовлетворяет всем сценариям. Его преимущества также являются его недостатками. Просто представьте, что каждый плагин должен предоставлять заполнитель Activity. Когда есть много плагинов, может быть большое количество классов Activity. .Есть также Серьезная проблема в том, что этот метод может отображаться только при открытии новой страницы в виде "прыжка", потому что он использует Activity как Composable контейнер, то есть если я хочу отображать в определенной странице текущей страницы Отображение области исходит из компонента Composable в плагине, что невозможно таким образом.
Непосредственно загружать составные компоненты в плагины
Для того, чтобы отображать компоненты из плагина в локальной области текущей страницы в хосте Composable
, мы не можем взять Activity
метод занятия ямы в качестве трамплина, мы можем рассмотреть возможность удаления Активности в плагине, то есть оставьте только чистый составной код компонента (чистый kotlin
код), а затем превратите его в apk
подключаемый модуль для загрузки хоста.Поскольку классы в подключаемом модуле могут быть загружены в хост, легко напрямую вызвать функцию Composable в плагине через отражение.
Поскольку код должен быть переведен в соответствующий код, прежде чем kotlin
он будет окончательно скомпилирован в файл , а мы знаем, что не существует такого понятия, как функция верхнего уровня , каждый файл должен соответствовать независимому классу и имя файла должно быть таким же, как у класса. Имя остается прежним. Так что в каком бы файле ни был написан наш Composable компонент , он в конечном итоге будет транслирован в класс, а затем мы загружаем класс в хост и вызываем метод в классе .DEX
Java
Java
Class
Java
Class
Java
xx.kt
Java
Java
Composable
Эта идея кажется идеальной, но все не так просто, как предполагалось, и вскоре я обнаружил жестокую реальность, мы знаем, что компилятор Compose применит некоторую «черную магию» к функции Composable в процессе компиляции, она IR времени компиляции, поэтому финальная функция Composable будет иметь некоторые дополнительные параметры. NewsList.kt
Например, и в предыдущем коде NewsDetail.kt
используйте инструмент декомпиляции, чтобы просмотреть их окончательный вид следующим образом:
Здесь вы можете видеть, что компилятор Compose вводит $composer
параметр (для реорганизации) и $changed
параметр (для сравнения параметров и пропуска реорганизации) для каждой Composable функции, то есть даже Composable функция без параметров будет внедрена в эти два параметра, тогда есть проблема, даже если мы можем загрузить класс в хост и получить ссылку на дескриптор функции Composable через отражение, но мы не можем их вызвать, потому что мы не можем предоставить и параметры, знает только среда выполнения $composer
Compose $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. Что касается того, почему вы хотите написать это так, вы можете увидеть скомпилированный результат ниже:content1
content2
Composable
@Composable () -> Unit
Composable lambda
Видно, что после трансляции в код Java, и ComposeProxy
в стали два публичных метода и , причем эти два метода не имеют параметров, поэтому мы можем вызвать их отражением после загрузки класса. Обратите внимание, что тип, который они возвращают, на самом деле соответствует типу функции в Kotlin, потому что в мире Java нет так называемого типа функции , и вместо этого используется тип интерфейса, подобный функции, для соответствия типу функции в Kotlin. Kotlin ( , не более 22).content1
content2
getContent1()
getContent2()
Function2<Composer, Integer, Unit>
@Composable () -> Unit
Function0...Function22
Таким образом, мы можем думать, что во время компиляции Function2<Composer, Integer, Unit>
и @Composable () -> Unit
эквивалентны, потому что последнее будет преобразовано в первое.
На самом деле, нам не нужно заботиться о том, что возвращается , потому что мы в конечном итоге будем вызывать и метод Function
через API отражения Java , то есть выполнять , и он возвращает объект (если он написан в коде kotlin, он возвращает объект типа), поэтому мы можем принудительно преобразовать этот (объект . Затем мы получаем объект типа функция в хосте, затем мы можем вызвать метод объекта функции (то есть вызвать функцию Composable).getContent1()
getContent2()
Method.invoke()
Object
Any
Object
Any
@Composable () -> Unit
@Composable () -> Unit
invoke
Измените его ниже 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
.
С другой стороны, вы можете установить его содержимое, вызвав ComposeView
of , потому что метод of фактически создает объект для выполнения :setContent{}
Composable
Activity
setContent{ }
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
, а затем определите в нем атрибут-член типа 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
хосту как отдельный .