An iOS Engineer learns about Android’s Jetpack Compose and loves it.

Dimitri James Tsiflitzis
7 min readSep 2, 2022

--

Recently I became involved with an Android app, along with an iOS app for a product we were building. Through this, I gained an incentive to learn Jetpack Compose which was the UI engine the Android app leveraged.

The experience ended up being very positive for me; enough to share my thoughts so that other iOS developers can learn, take a peek over the fence, and why not evolve into fully-fledged Android developers while they’re at it.

Choosing this two-platform path comes with benefits; the obvious one is that if you are versed in both the iOS and Android tech stacks you access superior cooperation among developers working on the same features. Through this, knowledge gaps are bridged, collaboration is much easier, and the result is the production of better apps which, we can agree, is the end goal.

The end result is the production of better apps.

Noteworthy differences between Compose and SwiftUI

Jetpack Compose is supported on Android from SDK 21 upwards. That’s Android 5.0-Lollipop which was released in late 2014. SwiftUI on the other hand has a minimum iOS deployment target of 13.0 which was released in late 2019. Let’s be realistic though: iOS 14.0 (Released in late 2020) is the real minimum since SwiftUI was experimental enough before that (think Swift 1.0) to not make it suitable for production apps. Compose has the advantage here.

Actually, let’s digress for a minute. At this point in time, iOS 16 is almost out. SwiftUI came out 3 major versions ago with 13.0. If you have been holding off using SwiftUI in production now is the time to reconsider that stance. It’s a perfect time to adopt SwiftUI.

Jetpack Compose uses a declarative syntax and, just like in SwiftUI, everything is done in code.

This next difference is a matter of personal taste, but SwiftUI has perhaps better syntax in my humble opinion. SwiftUI achieved this by actual language additions such as view builders which give its code its streamlined DSL feel. You will be able to judge that for yourself of course in the code section below. Both frameworks use a declarative syntax and everything is done in code. That means your UI doesn’t depend on any external non-code files such as XML layout files or storyboards, or xibs.

Jetpack is open source with a frequent release cadence. SwiftUI releases new versions with annual iOS updates only and is developed in-house by its owner.

Let’s code

The examples below are but a small subset of the full range of what is available for your UI development. They cover the common starting points when picking up a new UI framework though.

Hello World

Writing a compose function is as simple as writing a Kotlin function annotated with @Composable.

@Composable
fun HelloWorld() {
Text(“Hello World!”)
}

And this is how you would use it in an app:

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

Its SwiftUI equivalent body looks like this:

var body: some View {
Text(“Hello World!”)
}

Its view would look like this:

struct HelloView : View {
var body: some View {
Text(“Hello World!”)
}
}

A key distinction here is their structure. Compose uses functions to render its UI whereas SwiftUI uses structs with a view body being a property of that. The @Composable annotation is used to generate the required boilerplate code. In the case of SwiftUI, all the necessary code is already there.

Layout

In SwiftUI three common layout components that allow you to arrange subviews horizontally, vertically, or overlaying them along the z-axis are:

  • VStack which has Column as its Compose equivalent
  • HStack which has Row as its Compose equivalent
  • ZStack which has Stack as its Compose equivalent

The above statements alone piqued my interest initially and made me interested in learning Jetpack Compose (and the required Android infrastructure to support that i.e Kotlin). There was practically a one-to-one relationship there. In addition, if you have worked with Flutter at some point, even the names match with the functionality making them identical (with Compose that is). I would go as far as to say that if you are an experienced Flutter developer you are more than halfway there+ with the layout system in Jetpack Compose!

Let’s try and arrange some views in Compose while also showing the SwiftUI equivalent

Compose

@Composable
fun HelloWorld() {
Column {
Text(“Hello”)
Text(“World”)
}

}

SwiftUI

var body: some View {
VStack {
Text(“Hello”)
Text(“World”)
}

}

The above will show the words “hello” and “world” on top of each other in the order they are declared as in the code.

If we wanted to show these Text widgets next to each other we would use the following code:

Compose

@Composable
fun HelloWorld() {
Row {
Text(“Hello”)
Text(“World”)
}

}

SwiftUI

var body: some View {
HStack {
Text(“Hello”)
Text(“World”)
}

}

All these could also be combined with themselves and eachother, along with other widgets that support scrolling for instance, to form beautiful and complex layouts. In both frameworks, a valid layout is always produced. You will never run into UIKits ambiguous layouts that occur when the system of constraints has two or more valid solutions.

