如何开发可加载外部APK UI插件的Android Jetpack Compose宿主应用
Jetpack Compose插件化UI落地方案
针对你的需求,以下是可直接落地的实现思路和代码示例,核心是通过动态加载APK+Compose组件暴露的方式,实现宿主内加载插件UI:
一、基础架构与约定
首先要定义宿主和插件的公共契约,避免类加载冲突:
- 新建一个Android Library模块作为公共依赖,包含插件必须实现的接口:
// 公共库中的接口 interface ComposePlugin { // 返回插件的Compose UI组件 fun getPluginUI(): @Composable () -> Unit // 插件元信息,用于宿主展示 fun getPluginInfo(): PluginInfo } data class PluginInfo( val id: String, val name: String, val version: String, val description: String = "" )
- 宿主和所有插件都依赖这个公共库,确保双方对接口的定义一致。
二、插件APK开发
插件不需要启动入口Activity,只需要实现上述接口并打包成APK:
// 插件中的实现类 class RetroStyleUIPlugin : ComposePlugin { override fun getPluginUI(): @Composable () -> Unit { return { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = "复古风格UI插件", fontSize = 24.sp, color = Color(0xFF8B4513), fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(16.dp)) Button( colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFD2B48C)), onClick = { /* 可通过接口回调宿主能力,比如 toast */ } ) { Text("点击体验", color = Color.Black) } } } } } override fun getPluginInfo(): PluginInfo { return PluginInfo( id = "retro_ui", name = "复古风格UI", version = "1.0.0", description = "怀旧风界面展示" ) } }
- 插件打包时,在
AndroidManifest.xml中可以去掉默认的Launcher Activity,仅保留必要的配置,减小APK体积。
三、宿主核心实现
1. 插件下载
从GitHub Release下载插件APK到宿主的私有目录(无需存储权限):
// 简化的下载逻辑,实际可使用OkHttp等库 suspend fun downloadPlugin(githubAssetUrl: String, savePath: String): Boolean { return try { val response = URL(githubAssetUrl).openStream() val file = File(savePath) response.copyTo(file.outputStream()) true } catch (e: Exception) { e.printStackTrace() false } }
2. 加载插件APK
通过DexClassLoader加载插件中的实现类:
fun loadPlugin(apkPath: String, context: Context): ComposePlugin? { val dexCacheDir = context.cacheDir.resolve("plugin_dex") dexCacheDir.mkdirs() // 创建类加载器,宿主类加载器作为父类,确保公共库类一致 val classLoader = DexClassLoader( apkPath, dexCacheDir.absolutePath, null, context.classLoader ) return try { // 反射加载插件中的实现类,这里可以约定类名或从插件Manifest元数据读取 val pluginClass = classLoader.loadClass("com.your.plugin.package.RetroStyleUIPlugin") pluginClass.newInstance() as ComposePlugin } catch (e: ClassNotFoundException) { e.printStackTrace() null } catch (e: InstantiationException) { e.printStackTrace() null } }
3. 渲染插件UI
在宿主的Compose界面中直接加载插件返回的UI组件:
@Composable fun PluginDisplayScreen(plugin: ComposePlugin, onUninstall: () -> Unit) { Column(modifier = Modifier.fillMaxSize()) { // 宿主自带的插件头部 Row( modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column { Text(plugin.getPluginInfo().name, fontSize = 18.sp, fontWeight = FontWeight.Bold) Text("版本:${plugin.getPluginInfo().version}", fontSize = 12.sp, color = Color.Gray) } Button(onClick = onUninstall) { Text("卸载") } } Divider() // 渲染插件UI Box(modifier = Modifier.weight(1f)) { plugin.getPluginUI().invoke() } } }
4. 插件卸载
删除插件APK文件并清理缓存:
fun uninstallPlugin(apkPath: String, context: Context): Boolean { val apkFile = File(apkPath) val dexCacheDir = context.cacheDir.resolve("plugin_dex") return apkFile.delete() && dexCacheDir.deleteRecursively() }
四、分发与注意事项
- 分发方式:在GitHub Release上传插件APK,宿主通过GitHub API获取Release列表,解析assets中的APK信息(名称、版本、下载链接)展示给用户。
- 类加载冲突:确保宿主和插件使用的Compose版本、公共库版本完全一致,避免
ClassCastException。 - 权限限制:插件无法单独申请权限,所有需要的权限必须在宿主的
AndroidManifest.xml中声明。 - 内存管理:卸载插件时,要确保所有插件相关的引用被释放,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Akshar Kalathiya
相关产品推荐
相关产品推荐

