Android Jetpack Compose多平台用于Android和IOS

Android Jetpack Compose多平台用于Android和IOS

JetBrains和外部开源贡献者已经努力工作了几年时间来开发Compose Multiplatform,并最近发布了适用于iOS的Alpha版本。自然地,我们对其功能进行了测试,并决定通过使用该框架在iOS上运行我们的Dribbble复制音乐应用程序来进行实验,看看可能会出现什么挑战。

Compose Multiplatform面向桌面和iOS平台,利用Skia的功能,Skia是一个广泛应用于不同平台的开源2D图形库。Google Chrome、ChromeOS、Mozilla Firefox以及JetPack Compose和Flutter等广泛采用Skia作为其引擎。

Compose Multiplatform架构

为了理解Compose Multiplatform的方法,我们首先研究了JetBrains提供的概述,其中包括Kotlin Multiplatform Mobile(KMM)。

正如图表所示,Kotlin Multiplatform的一般方法包括:

  1. 为iOS特定的API(如蓝牙、CodeData等)编写代码。
  2. 为业务逻辑创建共享代码。
  3. 在iOS端创建UI。

Compose Multiplatform引入了共享UI代码的能力,不仅可以共享业务逻辑代码,还可以共享UI代码。您可以选择使用本机iOS UI框架(UIKit或SwiftUI),或直接将iOS代码嵌入Compose。我们希望查看我们在Android上复杂的本机UI在iOS上的工作情况,因此我们选择将本机iOS UI代码限制在最小范围内。目前,您只能使用Swift代码编写特定于平台的API,而对于特定于平台的UI,可以使用Kotlin和Jetpack Compose与Android应用程序共享所有其他代码。

如图所示,Kotlin Multiplatform的一般方法包括:

  1. 编写专门针对iOS API(如蓝牙和CodeData)的代码。
  2. 创建用Kotlin编写的共享业务逻辑代码。
  3. 在iOS端创建UI。

Compose Multiplatform扩展了代码共享的功能,现在您不仅可以共享业务逻辑代码,还可以共享UI代码。您仍然可以使用SwiftUI创建UI,或将UIKit直接嵌入Compose,我们将在下面进行讨论。通过这个新的开发方式,您只需要使用Swift代码来处理特定于平台的API和UI,而可以使用Kotlin和Jetpack Compose与Android应用程序共享其他所有代码。现在,让我们深入探讨启动所需的准备工作。

在iOS上运行的先决条件

获取iOS设置说明的最佳位置是官方文档本身。总结如下,以下是开始的所需条件:

  • Mac电脑
  • Xcode
  • Android Studio
  • Kotlin Multiplatform Mobile插件
  • CocoaPods依赖管理器

此外,JetBrains存储库中提供了一个模板,可以帮助处理多个Gradle设置。

https://github.com/JetBrains/compose-multiplatform-ios-android-template/#readme

项目结构

设置了基础项目后,您将看到三个主要目录:

  • androidApp
  • iosApp
  • shared

androidApp和shared是模块,因为它们与Android相关并使用build.gradle构建。iosApp是实际iOS应用程序的目录,您可以通过Xcode打开。androidApp模块只是Android应用程序的入口点。以下代码对于任何曾经为Android开发过的人来说都是熟悉的。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainView()
        }
    }
}

iosApp是iOS应用程序的入口点,其中包含一些样板式的SwiftUI代码:

import SwiftUI

@main
struct iOSApp: App {
 var body: some Scene {
  WindowGroup {
   ContentView()
  }
 }
}

由于这是入口点,您应该在这里实现顶层的更改——例如,我们添加了ignoresSafeArea修饰符,以在全屏显示应用程序:

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return Main_iosKt.MainViewController()
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
    var body: some View {
        ComposeView()
            .ignoresSafeArea(.all)
    }
}

上述代码已经可以在iOS上运行您的Android应用程序。在这里,您的ComposeUIViewController被包装在一个UIKit的UIViewController中,并呈现给用户。MainViewController()位于名为main.ios.kt的Kotlin文件中,而App()包含了Compose应用程序的代码。

fun MainViewController() = ComposeUIViewController { App()}

以下是JetBrains提供的另一个示例。

https://github.com/JetBrains/compose-multiplatform/tree/master/examples/chat

如果您需要一些特定于平台的功能,可以使用UIKitView将UIKit嵌入到Compose代码中。以下是JetBrains的一个地图视图示例。在使用UIKit时,与在Compose中使用AndroidView非常相似,如果您已经熟悉该概念的话。

https://github.com/JetBrains/compose-multiplatform/blob/ea310cede5f08f7960957369247a6575f7bc5392/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt#L7