State

Let’s use Compose’s mechanism to increment a value using a button and display that in a text widget.

@Composable fun Counter() {    val value = remember {
mutableStateOf(0)
}
Column {
Text("${value.value}")
Button(
onClick = { value.value++ }
)
{
Text(“Increment me”)
}
}
}

This is above using SwiftUI:

struct Counter: View {

@State var value: Int = 0
var body: some View { VStack { Text(“\(value)”) Button(action: {
self.count += 1
}) {
Text(“Increment”)
}
}
}
}

Note: For our Android colleagues the Swift language doesn’t have a ++ operator more here https://github.com/apple/swift-evolution/blob/master/proposals/0004-remove-pre-post-inc-decrement.md

Composable functions can use the remember API to store an object in memory. A value computed by remember survives across recompositions just like @State objects do, and any changes to value will trigger a recomposition. remember can be used to store both mutable and immutable objects. Recomposition is the equivalent-ish term to a redraw in SwiftUI.

Modifying

Modifying views, while being similar on both systems, has a slightly different syntax. Compose styling is done by passing the desired layout as constructor arguments. In SwiftUI, while constructor arguments exist, the majority of styling is carried out using View Modifiers, using the dot syntax, that adjusts the original view by producing a different version of it.

Let’s add some padding to view inside a Compose function:

Column(
modifier = Modifier
.size(width = 400.dp, height = 100.dp)
.padding(10.dp)
.fillMaxWidth()
.clickable(onClick = onClickMethod)
) {
Image(/*…*/)
Column { /*…*/ }
Text(/*…*/)

}

That dot syntax of Modifier will look familiar to SwiftUI developers. The modifier parameter is available on all Compose views e.g. for Text just above. For Text in particular, in addition to the modifier parameter there exist additional parameters for properties such as font or color. Like so:

@Composable
fun HugeText() {
Text(“Hello World”, fontSize = 30.sp)
}

Just like with SwiftUI view modifiers, the order of execution matters in modifier parameters in Compose also. For instance, in the above example, the onClickMethod method would apply to the expanded area that padding creates. If it was called before .padding() it would only apply to the non-padded area just like in SwiftUI 😉

Alignment

Compose’s Column supports two positional arguments. The first one is Vertical Arrangement which specifies the vertical arrangement of the layout’s children according to the value passed. The full range of values along with their appearance is illustrated in the image below which is found here. A picture is worth a thousand words has never rung truer.

https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Arrangement

Horizontal Alignment is used to define the horizontal alignment of a Column layout inside its parent layout.

Compose’s Row is the reverse of a Column. It has two arguments which are Vertical Alignment and horizontal Arrangement. The vertical alignment specifies the position of a row relative to its parent. Horizontal Arrangement defines the horizontal arrangement of the rows of children. The image below illustrates all possible arrangements.

https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Arrangement

I like the alignment methodology of Compose. Not as powerful as SwiftUI’s alignment guide but definitely more powerful that the alignment parameter of SwiftUIs stacks. I consider composes approach as optimal and I prefer it.

Tools

Both frameworks provide preview mechanisms via their IDE’s Android Studio and Xcode. In Android Studio you can preview a composable function right there in the IDE; you just need to add the @Preview annotation. Previews work great because they remove the need to launch an emulator each time you make a change. That is just about it for Android Studios previews though.

In both cases previews are pretty buggy and behave in strange ways at random times. You get used to them though 😊

In Xcode and SwiftUI, in additiona to as static preview, you can also interact with your previews, view animations, modify states, load remote data, and more. In both cases however previews are pretty buggy and behave in strange ways. You figure out your workarounds in the long run though.

Deinit

It’s so refreshing that at the time of this writing the two major mobile platforms have provided a fully developed and mature declarative UI.

Both of these solutions are a definite upgrade on their respective imperative predecessors; they are both declarative, state-driven, and reactive. Kotlin is easy to pick up for Swift developers too. I loved the when keyword and its diversity in Kotlin. If there was ever a time to seriously consider becoming a horizontal mobile developer this is it. The learning curves have accelerated and since both frameworks share similar principles they’re easier to learn. It’s also a great way to future-proof your skillset because let’s face it, the popularity of either dominant mobile platform is not guaranteed in perpetuity.

--

--

Dimitri James Tsiflitzis

Developer, scrum enthusiast, @sequeapp building productivity apps; formerly @nimber, @peopleperhour, @taxibeat; game dev @sprimp and orgniser @cocoaheadsgr