Blog Infos
Author
Published
Topics
, , ,
Published

In the world of mobile programming – after Symbian, Java ME and Windows Phone -, “native” was the adjective used to categorize an application that was made for Android using Java and for iOS using Objective-C. Nowadays, its definition has become the subject of debate with the emergence of cross-platform technologies. To further complicate matters, the concept of “native” has begun to extend beyond its technical definition and has started to encompass considerations related to user experience and interface, developer experience, and how frameworks are built.

In this text, I seek to share my reflection on what means to be native.

Design Systems

In the early versions of Android (pre-4.0), the user interface was more basic and utilitarian, with a focus on functionality rather than aesthetics. Designs varied across different versions and device manufacturers, leading to inconsistency in the user experience. Android’s UI was more fragmented and lacked a standardised design language.

Google introduced Holo design system with Android 4.0 in 2011. It featured a more skeuomorphic design, with elements like drop shadows, gradients, and bevels, which aimed to mimic real-world objects. However, as design trends evolved, Google introduced Material Design with Android 5.0 in 2014. Material Design brought a more modern, flat design language with bold colours, responsive animations, and a focus on tactile surfaces and realistic lighting. It was designed to create a unified experience across different devices and screen sizes.

iOS 6 and earlier versions also featured heavy use of skeuomorphic design elements, such as the faux leather texture in the Calendar app, the realistic-looking bookshelves in iBooks, and the design of the Notes app, which resembled a yellow legal pad. With the release of iOS 7 in 2013, Apple underwent a significant design overhaul which introduced a flat design language, abandoning many of the skeuomorphic elements in favor of clean, minimalist design principles. This new design language featured simpler iconography, thinner fonts, and a more vibrant colour palette.

Around that time, it was also quite common for design to be created with one platform in mind and then poorly adapted to the other, often resulting in a mere imitation rather than a tailored experience. Respect for the end user of the platform was not given as much consideration. Both development and design for mobile were still in their infancy during that period.

Q: Can we rely on design systems or user experience to classify an application as native? Were Android developers creating native apps when they had to mimic the design and UX of iOS apps?

Mimicking another platform’s design or creating a disruptive user interface doesn’t necessarily make an app non-native, as long as it’s implemented using the platform-specific Application Programming Interfaces (APIs), of the targeted platform.

Platform APIs

APIs are sets of rules and tools that allow different software applications to communicate with each other. However, whether an API is considered native depends on the context of the platform it serves.

For example, in the realm of web development, JavaScript is considered native because the web platform is predominantly defined in terms of the JavaScript call stack and memory management. This means that JavaScript is the primary language used to interact with web browsers and web-based applications.

Similarly, in the context of Android development, the Java Virtual Machine (JVM) is considered native to the Android platform according to the Android API specifications. In the iOS ecosystem, Swift and Objective-C are considered native languages because they conform to the specifications of the iOS platform’s API. These APIs define how applications interact with the iOS operating system, hardware components, and other software services.

If we extend our perspective to consider the CPU as the primary platform, the ARM architecture emerges as native to the CPU, for example. This means that the CPU’s functionalities and operations are inherently tied to the ARM architecture, which is optimised to work seamlessly with it.

Q: Can we agree that “native” is relative?

Yes, there’s no single definition of “native”. Whether an API is considered native it will depend on the context of the platform it serves. Modern mobile frameworks are built to help developers create applications for mobile platforms.

Mobile Frameworks

React Native is an open-source framework developed by Meta Platforms in 2015. It enables developers to use the JavaScript language and React framework to create mobile apps.

Flutter is an open-source framework developed by Google, in 2015, also capable of developing mobile apps and utilizes the Dart programming language.

Q: Are these frameworks “native”?

Simply put, Flutter’s engine orchestrates the rendering of Dart-created Widgets onto a Skia (or Impeller) Canvas sent to the platform.

Like React Native, Dart comes with its own virtual machines, called the Dart VM, which are optimised for running Dart code efficiently. In the AOT mode Dart VM only supports loading and executing precompiled machine code and, as Vyacheslav Egorov explains it, even precompiled machine code still needs VM to execute, as the VM provides the necessary runtime environment.

