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.painterResourceImage(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.currentval id = context.resources.getIdentifier(res, "font", context.packageName)return Font(id, weight, style)
}

使用时创建Font对象:

object Fonts {@Composablefun 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 % 60return 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 % 60return 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: () -> Voidfunc 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 = .rightswipeGestureRecognizer.numberOfTouchesRequired = 1containerController.view.addGestureRecognizer(swipeGestureRecognizer)return containerController}func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}func makeCoordinator() -> Coordinator {Coordinator(onSwipe: onSwipe)}class Coordinator: NSObject, UIGestureRecognizerDelegate {var onSwipe: () -> Voidvar 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.hqwc.cn/news/18710.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分布式搜索--elasticsearch

一、初识 elasticsearch 1. 了解 ES ① elasticsearch 是一款非常强大的开源 搜索引擎&#xff0c;可以帮助我们从海量数据中 快速找到需要的内容 ② elasticsearch 结合 kibana、Logstash、 Beats&#xff0c;也就是 elastic stack (ELK)&#xff0c;被 广泛应用在日志数据分…

架构师日记-到底该如何搭建一个新系统 | 京东云技术团队

一 前言 架构设计按照实施过程可分为工程架构&#xff0c;业务架构&#xff0c;部署架构等多个维度&#xff0c;一个好的系统架构标准应该具备可扩展、可维护、可靠性、安全性和高性能等特点。尽管这些特点大家都熟知&#xff0c;但在实际落地时&#xff0c;我们更为迫切的想知…

pytorch线性模型 学习前要学习的基础知识

跟着刘二大人学pytorch&#xff0c;补全一下我的基础缺失 1.numpy基础 import numpy as np from PIL import Image anp.array([1,2,3]) #生成一维数组 print(a) bnp.arange(1,4)#创建等差数组&#xff0c;默认等差是1&#xff0c;数组为1&#xff0c;2&#xff0c;3&#xff0…

Efficient Methods for Non-stationary Online Learning

Dynamic regret Adaptive regret 假设&#xff1a; 算法过程&#xff1a; Regret分析

D. Pairs of Segments

Problem - D - Codeforces 思路&#xff1a;其实它求的就是不相交区间的最大数量&#xff0c;但是它的区间是两个区间合并得到&#xff0c;所以我们可以直接将所有能合并的区间直接合并&#xff0c;然后做一遍不相交区间的最大数量&#xff0c;这样存在一种问题就是一个区间会不…

SolidWorks二次开发-BOM球标和材料表

目标先到100&#xff0c;实在没什么好写的了&#xff0c;先把这两个简单的功能列一下吧。 private void btnInsertBalloon_Click(object sender, EventArgs e){//插入对应的BOM气泡球 球标//操作步骤->选中视图&#xff0c;执行自动球标命令SldWorks swApp Utility.Conne…

Flowable边界事件-定时边界事件

定时边界事件 定时边界事件一、定义1. 图形标记2. 完整的流程图3. XML标记 二、测试用例2.1 定时边界事件xml文件2.2 定时边界事件测试用例 总结 定时边界事件 一、定义 时间达到设定的时间之后触发事件 由于定时边界事件和开始定时事件几乎差不多&#xff0c;四种情况我就不一…

linux入门练级篇 第三讲 基本指令3

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;&#x1f35f;&#x1f32f;C语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f…

揭秘Dalio全天候策略:基于中美市场ETF的量化回测

01 引言 Ray Dalio 是全球最大的对冲基金——桥水联合基金&#xff08;Bridgewater Associates&#xff09;的创始人和首席投资官&#xff0c;其投资哲学在金融界中广为人知。他开创了一种被称为"全天候策略"&#xff08;All Weather Strategy&#xff09;的投资策略…

【微信小程序-uniapp】CustomPicker 自定义单项选择器组件

1. 效果图 2. 组件完整代码 <template><view class="custom-picker"><view :class=<

mybatis 注解方式操作 sql

前言:注解的方式在某些查询的时候还是比较方便的 mybatis注解配置 mapUnderscoreToCamelCase 配置Select 注解Insert 注解Delete 注解 和 Update 注解Provider 注解 mapUnderscoreToCamelCase 配置 别名设置&#xff0c;mapUnderscoreToCamelCase 配置 配置可以将 带下划线 sq…

AtcoderABC309场

A - NineA - Nine 题目大意 判断两个数是否相邻且水平排列&#xff0c;即它们在同一行并且相邻。可以直接打印或者找规律 思路分析 可以直接打印或者找规律 时间复杂度 O&#xff08;1&#xff09; 代码 #include<bits/stdc.h> using namespace std; int main(){i…