13 hours in the past
JetBrains and exterior open supply contributors have been arduous at work on Compose Multiplatform for a number of years and not too long ago launched an alpha model for iOS. Naturally, we have been curious to check its performance, so we determined to check it out by attempting to run our software. Dribbble clone music app on iOS utilizing the framework and see what challenges may come up.
Composing Cross-Platform for Desktop and iOS harnesses the capabilities of Skia, an open supply 2D graphics library extensively used throughout totally different platforms. Extensively used software program comparable to Google Chrome, ChromeOS and Mozilla Firefox is offered by Skia, together with JetPack Compose and Flutter.
Compose Cross-Platform Structure
To know the Compose Multiplatform strategy, we first checked out an outline from JetBrains itself, together with Kotlin Multiplatform Cell (KMM).
As you possibly can see within the diagram, the final strategy of Kotlin Multiplatform contains:
- Code for iOS-specific APIs like Bluetooth, CodeData, and so forth
- Shared code for enterprise logic
- The UI is on the iOS aspect.
Compose Multiplatform launched the flexibility to share not solely enterprise logic code, but in addition consumer interface code. You have got the selection of utilizing the native iOS UI framework (UIKit or SwiftUI) or embedding the iOS code instantly into Compose. We wished to see how our complicated native Android UI would work on iOS, so we selected to restrict the native iOS UI code to a minimal. At present, you possibly can solely use Swift code for platform-specific APIs, and for platform-specific consumer interfaces, all different code that you would be able to share with Android apps utilizing utilizing Kotlin and Jetpack Compose.
The final strategy of Kotlin Multiplatform, as proven within the diagram, can embrace:
- Write iOS API-specific code like Bluetooth and CodeData.
- Generate shared code for enterprise logic written in Kotlin.
- Create UI on iOS aspect.
Compose Multiplatform has prolonged code sharing, so now you can share not solely the enterprise logic, but in addition the consumer interface. You possibly can nonetheless use SwiftUI for the entrance finish, or embed UIKit instantly in Compose, which we’ll focus on under. With this new growth, you possibly can solely use Swift code for platform-specific APIs, platform-specific UI, and share all different code with Android apps utilizing Kotlin and Jetpack Compose. Let’s dive into the mandatory preparation for a startup.
Stipulations to run on iOS
The perfect place to seek out iOS setup directions is official doc. To summarize it right here, here is what you have to get began:
As well as, there’s a template out there in Jetbrains . Repositorycan assist with many present Gradle setups.
Undertaking construction
As soon as you’ve got arrange the bottom venture, you will see three foremost directories:
androidApp
And shared
are modules as a result of they’re associated to Android and constructed with construct.gradle
. The iosApp
is the folder of the particular ios app that you would be able to open by way of Xcode. The androidApp
module is simply an entry level for android software. The code under ought to be acquainted to anybody who has ever developed for Android.
class MainActivity : AppCompatActivity() {
override enjoyable onCreate(savedInstanceState: Bundle?) {
tremendous.onCreate(savedInstanceState)
setContent {
MainView()
}
}
}
iosApp
is the entry level for an iOS app with some boilerplate SwiftUI code:
import SwiftUI
@foremost
struct iOSApp: App {
var physique: some Scene {
WindowGroup {
ContentView()
}
}
}
Since this can be a place to begin, you must implement top-level modifications right here — for instance, we add ignoresSafeArea
modifier to point out the app in full display:
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return Main_iosKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var physique: some View {
ComposeView()
.ignoresSafeArea(.all)
}
}
The above code already lets you run your Android app on iOS. that is yours ComposeUIViewController
wrapped inside a UIKit UIViewController
and current it to the consumer. MainViewController()
situated within the Kotlin file foremost.ios.kt and App()
comprises the Composable software code.
enjoyable MainViewController() = ComposeUIViewController { App()}
That is one other instance from JetBrains.
In case you want some platform-specific performance, you possibly can embed UIKit in your Compose code with UIKitView. This is an instance with a map view from Jetbrains. Utilizing UIKit is similar to utilizing AndroidView inside Compose, in case you might be aware of the idea.
The shared
module is crucial one of many three. Primarily, this Kotlin module comprises shared logic for each Android and iOS implementations, facilitating using the identical codebase on each platforms. Within the shared module you can find three folders, every serving its personal function: commonMain
, androidMain
And iosMain
. This is a degree of confusion – in truth, the precise shared code is inside commonMain
class. The opposite two folders are for writing platform-specific Kotlin code that may work or look totally different on Android or iOS. That is executed by writing a anticipate enjoyable
inside commonMain
code and deploy it utilizing actualFun
within the respective platform folder.
migrate
At the start of the migration course of, we’re positive that we’ll encounter some points that require particular fixes. Regardless that the app we selected emigrate was very mild on logic (it is mainly simply the UI, animations and transitions), we encountered fairly a couple of obstacles as anticipated. Listed here are some chances are you’ll encounter your self throughout the transfer.
Assets
The very first thing we needed to take care of was useful resource utilization. There isn’t a dynamically generated R class, solely Android-specific. As a substitute you need to put a useful resource within the assets folder and you have to specify the trail as a string. Instance for a picture:
import org.jetbrains.compose.assets.painterResource
Picture(
painter = painterResource(“picture.webp”),
contentDescription = "",
)
When implementing assets on this method, chances are you’ll expertise runtime issues as a substitute of compile time issues attributable to incorrectly named assets.
Additionally, in case you point out Android assets in your xml recordsdata, you have to take away the hyperlinks to the Android platform as nicely:
android:top="24dp"
- android:tint="?attr/colorControlNormal"
+ android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
- + android:pathData="M9.31,6.71c-0.39,0.39 -0.39,1.02 0,1.41L13.19,12l-3.88" />
Font
There isn’t a method to make use of the usual font loading methods that you’d use on iOS and Android in Cross-Platform Compose on the time of this writing. From what we are able to see, Jetbrains suggests loading fonts utilizing byteArray
as within the iOS code under:
personal val cache: MutableMap = mutableMapOf()
@OptIn(ExperimentalResourceApi::class)
@Composable
precise enjoyable font(title: String, res: String, weight: FontWeight, model: FontStyle): Font {
return cache.getOrPut(res) {
val byteArray = runBlocking {
useful resource("font/$res.ttf").readBytes()
}
androidx.compose.ui.textual content.platform.Font(res, byteArray, weight, model)
}
}
Nonetheless, we do not just like the asynchronous strategy or the truth that runBlocking
is used, this can block the principle UI thread throughout execution. So on Android we determined to make use of a extra widespread strategy with integer identifiers:
@Composable
precise enjoyable font(title: String, res: String, weight: FontWeight, model: FontStyle): Font {
val context = LocalContext.present
val id = context.assets.getIdentifier(res, "font", context.packageName)
return Font(id, weight, model)
}
We are able to make a Fonts
object and use it as wanted for comfort.
object Fonts {
@Composable
enjoyable abrilFontFamily() = FontFamily(
font(
"Abril",
"abril_fatface",
FontWeight.Regular,
FontStyle.Regular
),
)
}
Substitute Java with Kotlin
Java can’t be utilized in code, as a result of Compose Multiplatform makes use of the Kotlin compiler plugin. Subsequently, we have to rewrite the components of Java code which might be used. For instance, in our software, a Time
Formatter converts observe time in seconds to a extra handy format in minutes. We had to surrender utilizing java.util.concurrent.TimeUnit
however it turned out to be good as a result of it gave us an opportunity to refactor the code and write nicer code.
enjoyable format(playbackTimeSeconds: Lengthy): 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
Generally we use Android native canvas to create drawings. Nonetheless, in Compose Multiplatform, we do not have entry to Android’s native canvas within the widespread code, and this code needs to be tailored accordingly. For instance, we’ve got a dynamic title textual content primarily based on measureText(letter)
perform from the unique canvas to allow letter-by-letter animation. We needed to discover a totally different strategy to this performance, so we rewrote it with the Compose canvas and used TextMeasurer
as a substitute of Paint.measureText(letter)
enjoyable format(playbackTimeSeconds: Lengthy): 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)
}
}
alphabet.forEach { letter ->
- sizeMap[letter] = textPaint.measureText(letter.toString())
+ sizeMap[letter] = textMeasurer.measure(
+ textual content = AnnotatedString(
+ textual content = letter.toString(),
+ spanStyle = spanStyle
+ )
+ ).dimension.width
}
The drawText
the strategy can also be primarily based on the unique canvas and needs to be rewritten:
-it.nativeCanvas.drawText(
- textual content,
- 0,
- lastIndex,
- 0f,
- top / 2f + textOffset,
- textPaint
-)
+drawText(
+ textLayoutResult = textMeasurer.measure(
+ textual content = annotatedString.subSequence(0, lastIndex)
+ ),
+ topLeft = Offset(0f, baseOffset),
+ shade = textColor
+)
gesture
On Android, BackHandler
at all times out there – it handles again or again button press gestures relying on the navigation mode out there for the machine. However this strategy will not work with Compose Multiplatform, as a result of BackHandler
is a part of the Android supply code. Use . as a substitute anticipate enjoyable
:
@Composable
anticipate enjoyable BackHandler(isEnabled: Boolean, onBack: ()-> Unit)
//Android implementation
@Composable
precise enjoyable BackHandler(isEnabled: Boolean, onBack: () -> Unit) {
BackHandler(isEnabled, onBack)
}
There are totally different approaches that may be recommended to attain the specified end in iOS. For instance, you possibly can write your individual again gesture in Compose, or if in case you have a number of screens in your app you possibly can wrap every display in a separate UIViewController and use native iOS navigation with the included UINavigationController default gestures.
We opted for an iOS-side gesture-handling implementation that does not wrap separate screens of their respective controllers (because the transition between views in our app is personalized). so many). This can be a good demonstration of easy methods to hyperlink these two languages. To start with, we add a local iOS SwipeGestureViewController for gesture detection and a handler for gesture occasions. Full iOS implementation you possibly can see This
struct SwipeGestureViewController: UIViewControllerRepresentable {
var onSwipe: () -> Void
func makeUIViewController(context: Context) -> UIViewController {
let viewController = Main_iosKt.MainViewController()
let containerController = ContainerViewController(youngster: viewController) {
context.coordinator.startPoint = $0
}
let swipeGestureRecognizer = UISwipeGestureRecognizer(
goal:
context.coordinator, motion: #selector(Coordinator.handleSwipe)
)
swipeGestureRecognizer.route = .proper
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
}
}
}
Then we create a corresponding perform in the principle.ios.kt file:
enjoyable onBackGesture() {
retailer.ship(Motion.OnBackPressed)
}
We are able to name this perform in Swift like this:
public func onBackGesture() {
Main_iosKt.onBackGesture()
}
We implement a retailer that collects actions.
interface Retailer {
enjoyable ship(motion: Motion)
val occasions: SharedFlow
}
enjoyable CoroutineScope.createStore(): Retailer {
val occasions = MutableSharedFlow()
return object : Retailer {
override enjoyable ship(motion: Motion) {
launch {
occasions.emit(motion)
}
}
override val occasions: SharedFlow = occasions.asSharedFlow()
}
}
This retailer accumulates actions utilizing retailer.occasions.acquire
:
@Composable
precise enjoyable BackHandler(isEnabled: Boolean, onBack: () -> Unit) {
LaunchedEffect(isEnabled) {
retailer.occasions.acquire {
if(isEnabled) {
onBack()
}
}
}
}
This helps bridge the variations in gesture dealing with throughout the 2 platforms and makes iOS apps pure and intuitive to navigate backwards and forwards.
Small mistake
In some circumstances, chances are you’ll encounter minor points like this: on the iOS platform, an merchandise that scrolls up is seen when touched. You possibly can examine the anticipated conduct (Android) with the defective iOS conduct under:
It occurred as a result ofModifier.clickable
causes an merchandise to achieve focus when it’s touched, thereby triggering the “bringIntoView” scrolling mechanism. The main target administration is totally different on Android and iOS, which causes this totally different conduct. We fastened this by including a .focusProperties { canFocus = false }
modifier for the merchandise.
Last
Compose Multiplatform is the subsequent stage in Multiplatform growth for Kotlin language after KMM. This know-how provides much more alternatives for code sharing — not simply enterprise logic however consumer interface parts as nicely. Whereas it is attainable to mix Compose and SwiftUI in your cross-platform app, this does not appear very easy for the time being.
You must take into consideration whether or not your software has enterprise logic, consumer interface, or purposeful capabilities that might profit from sharing code throughout a number of platforms. In case your software requires a whole lot of platform-specific options, KMM and Compose Multiplatform will not be your best option. The repo comprises the complete implementation. You can even test the present library to be extra conscious of present KMM capabilities.
For us, we’re very impressed and assume that Compose Multiplatform can be utilized in our actual tasks, as soon as the secure model is launched. It’s best suited to consumer interface-heavy purposes with out many hardware-specific options. It might be a viable various for each Flutter and native growth, however time will inform. In the meantime, we’ll proceed to concentrate on native growth — test it out iOS And Android posts!