ovCompose + AI 开发跨三端的 Now in Kotlin App

Now in Kotlin 是一个基于 ovCompose 技术构建的跨平台项目。本项目旨在帮助开发者了解 Kotlin 最新技术动态,同时演示如何利用 ovCompose 在 Android、iOS 和 鸿蒙三个平台上实现代码共享与高效开发。

功能特性

目前 Now in Kotlin 主要包括两个功能

  • Kotlin 炉边漫谈播客:内容来源于podcast.kotlin.tips,是一个以 Kotlin 为主题的中文播客节目。
  • Kotlin 技术月报:内容来源于beijing-kug.github.io/news/,每月更新 Kotlin 相关的技术动态。

UI 设计

程序员一般不是很擅长 UI 设计,我这里直接使用了Motiff 妙多 来生成设计稿与 Logo。

Motiff 不仅支持根据提示词生成设计稿,也可以选中指定区域之后与 AI 对话进行微调,同时生成的设计稿也支持通过 MCP 直接生成代码。

具体实现

UI 界面

如上面所说,通过 Motiff MCP 生成 Compose 代码即可,生成的 Compose 代码可通过 ovCompose 复用到 iOS 与 鸿蒙平台。

生成的 UI 代码基本不需要改动,需要注意的一点是目前 ovCompose 还不支持 material3,因此生成的代码的部分需要注意一下。

路由简单实现

由于目前 ovCompose 还没有支持 Navigation,因此需要简单实现路由跳转。

如下所示,实际上是把 navigator 栈中的所有页面都绘制了,但是只有 zIndex 最高的那个可见。

kotlin 复制代码
@Composable
fun NavigatorHost(initialScreen: Screen) {
    val navigator = remember { Navigator(initialScreen) }

    BackHandler(navigator.stack.size > 1) {
        navigator.pop()
    }

    Box(Modifier.fillMaxSize()) {
        CompositionLocalProvider(LocalNavigator provides navigator) {
            // 遍历导航栈中的所有页面
            navigator.stack.forEachIndexed { index, screen ->
                // 使用 key 来为每个屏幕的 Composable 提供一个稳定的身份,这是状态保持的关键!
                key(screen) {
                    // 使用 zIndex 来确保栈顶的屏幕绘制在最上层。
                    // 所有屏幕都在组件树中,但只有 zIndex 最高的那个可见。
                    Box(modifier = Modifier.fillMaxSize().zIndex(index.toFloat())) {
                        screen.Content()
                    }
                }
            }
        }
    }
}

网络图片加载简单实现

由于 ovCompose 目前还没有支持网络图片加载,因此也需要简单实现一个AsyncImage

如下所示,通过传入的 url 下载 ByteArray 并转化为 Bitmap,下载成功的 Bitmap 会做一个内存缓存。

有点需要注意的 iOS 平台与鸿蒙平台都使用了Image.makeFromEncoded来解码 Bitmap,但是这里 iOS 平台没有问题,鸿蒙平台只支持 PNG 格式。 因为项目中使用的图片正好都是 PNG 格式所以没有问题,如果你需要使用其它格式图片,还需要调用鸿蒙 API 处理一下解码问题。

kotlin 复制代码
@Composable
fun AsyncImage(
    url: String,
    placeHodler: DrawableResource,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    contentScale: ContentScale = ContentScale.Fit
) {
    // 2. 使用 remember 管理当前状态
    var state: AsyncImageState by remember(url) { mutableStateOf(AsyncImageState.Loading) }

    // 3. 使用 LaunchedEffect 启动异步加载
    // 当 url 变化时,这个协程会自动取消并重新启动
    LaunchedEffect(url) {
        // ...
        asyncImageCache.set(url, AsyncImageState.Loading)
        val byteArray: ByteArray? = withContext(Dispatchers.IO) {
            fetchImageForGetRequest(url)
        }

        val bitmap = imageBitmapFromBytes(byteArray)
        state = AsyncImageState.Success(bitmap)
        asyncImageCache.set(url, state)
    }
    
    // ...

    // 5. 根据当前状态显示不同的 UI
    Box(modifier = modifier) { // 使用 Box 来确保占位符和图片尺寸一致
        when (val currentState = state) {
            is AsyncImageState.Success -> {
                Image(
                    bitmap = currentState.bitmap,
                    contentDescription = contentDescription,
                    modifier = Modifier.matchParentSize(), // 图片填满 Box
                    contentScale = contentScale
                )
            }
        }
    }
}

