従来の Android 開発モデルでは、インターフェイスがActivity
などのFragment
コンポーネントに過度に依存しているため、ビジネス モジュール内に多数のActivity
クラスが存在することが多く、多くのプラグイン フレームワークが生まれました。これらのプラグイン フレームワークは基本的に、さまざまな A フック/リフレクション メソッドを使用して、未登録コンポーネントの使用の問題を解決します。Jetpack Compose の世界に入ると、Activity
の役割は軽視されます。Composable
コンポーネントは画面レベルの表示を引き受けることができるため、Activity
アプリケーションにそれほど多くのクラスは必要なくなります。必要に応じて、Activity
単一の Pure Composeを作成することもできます。応用。
この記事では主に、Jetpack Compose でプラグイン/動的読み込みを実装できるいくつかの実現可能なソリューションを検討します。
アクティビティ占有の方法でプラグインのコンポーザブル コンポーネントにアクセスします。
実際、この方法は従来の View 開発でも実行できますが、Compose では Activity が 1 つしか使用できず、残りのページは Composable コンポーネントを使用して実装されるため、この方法の方が適していると感じます。したがって、主なアイデアは、プラグイン内に実際に存在するAndroidManifest.xml
ピット占有Activity
クラスをホスト アプリケーションに登録し、そのクラスをホスト内のプラグインにロードし、プラグイン内でクラスを開始し、異なるコンポーザブル コンポーネントを表示するには、異なるパラメータを渡します。端的に言えば、空のシェルのアクティビティを出発点として使用して、さまざまなコンポーザブルを表示することです。Activity
Activity
Activity
まず、プロジェクト内に新しいモジュール module を作成し、このモジュールはアプリケーション モジュールとして開発されているため、 build.gradle 内の'com.android.library'
プラグイン構成を変更し'com.android.application'
、最後に apk の形式でプラグインを提供します。PluginActivity
次に、出発点として新しいアクティビティを作成し、テスト用に 2 つのコンポーザブル ページを作成します。
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))
}
}
AssemblyDebug を実行し、生成された 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
クラスは 3 つのメソッドを定義します:このメソッドは、外部メモリ カード内の をアプリケーションのキャッシュ ディレクトリにコピーするloadPlugin()
役割を果たし、プラグインに をロードするための を定義します。このメソッドは、ClassLoader を使用してロードして返すことです。指定されたとおり;この方法は、プラグイン内の配列をホストの配列にマージし、ロードされたプラグインがホスト アプリケーションによって認識されるようにすることです。assets
apk
Class
DexClassLoader
loadClass()
className
Class
mergeDexElement()
dexElements
dexElements
Class
次に、1 つを定義し、PluginViewModel
上記のPluginManager
3 つのメソッドをそれぞれ呼び出して処理し、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 開発と比較して、各プラグインはホスト内の場所を占有するためのプラグインを 1 つだけ提供するだけで済むことです。コンテンツの表示や独立したナビゲーション機能がないため、多くのActivity
クラスを使用する必要があります。以前の従来の開発で 1 つのクラスしか使用できずActivity
、異なる View 間でページを切り替えていたら、おかしくなると思います。 . 失われた。しかし、今は違います。コンポーザブル コンポーネントは、画面レベルのコンテンツの表示を独立して担当することができ、アクティビティとは独立したナビゲーション機能も持つことができます。単独でリーダーになることができるため、基本的にアクティビティ クラスが多すぎる必要はありません。ホスト内のスペースを占有する必要があるアクティビティの数 当然のことながら、アクティビティの数は非常に少ないです。
ただし、この方法は必ずしもすべてのシナリオを満たすわけではありません。利点もありますが、欠点もあります。各プラグインがプレースホルダーのアクティビティを提供する必要があることを想像してください。プラグインが多数ある場合、依然として多数のアクティビティ クラスが存在する可能性があります。また、深刻な問題は、このメソッドはコンポーザブル コンテナとしてアクティビティを使用するため、つまり、特定のページに表示したい場合、「ジャンプ」の形式で新しいページを開かないと表示できないことです。現在のページのエリア表示はプラグインのコンポーザブル コンポーネントから取得されますが、この方法では実現できません。
コンポーザブルコンポーネントをプラグインに直接ロードする
プラグインからのコンポーネントをホスト上の現在のページのローカル領域に表示するにはComposable
、その穴を踏み台として占有する方法は取れずActivity
、プラグイン内のActivityを削除することが考えられますが、つまり、純粋な Composable コンポーネント コード(純粋なkotlin
コード) のみを保持し、それをapk
ホストがロードできるようにプラグインに作成します。プラグイン内のクラスはホストにロードできるため、次のようにする必要があります。リフレクションを通じてプラグイン内の Composable 関数を直接呼び出すのが簡単です。
kotlin
コードは、最終的にファイルにコンパイルされるDEX
前に対応するコードに変換される必要があり、トップレベル関数などの概念がないことJava
がわかっているため、各ファイルは独立したクラスに対応する必要があり、ファイルの名前はクラスの名前と同じである必要があります。名前は変わりません。そのため、コンポーザブル コンポーネントがどのファイルに記述されているかに関係なく、最終的にはクラスに変換され、そのクラスをホストにロードして、クラス内のメソッドを呼び出します。Java
Class
Java
Class
Java
xx.kt
Java
Java
Composable
このアイデアは完璧であるように見えますが、物事は想像ほど単純ではありません。すぐに残酷な現実を発見しました。Compose コンパイラはコンパイル プロセス中に Composable 関数に「黒魔術」を適用し、改ざんすることがわかっています。コンパイル時に IR が変更されるため、最終的なコンポーザブル関数にいくつかの追加パラメーターが追加されます。たとえば、前のコードではNewsList.kt
、NewsDetail.kt
次のように逆コンパイル ツールを使用して最終形式を表示します。
$composer
ここでは、Compose コンパイラーが各コンポーザブル関数に対してパラメーター (再編成用) とパラメーター (パラメーター比較および再編成のスキップ用)を挿入していることがわかります$changed
。つまり、パラメーターのないコンポーザブル関数であっても、この 2 つのパラメーターに挿入されます。ホストにクラスをロードし、リフレクションを通じて Composable 関数のハンドル参照を取得できたとしても、パラメータを提供できないため、それらを呼び出すことはできません。Compose$composer
ランタイムのみ$changed
が知っているため、問題が発生します。これらのパラメータを指定する方法。
これは恥ずかしいことであり、神のみぞ知るメソッドを呼び出したいのと同じことです。
それで、これを行う方法はありませんか?実際には、ホストで Composable 関数を呼び出したいのですが、直接呼び出すことはできないので、考え方を変えて、間接的に呼び出すことができます。
まず、クラスでComposable lambda型のプロパティを定義することによって、 Composable関数を格納できます。つまり、Composable function typeのプロパティ メンバーを提供します。たとえば、次のように書くことができます。
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
クラス内のcontent1
メンバープロパティの型はcontent2
両方とも関数型ですComposable
。つまり@Composable () -> Unit
、実際には 2 つが定義されていますComposable lambda
。実際のコンポーザブル ビジネス コンポーネントは、本質的にコンポーザブル内の別のコンポーザブルを呼び出すことになるため、ラムダ ブロックで呼び出すことができます。なぜこのように書くのかについては、コンパイルした結果を以下に示します。
Java コードに変換された後、と は2 つのパブリック メソッドとComposeProxy
になり、これら 2 つのメソッドにはパラメータがないことがわかります。そのため、クラスをロードした後にリフレクションによってそれらを呼び出すことができます。返される型は実際には Kotlin の関数型に対応していることに注意してください。Java の世界にはいわゆる関数型は存在せず、代わりに関数のようなインターフェイス型が Kotlin の関数型に対応するために使用されます。 Kotlin ( 、最大で 22 個あります)。content1
content2
getContent1()
getContent2()
Function2<Composer, Integer, Unit>
@Composable () -> Unit
Function0...Function22
したがって、後者は前者に変換されるため、コンパイル時にFunction2<Composer, Integer, Unit>
と は同等であると考えることができます。@Composable () -> Unit
実際、何が返されるのかを気にする必要はありません。最終的にはFunction
Java のリフレクション API を介しておよび メソッドをgetContent1()
呼び出すことになるためgetContent2()
、つまり、executeMethod.invoke()
はオブジェクトを返します(Kotlin コードで記述されている場合はオブジェクトObject
を返します)。Any
の type) なので、プラグインをロードするコードを作成するときに、この) オブジェクトを関数タイプに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 を生成し、それをアプリ モジュールのアセット ディレクトリにコピーしてから、アプリを実行して効果を確認します。
コンポーザブル コンポーネントをプラグインからホストのコンポーザブル インターフェイスに直接ロードすることに成功したことがわかります。これは完璧です。これは、ホスト内のプラグインからActivity/Fragment
何でも呼び出せることも意味します。つまり、プラグイン内に Activity クラスを保持する必要はなく、コンポーザブル コンポーネントとそれに関連するビジネス ロジック コードのみを保持する必要があります。Composable
Composable
従来のプラグイン方式と比較して、この方式では AMS によるアクティビティの検証を回避する方法を考慮する必要がなく、マニフェスト内のアクティビティを占有する必要がなく、システム内のさまざまなフック ポイントを見つける必要もありません。動的プロキシ メソッドを使用したソース コード。アクティビティはプラグインではまったく必要ないため、アクティビティを秘密裏に置き換えます。それ以降、プラグインは非常に純粋な道を進むことができます。
Compose のプラグイン テクノロジの探索はここで終わったわけではありません。最後に、それを試す別の方法があります。
Matryoshka モードでプラグインにコンポーザブル コンポーネントをロードする
このアプローチは、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())
}
その中には、Android ネイティブ コンポーネントを内部にネストできる機能がAndroidView
あります。Composable
View
一方、ComposeView
of を呼び出すことでsetContent{}
その内容を設定できますComposable
。これは、of の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
、その中で関数型のメンバー属性を定義します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 を表示します。
次に、クラスを host にロードしComposeViewProxy
、メソッドをリフレクティブに呼び出し、返された結果を取得して、それをgetPluginView()
host で使用するだけです。AndroidView
を変更しPluginViewModel
、次のコードを追加します。
class PluginViewModel: ViewModel() {
// ...省略其它无关代码
val composeViewProxyClassName = "com.fly.compose.plugin.news.ComposeViewProxy"
var pluginView by mutableStateOf<Context.(String) -> ComposeView>({
ComposeView(this)})
var isLoadPluginViewSuccess by mutableStateOf(false)
fun loadPluginView() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val composeViewProxyClass = PluginManager.loadClass(composeViewProxyClassName)
composeViewProxyClass?.let {
proxyClass ->
val getPluginViewMethod: Method = proxyClass.getDeclaredMethod("getPluginView")
val obj = proxyClass.newInstance()
pluginView = getPluginViewMethod.invoke(obj) as (Context.(String) -> ComposeView)
isLoadPluginViewSuccess = true
}
}
}
}
}
HostScreen
テストコードを次のように変更します。
@Composable
fun HostScreen(viewModel: PluginViewModel = viewModel()) {
CommonLayout(viewModel) {
Button(onClick = {
viewModel.loadPluginView() }) {
Text(text = "点击加载插件中的 ComposeView")
}
// 加载成功后调用插件中的ComposeView
if (viewModel.isLoadPluginViewSuccess) {
SimpleAndroidView {
context ->
viewModel.pluginView(context, "content1")
}
SimpleAndroidView {
context ->
viewModel.pluginView(context, "content2")
}
}
}
}
@Composable
private fun <T: View> SimpleAndroidView(factory: (Context) -> T) {
AndroidView(
factory = {
context -> factory(context) },
modifier = Modifier.wrapContentSize()
)
}
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
private fun CommonLayout(
viewModel: PluginViewModel = viewModel(),
content: @Composable () -> Unit
) {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text(text = "当前是宿主中的Composable页面")
Button(onClick = {
viewModel.loadPlugin(context) }) {
Text(text = "点击加载插件Classloader")
}
val isLoadSuccess = viewModel.isPluginLoadSuccess.collectAsStateWithLifecycle()
Text(text = "插件Classloader是否加载成功:${
isLoadSuccess.value}")
if (isLoadSuccess.value) {
Button(onClick = {
viewModel.mergeDex(context) }) {
Text(text = "点击合并插件Dex到宿主中")
}
val isMergeDexSuccess = viewModel.isMergeDexSuccess.collectAsStateWithLifecycle()
Text(text = "合并插件Dex到宿主是否成功:${
isMergeDexSuccess.value}")
if (isMergeDexSuccess.value) {
content()
}
}
}
}
実行結果:
実行効果は前のソリューションと同じであり、このメソッドのコード ロジックは前のソリューションとほぼ同じですが、前のソリューションと比較して、このソリューションのプラグインはコンポーネントを直接公開する必要がありませんComposable
。ホストですが、提供するだけですComposeView
。
実際、この方法で開発されたプラグインは、ネイティブView
とComposable
同時に混在させることができ、たとえば、プロジェクトの変換プロセスにおいて、変換が完了する前にインターフェースの一部をリリースする必要がある場合があります。この時点では、この部分は元の実装を引き続き使用できView
、Composable
新たに実装された部分を使用する別の部分はComposeView
を通じてページ全体に埋め込まれ、ページ全体がView
別の . としてホストに提供されます。