Jetpack Compose での動的な読み込みとプラグイン テクノロジの探索

従来の Android 開発モデルでは、インターフェイスがActivityなどのFragmentコンポーネントに過度に依存しているため、ビジネス モジュール内に多数のActivityクラスが存在することが多く、多くのプラグイン フレームワークが生まれました。これらのプラグイン フレームワークは基本的に、さまざまな A フック/リフレクション メソッドを使用して、未登録コンポーネントの使用の問題を解決します。Jetpack Compose の世界に入ると、Activityの役割は軽視されます。Composableコンポーネントは画面レベルの表示を引き受けることができるため、Activityアプリケーションにそれほど多くのクラスは必要なくなります。必要に応じて、Activity単一の Pure Composeを作成することもできます。応用。

この記事では主に、Jetpack Compose でプラグイン/動的読み込みを実装できるいくつかの実現可能なソリューションを検討します。

アクティビティ占有の方法でプラグインのコンポーザブル コンポーネントにアクセスします。

実際、この方法は従来の View 開発でも実行できますが、Compose では Activity が 1 つしか使用できず、残りのページは Composable コンポーネントを使用して実装されるため、この方法の方が適していると感じます。したがって、主なアイデアは、プラグイン内に実際に存在するAndroidManifest.xmlピット占有Activityクラスをホスト アプリケーションに登録し、そのクラスをホスト内のプラグインにロードし、プラグイン内でクラスを開始し、異なるコンポーザブル コンポーネントを表示するには、異なるパラメータを渡します。端的に言えば、空のシェルのアクティビティを出発点として使用して、さまざまなコンポーザブルを表示することです。ActivityActivityActivity

まず、プロジェクト内に新しいモジュール 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 を使用してロードして返すことです。指定されたとおり;この方法は、プラグイン内の配列をホストの配列にマージし、ロードされたプラグインがホスト アプリケーションによって認識されるようにすることです。assetsapkClassDexClassLoaderloadClass()classNameClassmergeDexElement()dexElementsdexElementsClass

次に、1 つを定義し、PluginViewModel上記のPluginManager3 つのメソッドをそれぞれ呼び出して処理し、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がわかっているため、各ファイルは独立したクラスに対応する必要があり、ファイルの名前はクラスの名前と同じである必要があります。名前は変わりません。そのため、コンポーザブル コンポーネントがどのファイルに記述されているかに関係なく、最終的にはクラスに変換され、そのクラスをホストにロードして、クラス内のメソッドを呼び出します。JavaClassJavaClassJavaxx.ktJavaJavaComposable

このアイデアは完璧であるように見えますが、物事は想像ほど単純ではありません。すぐに残酷な現実を発見しました。Compose コンパイラはコンパイル プロセス中に Composable 関数に「黒魔術」を適用し、改ざんすることがわかっています。コンパイル時に IR が変更されるため、最終的なコンポーザブル関数にいくつかの追加パラメーターが追加されます。たとえば、前のコードではNewsList.ktNewsDetail.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 個あります)。content1content2getContent1()getContent2()Function2<Composer, Integer, Unit>@Composable () -> UnitFunction0...Function22

したがって、後者は前者に変換されるため、コンパイル時にFunction2<Composer, Integer, Unit>と は同等であると考えることができます。@Composable () -> Unit

実際、何が返されるのかを気にする必要はありません。最終的にはFunctionJava のリフレクション API を介しておよび メソッドをgetContent1()呼び出すことになるためgetContent2()、つまり、executeMethod.invoke()はオブジェクトを返します(Kotlin コードで記述されている場合はオブジェクトObjectを返します)。Anyの type) なので、プラグインをロードするコードを作成するときに、この) オブジェクトを関数タイプにObject(次に、ホストで関数タイプのオブジェクトを取得し、関数オブジェクトのメソッドを呼び出すことができます(つまり、コンポーザブル関数を呼び出します)。Any@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 を生成し、それをアプリ モジュールのアセット ディレクトリにコピーしてから、アプリを実行して効果を確認します。

ここに画像の説明を挿入

コンポーザブル コンポーネントをプラグインからホストのコンポーザブル インターフェイスに直接ロードすることに成功したことがわかります。これは完璧です。これは、ホスト内のプラグインからActivity/Fragment何でも呼び出せることも意味しますつまり、プラグイン内に Activity クラスを保持する必要はなく、コンポーザブル コンポーネントとそれに関連するビジネス ロジック コードのみを保持する必要があります。ComposableComposable

従来のプラグイン方式と比較して、この方式では AMS によるアクティビティの検証を回避する方法を考慮する必要がなく、マニフェスト内のアクティビティを占有する必要がなく、システム内のさまざまなフック ポイントを見つける必要もありません。動的プロキシ メソッドを使用したソース コード。アクティビティはプラグインではまったく必要ないため、アクティビティを秘密裏に置き換えます。それ以降、プラグインは非常に純粋な道を進むことができます。

Compose のプラグイン テクノロジの探索はここで終わったわけではありません。最後に、それを試す別の方法があります。

Matryoshka モードでプラグインにコンポーザブル コンポーネントをロードする

このアプローチは、ComposableAndroid とのView相互運用性に触発されました。たとえば、ComposableAndroid を表示するには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ありますComposableView

一方、ComposeViewof を呼び出すことでsetContent{}その内容を設定できますComposable。これは、of のActivityメソッドが実際に を作成して実行するためですsetContent{ }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プラグインから取得した後、これをComposeView使用してホスト インターフェイスに埋め込むことができます。プラグインの内部をホストに公開する必要はなく、 の内部にラップできます。AndroidViewComposableComposableComposeView

前と同様に、プラグインでクラスを定義し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

実際、この方法で開発されたプラグインは、ネイティブViewComposable同時に混在させることができ、たとえば、プロジェクトの変換プロセスにおいて、変換が完了する前にインターフェースの一部をリリースする必要がある場合があります。この時点では、この部分は元の実装を引き続き使用できViewComposable新たに実装された部分を使用する別の部分はComposeViewを通じてページ全体に埋め込まれ、ページ全体がView別の . としてホストに提供されます。

おすすめ

転載: blog.csdn.net/lyabc123456/article/details/128755247#comments_27327499