Similarly, it diverges from our definition of native code.

Q: If React Native and Flutter by our definition do not create “native” apps, how about Kotlin?

Kotlin inherent interoperability eliminates the need for a bridge layer or virtual machine, enabling it to seamlessly share objects across platforms. As a result, Kotlin aligns with our criterion for native code, showcasing its native behaviour across diverse platforms such as Android, iOS, Web, and beyond.

Kotlin native behaviour

Unlike Dart and JavaScript, Kotlin stands out with its remarkable ability to directly interact with Java (Kotlin/JVM), Swift (Kotlin/Native), JavaScript (Kotlin/JS), and WebAssembly (Kotlin/Wasm) thanks to its compiler backends’ architectures.

By leveraging these compiler backends, Kotlin Multiplatform (KMP) projects can generate native code for each targeted platform, enabling shared Kotlin code to execute natively without the overhead of a virtual machine or bridge layer. This approach provides developers with the flexibility to write code once and deploy it on multiple platforms while maintaining native performance and behaviour.

Let’s understand it in the mobile realm.

The Kotlin/JVM compiler translates Kotlin code into Java bytecode, allowing it to run on the Java Virtual Machine. Throughout this process, the Kotlin compiler leverages various optimisation techniques to generate efficient bytecode and optimise the performance of the resulting JVM executable. Additionally, it ensures interoperability with existing Java code, allowing Kotlin and Java to seamlessly work together within the same project.

Kotlin/Native

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

Making it possible to be used in Kotlin code:

import kotlinx.cinterop.convert
import kotlinx.datetime.LocalDate
import platform.Foundation.NSCalendar
import platform.Foundation.NSDate
import platform.Foundation.NSDateComponents
import platform.Foundation.NSDateFormatter
import platform.Foundation.NSLocale
import platform.Foundation.currentLocale
//iosMain
internal actual class Platform actual constructor() {
actual fun formatDateTimeWithLocale(date: LocalDate, pattern: String): String {
return NSDateFormatter()
.apply {
dateFormat = pattern
locale = NSLocale.currentLocale
}
.stringFromDate(date.toNsDate() ?: throw IllegalStateException("Failed to convert LocalDate $date to NSDate"))
}
private fun LocalDate.toNsDate(): NSDate? {
val components = NSDateComponents()
components.year = this.year.convert()
components.month = this.monthNumber.convert()
components.day = this.dayOfMonth.convert()
return NSCalendar.currentCalendar.dateFromComponents(components)
}
}
view raw Platform.kt hosted with ❤ by GitHub
//commonMain
internal expect class Platform() {
fun formatDateTimeWithLocale(date: LocalDate, pattern: String): String
}
//will print for example 09 Março, 09 March, 3月9日...
println(Platform().formatDateTimeWithLocale(LocalDate(2024, 3, 9), "dd MMMM"))
view raw Platform.kt hosted with ❤ by GitHub

Q: Kotlin modules can integrate into Swift/Objective-C code when compiled into a framework?

Let’s put it to the test: we’ll take a kotlinx.LocalDate sent from the imported SharedModule framework, convert it to foundation.Date and format it according to the user’s Locale within our iOS project using Swift.

//commonMain
data class SharedObject(val currentDate: LocalDate)
//iosMain
actual fun platformModule() = module {
factory { SharedObject(LocalDate(2024, 3, 9)) }
}
object SharedObjectsProvider : KoinComponent {
fun sharedObject() = get<SharedObject>()
}
view raw SharedObject.kt hosted with ❤ by GitHub

Since Kotlin/Native compiles to Objective-C, it generates Objective-C headers for frameworks:

/*
* Code was simplified to facilitate interpretation
*/
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("SharedObject")))
@interface SharedModuleSharedObject : SharedModuleBase
- (instancetype)initWithCurrentDate:(SharedModuleKotlinx_datetimeLocalDate *)currentDate __attribute__((swift_name("init(currentDate:)"))) __attribute__((objc_designated_initializer));
...
@property (readonly) SharedModuleKotlinx_datetimeLocalDate *currentDate __attribute__((swift_name("currentDate")));
@end
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Kotlinx_datetimeLocalDate")))
@interface SharedModuleKotlinx_datetimeLocalDate : SharedModuleBase <SharedModuleKotlinComparable>
...
@property (readonly) int32_t dayOfMonth __attribute__((swift_name("dayOfMonth")));
@property (readonly) int32_t monthNumber __attribute__((swift_name("monthNumber")));
@property (readonly) int32_t year __attribute__((swift_name("year")));
@end
view raw SharedModule.h hosted with ❤ by GitHub

By importing our SharedModule into Swift code, we gain access to it:

import Foundation
import SharedModule
func formatDateTimeWithLocale(date: Kotlinx_datetimeLocalDate, pattern: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = pattern
formatter.locale = Locale.current
return formatter.string(from: date.toDate())
}
extension Kotlinx_datetimeLocalDate {
func toDate() -> Date {
var components = DateComponents()
components.year = Int(self.year)
components.month = Int(self.monthNumber)
components.day = Int(self.dayOfMonth)
return Calendar.current.date(from: components)!
}
}
//will print for example 09 Março, 09 March, 3月9日...
let sharedObject = SharedObjectsProvider().sharedObject()
print(formatDateTimeWithLocale(date: sharedObject.currentDate, pattern: "dd MMMM"))
view raw View.swift hosted with ❤ by GitHub

And here we have it! Kotlin modules can integrate into Swift/Objective-C.

Q: This brings us to the final test: Are the memory space and call stack shared, allowing us to meet our technical criterion for true native functionality?

Let’s put it to the test: we’ll utilize the WhosNext project to print the ViewModel created in shared code, then compare its memory address between the shared module and the iOS app module to verify their consistency.

//iosMain
actual fun platformModule() = module {
factory { TimerViewModel() }
}
object ViewModels : KoinComponent {
fun timerViewModel() = get<TimerViewModel>().also {
println("Kotlin object ${it.objcPtr()}")
}
}
import SwiftUI
import WhosNextShared
struct TimerScreen: View {
@StateViewModel private var viewModel = ViewModels().timerViewModel()
var body: some View {
Timer(
...
).onAppear() {
print("Swift object \(Unmanaged.passUnretained(viewModel).toOpaque())")
}
}
}

And the result is:

Kotlin object 0x600003794a80
Swift object 0x0000600003794a80

Kotlin object 1927122451
JVM object 1927122451

Kotlin object TimerViewModel@948473487
Wasm object TimerViewModel@948473487

Success for all platforms!

Kotlin’s superpower lies in its ability to interact with all four platforms natively, opening the doors for two-way interoperability. This capability extends to Compose Multiplatform UI framework (CMP), as it embraces the same philosophy within the Compose Compiler:

Q: Kotlin is clearly the “native winner”, but, does it matter to be native?

Addressing this question requires consideration of various factors, such as adoption risk (all-in vs. incremental), native stack teams vs. non-native stack teams, team’s availability, client preferences and market demands, developer experience, performance, and more.

That alone would be the subject for another article; perhaps one day I’ll return with my reflections.

Closing remarks

Design systems and user interfaces don’t determine whether an application is native or non-native. The term “native” lacks a singular definition; whether an API is considered native depends on the context of the platform it serves. Defining “native” in terms of its ability to share memory space and call stack, establishes a technical criterion for native functionality. In this context, modern mobile frameworks don’t meet this criterion. Kotlin, on the other hand, stands out as native due to its compiler architectures (KMP & CMP) that unlocks two-way interoperability and meets our shared memory space criterion.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
With JCenter sunsetted, distributing public Kotlin Multiplatform libraries now often relies on Maven Central…
READ MORE
blog
With Compose Multiplatform 1.6, Jetbrains finally provides an official solution to declare string resources…
READ MORE
blog
One of the features of Android Studio is the Layout Inspector, which lets you…
READ MORE
blog
There are already so many app-level architecture and presentation layer patterns (MVC/MVP/MVVM/MVI/MVwhatever) that exist…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu