Android Jetpack Compose多平台用于Android和IOS

news2024/12/27 18:31:54

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文章!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode47. 全排列 II 回溯剪枝的细节问题

题目描述&#xff1a; 1、思路 作为回溯算法的经典问题&#xff0c;常用的方法是&#xff0c;每次dfs前先判断是否达到临界条件&#xff0c;满足条件则加入结果集并return。通过循环和dfs来构建树&#xff0c;查找出全部满足条件的集合。 例如本题&#xff0c;如1&…

dp动态规划详解下

dp动态规划详解上&#xff1a;https://blog.csdn.net/weixin_73936404/article/details/131527247?spm1001.2014.3001.5501 目录 dp动态规划详解上&#xff1a;https://blog.csdn.net/weixin_73936404/article/details/131527247?spm1001.2014.3001.5501 【案例1】 【题目描…

强度(极限强度、屈服强度)、韧性材料与脆性材料(韧性材料与脆性材料的强度、为什么脆性材料在压缩时比在拉伸时更坚固?)、材料的延展性、韧性、弹性

1.强度 强度&#xff08;Strength&#xff09;是材料可以承受的应力的量度&#xff0c;通常使用极限强度&#xff08;Ultimate strength&#xff09;和屈服强度&#xff08;Yield strength&#xff09;来定义材料的强度-极限。 材料的极限抗拉强度定义为在拉伸试验过程中达到的…

windows 编译libyuv

一、libyuv下载 git clone https://chromium.googlesource.com/external/libyuv 二、libjpeg-turbo下 git clone https://github.com/libjpeg-turbo/libjpeg-turbo.git 三、编译可以参考 BUILDING.md 需要环境&#xff1a; VS2019 CMake YASM 启动vs工具 编译&#xff1…

js模块化开发

◼ 到底什么是模块化、模块化开发呢&#xff1f;  事实上模块化开发最终的目的是将程序划分成一个个小的结构&#xff1b;  这个结构中编写属于自己的逻辑代码&#xff0c;有自己的作用域&#xff0c;定义变量名词时不会影响到其他的结构&#xff1b;  这个结构可以将自己…

SAP S4 Hana 下面ACDOCA 凭证行字段增强创建过程

网上找到这个类下面是可以新增增强的 现在需要在如下位置建立四代增强点 保存以后会出现下面的增强项 保存激活后&#xff0c;完成在源程序中增加了一个4代显式增强点.上面步骤只是在程序中建立了一个增强点&#xff0c;并没有执行什么动作&#xff0c;就相当于建立一个容器。如…

Python将Excel数字对应列的字母写成字典(json)

在日常的办公中&#xff0c;我们经常需要利用python去读写Excel类型的文件&#xff0c;有时候我们需要将每个数字代表的列的字母表现出来&#xff0c;那么我们可以利用Python实现&#xff0c;而且今天的代码可以根据自己的需求去任意的改变你想规定的长度 如,或者更长 a {1:…

1分钟搭建VPN服务器

1分钟搭建一个VPN服务器 VPN技术在保障网络通信安全和隐私上发挥着重要作用。IPsec VPN是其中一种常用的VPN模式。本文将介绍如何通过使用Docker来快速搭建IPsec VPN Server。 什么是IPsec VPN&#xff1f; IPsec即Internet Protocol Security&#xff0c;是一种用于保护互联…

「2024」预备研究生mem- 形式逻辑强化:逻辑的特殊文字表述方式(重点记忆)

一、形式逻辑强化&#xff1a;逻辑的特殊文字表述方式 二、课后题

关于torch.load报出找不到模型的错误,但路径明明正确

后来发现是因为使用 torch.save(model,save.pt) 会保存整个文件时会默认保存训练py文件的父目录&#xff0c;用torch.load导入文件时搜索路径必须有此父路径&#xff0c;否则将会提示no model named model这样的错误 解决办法是使用sys.path.apend把该父目录加入搜索路径中 …

Java开发 - 探寻Spring的秘密