shared模块是这三个模块中最重要的一个。这个Kotlin模块实质上包含了Android和iOS实现的共享逻辑,促进了在两个平台上使用相同的代码库。在shared模块中,您会发现三个目录,每个目录都有自己的用途:commonMain、androidMain和iosMain。这是一个令人困惑的地方-实际上,实际共享的代码位于commonMain目录中。其他两个目录用于编写特定于平台的Kotlin代码,在Android或iOS上会有不同的行为或外观。这是通过在commonMain代码中编写expect fun,并在相应的平台目录中使用actual Fun来实现的。

Migration

在开始迁移时,我们确信会遇到一些需要特定修复的问题。尽管我们选择迁移的应用程序在逻辑上非常简单(基本上只有UI、动画和过渡),但如预期的那样,我们遇到了相当多的困难。以下是在迁移过程中可能遇到的一些问题。

Resource

我们首先要处理的是资源的使用。没有动态生成的R类,这仅适用于Android。相反,您需要将资源放在资源目录中,并将路径指定为字符串。以下是一个图像的示例:

import org.jetbrains.compose.resources.painterResource

Image(
    painter = painterResource(“image.webp”),
    contentDescription = "",
)

当以这种方式实现资源时,如果资源名称不正确,可能会发生运行时崩溃,而不是编译时崩溃。

此外,如果您在XML文件中引用Android资源,还需要摆脱与Android平台的链接。

<vector xmlns:android="http://schemas.android.com/apk/res/android" 
    android:width="24dp"
    android:height="24dp" 
-   android:tint="?attr/colorControlNormal"     
+   android:tint="#000000"
    android:viewportWidth="24"
    android:viewportHeight="24">
-   <path android:fillColor="@android:color/white"
+   <path android:fillColor="#000000"
        android:pathData="M9.31,6.71c-0.39,0.39 -0.39,1.02 0,1.41L13.19,12l-3.88" />
</vector>

Font

在编写本文时,Compose Multiplatform中没有使用iOS和Android上常用的标准字体加载技术的方法。据我们所见,Jetbrains建议使用字节数组来加载字体,如下所示的iOS代码:

private val cache: MutableMap<String, Font> = mutableMapOf()

@OptIn(ExperimentalResourceApi::class)
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
    return cache.getOrPut(res) {
        val byteArray = runBlocking {
            resource("font/$res.ttf").readBytes()
        }
        androidx.compose.ui.text.platform.Font(res, byteArray, weight, style)
    }
}

然而,我们不喜欢异步的方法,也不喜欢在执行过程中阻塞主线程的runBlocking的使用。因此,在Android上,我们决定采用一种更常见的方法,使用整数标识符:

@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font {
    val context = LocalContext.current
    val id = context.resources.getIdentifier(res, "font", context.packageName)
    return Font(id, weight, style)
}

使用时创建Font对象:

object Fonts {
    @Composable
    fun abrilFontFamily() = FontFamily(
        font(
            "Abril",
            "abril_fatface",
            FontWeight.Normal,
            FontStyle.Normal
        ),
    )
}

使用Kotlin替换Java


在Compose Multiplatform中不可能使用Java代码,因为它使用Kotlin编译器插件。因此,我们需要重写使用到Java代码的部分。例如,在我们的应用中,一个时间格式化器将音乐曲目的时间从秒转换为更方便的分钟格式。我们不得不放弃使用java.util.concurrent.TimeUnit,但事实证明这是好事,因为它给了我们重构代码并更优雅地编写代码的机会。

fun format(playbackTimeSeconds: Long): String {
-   val minutes = TimeUnit.SECONDS.toMinutes(playbackTimeSeconds)
+   val minutes = playbackTimeSeconds / 60
-   val seconds = if (playbackTimeSeconds < 60) {
-       playbackTimeSeconds
-   } else {
-       (playbackTimeSeconds - TimeUnit.MINUTES.toSeconds(minutes))
-   }
+   val seconds = playbackTimeSeconds % 60
    return buildString {
        if (minutes < 10) append(0)
        append(minutes)
        append(":")
        if (seconds < 10) append(0)
        append(seconds)
    }
}

Native Canvas

有时候,我们会使用Android Native画布来创建绘图。然而,在Compose Multiplatform中,我们无法在通用代码中访问Android原生画布,因此代码必须进行相应的调整。例如,我们有一个动画标题文本,它依赖于本机画布的measureText(letter)函数,以实现逐字动画效果。我们不得不为这个功能寻找替代方法,所以我们使用了Compose画布来重写它,并使用TextMeasurer代替Paint.measureText(letter)