ViewModel 简单支持

ViewModel 目前也没有支持,因此也需要自己做一个简单实现。如下所示,通过rememberLifecycleAware即可创建一个与 Compose 组件生命周期一致的 ViewModel。

kotlin 复制代码
@Composable
fun <T : LifecycleAware> rememberLifecycleAware(
    // producer 是一个生产(创建)我们对象的 lambda
    producer: (scope: CoroutineScope) -> T
): T {
    val scope = rememberCoroutineScope()
    // 1. 使用 remember 创建并记住实例
    val instance = remember { producer(scope) }

    // 2. 绑定通用的 onStart/onStop 生命周期
    DisposableEffect(instance) {
        instance.onCreate()
        onDispose {
            instance.onDestroy()
        }
    }

    // 3. 返回实例
    return instance
}

// 创建一个跟 Compose 组件生命周期一致的 ViewModel
val monthlyReportViewModel = rememberLifecycleAware { scope ->
    MonthlyReportViewModel(scope)
}

internal class MonthlyReportViewModel(val scope: CoroutineScope) : LifecycleAware {
    override fun onCreate() {
        scope.launch{
            // ...
        }
    }

    override fun onDestroy() {
    }
}

WebView 与音频播放支持

WebView 与音频播放都必须使用平台相关能力,因此需要与原生层进行交互。 而 ovCompose 支持与 Android, iOS, 鸿蒙原生组件嵌套,同时也支持与原生 API 互操作。 如下所示,就是一个 Compose 嵌套 ArkUIView 的示例,其他的实现可参见源码。

kotlin 复制代码
@Composable
actual fun ActualWebView(
    state: WebViewState, modifier: Modifier
) {
    OhosWebView(state, modifier)
}

@OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class)
@Composable
fun OhosWebView(
    state: WebViewState, modifier: Modifier
) {
    ArkUIView(
        name = "harmonyWebView",
        modifier = modifier,
        parameter = js {
            "url"(state.webUrl)
            "backgroundColor"("#FF0000FF")
        },
    )
}

其他注意事项

  • 网络库:Ktor 等库也没有适配鸿蒙,网络库这里使用了腾讯开源的NetworkKMM。 在接入网络库的过程中需要注意鸿蒙平台需要配置linkerOpts,以正确链接lpbcurlwrapper等库
  • 反序列化:反序化则是在最近已经支持了 kotlinx.serialization,直接使用即可
  • 添加资源:图片资源新增时需要在 ComposeApp 与 harmonyApp 中都添加一份

下载使用

Android 平台

可通过链接下载:www.pgyer.com/8SryRWGk,或者也可以通过扫码安装:

iOS 平台

由于未申请 iOS 开发者证书,只能下载源码编译安装到模拟器或者自己手机

鸿蒙平台

鸿蒙平台提审中,如需体验也可先下载源码编译安装

项目地址

github.com/RicardoJian...

相关推荐
2501_915106322 小时前
Xcode 上传 ipa 全流程详解 App Store 上架流程、uni-app 生成 ipa 文件上传与审核指南
android·macos·ios·小程序·uni-app·iphone·xcode
消失的旧时光-19432 小时前
Kotlinx.serialization 使用讲解
android·数据结构·android jetpack
charlie1145141912 小时前
Kotlin 的 apply / with / run 详解
开发语言·kotlin·程序设计·面对对象
猛码Memmat3 小时前
华为HarmonyOS开发文档
华为·harmonyos
灿烂阳光g3 小时前
SELinux 策略文件编写
android·linux
.豆鲨包3 小时前
【Android】Viewpager2实现无限轮播图
android·java
xiangxiongfly9153 小时前
Android CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout实现折叠置顶效果
android·折叠
祥睿夫子3 小时前
ArkTS 未被深挖的核心点:静态多态限制、静态成员与单例实战
harmonyos
柿蒂4 小时前
从if-else和switch,聊聊“八股“的作用
android·java·kotlin