前言 Spring是企业级J2EE一站式解决方案&#xff0c;提供了整个项目的表现层、业务层、持久层&#xff0c;而且&#xff0c;它的生态特别完善&#xff0c;可以和其他框架无缝对接&#xff0c;现在做Java的哪个项目里没有Spring的说出不去都不信。但往往我们开发者只重视项目是…

重定向:电商行业打败对手的杀手锏

重定向是一种在线营销策略&#xff0c;针对对产品或服务表示兴趣的潜在客户。可以追踪那些访问过您的网站、但未进行过消费的用户&#xff0c;再次向他们展示相关产品&#xff0c;激起消费欲。再营销则是可以追踪那些将商品加入网页购物车&#xff0c;但最后没有购买的用户&…

物流RFID设备一般在哪些场景应用?

随着现代物流行业的快速发展&#xff0c;传统条码技术信息量少&#xff0c;易脏污损毁&#xff0c;耐用性不高等问题很难满足物流企业多样化的需求&#xff0c;物流RFID设备的应用也越来越广泛。下面我们就跟大家一起来分析一下&#xff0c;物流RFID设备可以在哪些场景中应用。…

计算机体系结构基础知识介绍之动态调度(三)

首先回顾一下tomasulo算法&#xff0c; Tomasulo算法的第一个优点是分布式的冒险检测逻辑&#xff0c;这是通过使用预留站和公共数据总线实现的。预留站是一种存储指令和操作数的缓冲区&#xff0c;每个功能单元都有自己的预留站。公共数据总线是一种广播结果的方式&#xff0…

SpringBoot07:Thymeleaf模板引擎

目录 一、Thymeleaf 1、模板引擎 2、引入Thymeleaf 3、Thymeleaf分析 二、测试 1、编写一个TestController 2、编写一个测试页面welcome.html放在templates目录下 3、启动项目请求测试 三、Thymeleaf语法学习 1、修改测试请求&#xff0c;增加数据传输 2、要使用thy…

zabbix服务部署

文章目录 zabbix1 zabbix简介1.1 组成部件1.2 监控原理1.3 Zabbix 6.0新特性1.4 Zabbix6.0功能组件1.4.1 Zabbix Server1.4.2 数据库1.4.3 Web界面1.4.4 Zabbix Agent1.4.5 Zabbix Proxy1.4.6 Java Gateway 2 部署zabbix服务端2.1 部署Nginx2.2 安装PHP2.3 修改Nginx配置2.4 修…

Docker安装ElasticSearch7.14.0 docker安装elasticsearch7.14.0完整详细教程

Docker安装ElasticSearch7.14 docker安装elasticsearch7.14完整详细教程 Docker 上安装 ElasticSearch 7.14.0 的步骤&#xff1a;选择要安装的ElasticSearch 版本1、拉取 ElasticSearch 镜像2、创建并运行容器关闭容器启动容器重启容器 3、elasticsearch常用端口以及作用4、测…

耳夹式骨传导耳机测评!2023年最全耳夹骨传导耳机盘点

现在市面上的骨传导耳机品牌层出不穷&#xff0c;骨传导耳机好不好用&#xff0c;主要还是看耳机的品牌背景以及独家的音质技术调配&#xff0c;较大的骨传导耳机品牌在购买时售后以及使用体验上都具有一定的保障&#xff0c;下面就分享一些值得入手的骨传导耳机给大家吧~ 第一…

安装.net framework3.5 无法打开运行空间池,服务器管理器winRM插件可能已经损坏

解决方案&#xff1a; 1.以管理员权限打开命令提示符&#xff0c;然后运行如下命令&#xff1a; "reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f" 2.然后运行"winrm quickco…

玩转 TypeScript 中【class类】的定义与使用方法解读。

目录 类的概念类的继承 &#xff1a;类的存取器&#xff1a;类的静态方法与静态属性&#xff1a;类的修饰符&#xff1a;参数属性&#xff1a;抽象类&#xff1a;类的类型: 类的概念 类是用于创建对象的模板。他们用代码封装数据以处理该数据。JavaScript 中的类建立在原型上&a…