fun format(playbackTimeSeconds: Long): String {
-   val minutes = TimeUnit.SECONDS.toMinutes(playbackTimeSeconds)
+   val minutes = playbackTimeSeconds / 60
-   val seconds = if (playbackTimeSeconds < 60) {
-       playbackTimeSeconds
-   } else {
-       (playbackTimeSeconds - TimeUnit.MINUTES.toSeconds(minutes))
-   }
+   val seconds = playbackTimeSeconds % 60
    return buildString {
        if (minutes < 10) append(0)
        append(minutes)
        append(":")
        if (seconds < 10) append(0)
        append(seconds)
    }
}


drawText方法也依赖于本机画布,因此必须进行重写:

Gestures

在Android上,BackHandler始终可用 - 它处理后退手势或后退按钮按下,具体取决于设备可用的导航模式。但是这种方法在Compose Multiplatform中不起作用,因为BackHandler是Android源集的一部分。相反,让我们使用expect fun

@Composable
expect fun BackHandler(isEnabled: Boolean, onBack: ()-> Unit)

//Android implementation
@Composable
actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) {
  BackHandler(isEnabled, onBack)
}

在iOS中,可以提出许多不同的方法来实现所需的结果。例如,您可以在Compose中编写自己的后退手势,或者如果应用中有多个屏幕,可以将每个屏幕包装在单独的UIViewController中,并使用包含默认手势的本机iOS导航器UINavigationController

我们选择了一种在iOS侧处理后退手势的实现方式,而无需将单独的屏幕包装在相应的控制器中(因为我们的应用程序中的视图之间的过渡是高度定制的)。这是如何将这两种语言链接在一起的很好的示例。首先,我们添加了一个原生的iOS SwipeGestureViewController来检测手势,并为手势事件添加了处理程序。完整的iOS实现可以在这里看到。

https://github.com/exyte/ComposeMultiplatformDribbbleAudio/blob/main/iosApp/iosApp/ContentView.swift

struct SwipeGestureViewController: UIViewControllerRepresentable {
    var onSwipe: () -> Void
    
    func makeUIViewController(context: Context) -> UIViewController {
        let viewController = Main_iosKt.MainViewController()
        let containerController = ContainerViewController(child: viewController) {
            context.coordinator.startPoint = $0
        }
        
        let swipeGestureRecognizer = UISwipeGestureRecognizer(
            target:
                context.coordinator, action: #selector(Coordinator.handleSwipe)
        )
        swipeGestureRecognizer.direction = .right
        swipeGestureRecognizer.numberOfTouchesRequired = 1
        containerController.view.addGestureRecognizer(swipeGestureRecognizer)
        return containerController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(onSwipe: onSwipe)
    }
    
    class Coordinator: NSObject, UIGestureRecognizerDelegate {
        var onSwipe: () -> Void
        var startPoint: CGPoint?
        
        init(onSwipe: @escaping () -> Void) {
            self.onSwipe = onSwipe
        }
        
        @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
            if gesture.state == .ended, let startPoint = startPoint, startPoint.x < 50 {
                onSwipe()
            }
        }
        
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            true
        }
    }
}

然后,在main.ios.kt文件中创建一个相应的函数:

fun onBackGesture() {
    store.send(Action.OnBackPressed)
}

我们可以在Swift中像这样调用这个函数:

public func onBackGesture() {
    Main_iosKt.onBackGesture()
}

我们实现了一个收集动作的存储库。

interface Store {
    fun send(action: Action)
    val events: SharedFlow<Action>
}

fun CoroutineScope.createStore(): Store {
    val events = MutableSharedFlow<Action>()

    return object : Store {
        override fun send(action: Action) {
            launch {
                events.emit(action)
            }
        }
        override val events: SharedFlow<Action> = events.asSharedFlow()
    }
}

该存储库使用store.events.collect方法累积动作。

@Composable
actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) {
    LaunchedEffect(isEnabled) {
        store.events.collect {
            if(isEnabled) {
                onBack()
            }
        }
    }
}

这有助于解决两个平台上手势处理的差异,使iOS应用程序在后退导航方面具有原生和直观的体验。

最少Bug

在某些情况下,您可能会遇到一些次要问题,例如在iOS平台上,当点击时,项目会向上滚动以变得可见。您可以将期望的行为(Android)与下面的错误iOS行为进行比较:

这是因为Modifier.clickable在项目被点击时使其获得焦点,从而触发bringIntoView滚动机制。Android和iOS上的焦点管理不同,导致了这种不同的行为。我们通过为项目添加.focusProperties { canFocus = false }修饰符来解决这个问题。

结论

Compose Multiplatform是Kotlin语言在KMM之后的多平台开发的下一个阶段。这项技术为代码共享提供了更多的机会,不仅限于业务逻辑,还包括UI组件。尽管在多平台应用程序中可以结合使用Compose和SwiftUI,但目前看起来并不是很直观。

您应该考虑您的应用程序是否具有可从多个平台共享代码的业务逻辑、UI或功能能力。如果您的应用程序需要许多特定于平台的功能,KMM和Compose Multiplatform可能不是最佳选择。该存储库包含完整的实现。您还可以查看现有的库,以更加了解当前KMM的功能。

https://github.com/terrakok/kmm-awesome

至于我们,我们对Compose Multiplatform印象深刻,并认为一旦发布稳定版本,它可以在我们的实际项目中使用。它最适合于UI较重的应用程序,没有大量特定于硬件的功能。它可能是Flutter和原生开发的可行替代方案,但时间将证明一切。与此同时,我们将继续专注于原生开发-请查看我们的iOS和Android文章!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/38704.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

排序算法总结

目录 插入排序和希尔排序 堆排序 归并排序 快速排序 桶排序、计数排序、基数排序 这些排序的比较 冒泡排序和选择排序就不说了&#xff0c;直接介绍下面的几种排序算法&#xff1a; 插入排序和希尔排序 插入排序与希尔排序_小白麋鹿的博客-CSDN博客https://blog.csdn.n…

C国演义 [第十二章]

第十二章 打家劫舍题目理解步骤dp数组递推公式初始化遍历顺序 代码 打家劫舍II题目理解步骤递推公式初始化遍历顺序 代码 打家劫舍 力扣链接 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋…

如何保证消息的可靠性+延迟队列(TTL+死信队列+延迟队列)

目录 1.如何保证消息的可靠性 1.1.消息的可靠投递 confirm机制 return机制 1.2.如何保证消息在队列中不丢失 1.3.确保消息能可靠的被消费掉 2.延迟队列 2.1.TTL 2.2.死信队列 2.3.延迟队列 3.如何防止消费者重复消费消息 1.如何保证消息的可靠性 1.1.消息的可靠投递…

VMware扩展磁盘提示:在部分链上无法执行所调用的函数。请打开父虚拟磁盘

VMware扩展磁盘提示&#xff1a;在部分链上无法执行所调用的函数。请打开父虚拟磁盘 在为VMware中的虚拟机扩展磁盘时提示&#xff1a;在部分链上无法执行所调用的函数。请打开父虚拟磁盘。 出现这个问题是因为你先前创建过快照&#xff0c;但是快照删除时候&#xff0c;残余文…

【数据结构】链表及无头单向非循环链表实现

目录 1.顺序表的问题 2.链表的概念、结构及分类 3.无头单向非循环链表实现 3.1创建节点 3.2头插数据 3.3头删数据 3.4尾插 3.5尾删 3.6链表销毁 3.7查找一个元素 3.8在pos之前插入 3.9在pos之后插入 3.10删除pos位置 3.11删除pos之后的位置 1.顺序表的问题 顺…

Linux操作系统——第五章 进程信号

目录 信号概念 用kill -l命令可以察看系统定义的信号列表 信号处理常见方式概览 产生信号 1. 通过终端按键产生信号 2. 调用系统函数向进程发信号 3. 由软件条件产生信号 4. 硬件异常产生信号 阻塞信号 1. 信号其他相关常见概念 2. 在内核中的表示 3. sigset_t 4.…

C语言——指针详解(进阶)

轻松学会C语言指针 一、字符指针二、数组指针2.1 数组指针的定义2.2 &数组名VS数组名2.3 数组指针的使用 三、指针数组四、数组参数和指针参数4.1 一维数组传参4.2 二维数组传参4.3 一级指针传参4.4 二级指针传参 五、函数指针六、函数指针数组七、指向函数指针数组的指针八…

Kubernetes - HPA-VPA - metrics介绍和安装 - HPA实验

目录 参考文章&#xff1a;(97条消息) Kubernetes-自动扩展器HPA、VPA、CA_hpa vpa_SRE运维充电站的博客-CSDN博客 HPA VPA 官方网址&#xff1a;autoscaler/vertical-pod-autoscaler at master kubernetes/autoscaler GitHub HPA和VPA进行扩缩容的区别&#xff1a; me…

小研究 - 面向 Java 的高对抗内存型 Webshell 检测技术(四)

由于 Web 应用程序的复杂性和重要性, 导致其成为网络攻击的主要目标之一。攻击者在入侵一个网站后, 通常会植入一个 Webshell, 来持久化控制网站。但随着攻防双方的博弈, 各种检测技术、终端安全产品被广泛应用, 使得传统的以文件形式驻留的 Webshell 越来越容易被检测到, 内存…

《TCP IP网络编程》第七章

第七章&#xff1a;优雅的断开套接字的连接 TCP 的断开连接过程比建立连接更重要&#xff0c;因为连接过程中一般不会出现大问题&#xff0c;但是断开过程可能发生预想不到的情况。因此应该准确掌控。所以要掌握半关闭&#xff08;Half-close&#xff09;&#xff0c;才能明确断…

windows10 搭建hadoop环境,并且使用hadoop命令

hadoop 环境创建 1. 八、window搭建spark IDEA开发环境 按照步骤安装完 2. windows下安装和配置hadoop 配置环境变量&#xff0c;注意JAVA_HOME路径&#xff0c;修改后&#xff0c;重启电脑&#xff0c;不重启容易报错&#xff01;&#xff01;&#xff01; ​ 新建dat…

数据结构(王道)——数据结构之 二叉树

一、数据结构之 二叉树概念&#xff1a; 特殊的二叉树结构&#xff1a; 满二叉树完全二叉树 二叉排序树 平衡二叉树 二叉树基本概念总结&#xff1a; 二、二叉树的常用性质&#xff1a; 1、叶子结点比二分支结点多一个

Kubernetes - kubeadm部署

Kubernetes - kubeadm部署 1 环境准备1.1 在各个节点上配置主机名&#xff0c;并配置 Hosts 文件1.2 关闭防护墙&#xff0c;禁用selinux&#xff0c;关闭swap1.3 配置免密登录1.4 配置内核参数1.5 配置br_netfilter 2. 安装K8s2.1 安装docker(各节点)2.2 安装K8s组件(各节点)2…

走进Vue2飞入Vue3

✅作者简介&#xff1a;大家好&#xff0c;我是Cisyam&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Cisyam-Shark的博客 &#x1f49e;当前专栏&#xff1a; Vue ✨特色专栏&#xff…

“掌握更多的快速排序技巧:三路划分、双路快排和非递归的深入理解”

快速排序是一种基于分治思想的排序算法&#xff0c;它能够以极快的速度将一个乱序的数组重新排列成有序的序列。不仅如此&#xff0c;快速排序还具有简洁的实现代码和良好的可扩展性&#xff0c;成为最受欢迎的排序算法之一。接下来&#xff0c;让我带你了解一下它的魅力吧&…

【Matlab】智能优化算法_麻雀搜索算法SSA

【Matlab】智能优化算法_麻雀搜索算法SSA 1.背景介绍2.数学模型3.文件结构4.伪代码5.详细代码及注释5.1 Get_Functions_details.m5.2 main.m5.3 SSA.m 6.运行结果7.参考文献 1.背景介绍 麻雀通常是群居的鸟类&#xff0c;有很多种类。它们分布在世界的大部分地区&#xff0c;喜…

Http host 标头攻击

一、什么是http host 标头攻击 HTTP Host 标头攻击是一种网络安全攻击技术&#xff0c;利用了 HTTP 协议中的 Host 标头字段的漏洞。Host 标头字段用于指定客户端请求的目标主机名或域名。 攻击者可以通过构造恶意的 HTTP 请求&#xff0c;伪造或篡改 Host 标头字段的值&#x…

高精尖领域数据暴增,分布式存储渐当大任

近年来&#xff0c;数据存储市场“最靓的仔”无疑就是分布式存储。 大模型火了之后&#xff0c;围绕Chat的应用也越来越多&#xff0c;通过AI生成图片、报表、音视频的应用比比皆是。众所周知&#xff0c;要想训练出一个有学习能力的、可理解的、响应迅速的大模型应用&#xf…

[NGINX] NGINX下载、安装编译、启动检查停止命令

一、NGINX 下载 mkdir -p /soft/nginx cd /soft/nginx wget https://nginx.org/download/nginx-1.21.6.tar.gz二、下载相关依赖 ①在线安装依赖&#xff1a; yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel ②下载依赖到本地安装依赖&#xff1a; y…

InsCode Stable Diffusion使用教程【InsCode Stable Diffusion美图活动一期】

记录一下如何使用 InsCode Stable Diffusion 进行 AI 绘图以及使用感受。 一、背景介绍 目前市面上比较权威&#xff0c;并能用于工作中的 AI 绘画软件其实就两款。一个叫 Midjourney&#xff08;简称 MJ&#xff09;&#xff0c;另一个叫 Stable Diffusion&#xff08;简称 …