You are on page 1of 75

Build multi-device and

Isf
ag
an
te ng
multi-screen app with Kotlin
and Compose Multiplatform
Muh Isfhani Ghiath • DevFest Surabaya 2023
@isfaaghyth with.isfa.dev

Muh Isfhani Ghiath (isfa)


Senior Software Engineer - Android, Tokopedia
Google Developer Experts for Android
Tech Lead, kepul.id
Agenda

Outline
Overview
Background
Walkthrough
Study Case
Multiplatform options
Multiplatform Alternatives
Hybrid a really native compiled!
KMM and Flutter Comparison
…. React Native?
Share the logic of iOS and Android
apps while keeping the UX native.
Kotlin Multiplatform Mobile is an
SDK for iOS and Android app
development. It offers all the
combined benefits of creating
cross-platform and native apps.

“Shared business, Native UI” -CashApp


Supported Platform: Previous

● Android applications and ● Apple iOS on ARM64 (iPhone 5s and


libraries newer)
● Android NDK on ARM32 ● Apple watchOS on ARM64 (Apple Watch
and ARM64 platforms Series 4 and newer)
● ARM32 (earlier models) platforms
● Desktop simulators on both Intel-based
and Apple Silicon platforms
Supported Platform: Current
Things can be shared

● Data Layer

● Business Logic (Domain) Layer

● Presentation Logic such as


Validation, Formulation etc

● Utilities or anything similar logic


among platform
Benefits
Android and iOS nowadays
Code Sharing across platforms
A Platform-specific APIs
Stable! strict compatibility guarantees ✅
kotl.in/kmp-stability

Compiler Language Library


Support features APIs

Build IDE
tooling support
Previous: Configuration targets

kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()

sourceSets {
val commonMain by getting
val iosMain by getting {
dependsOn(commonMain)
}
}
}
Stable: Configuration targets

kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()

sourceSets {
commonMain.dependencies {}
androidMain.dependencies {}
}
}
kotl.in/kmp-portal
Compose
Multiplatform! 🤩
jb.gg/compose
Compose Multiplatform
Built on Jetpack Compose
Build Multiplatform UIs
using APIs you already know.

Jetpack Compose
Compose Multiplatform
APIs APIs
Build Multiplatform UIs
using APIs you already know.

Compose
Multiplatform
APIs
Behind Compose Multiplatform
Behind Compose Multiplatform
Behind Compose Multiplatform
Skiko
Kotlin MPP bindings to Skia

Skiko (short for Skia for Kotlin) is the graphical library exposing
significant part of Skia library APIs to Kotlin, along with the
gluing code for rendering context.
Compose for iOS Alpha 😍
Available in Compose Multiplatform 1.5.10
Latest Updates (1.5.10) 🤩

* Dialogs, popups
* Window Insets
* 120Hz refresh rate
* Natural scrolling for iOS
* Stabilized test framework for desktop
kmp.jetbrains.com
Getting Started

commonMain

@Composable fun SampleApp() {


Text(text=”bro, piye kabare?”)
}
Getting Started

iosMain

@Composable fun MainViewController(): UIViewController {


return ComposeUIViewController { SampleApp() }
}
Getting Started

ComposeView.swift

struct ComposeView: UIViewControllerRepresentable {


func makeUiViewController(context: Context) -> UIViewController {
Main_iosKt.MainViewController()
}
}

var body: some View { ComposeView() }


Let’s create a simple app! 🤩
First, let’s setup our stacks

Ktor KMPNativeCoroutines SKIE-TouchLab Kermit-Logger

Spotify-MobiusKt Kotlinx.Serialization Turbine-Test Compose JB

Decompose qdsfdhvh-image Material3


Mobius Sneak Peek
Mobius Sneak Peek

Evaluation Strategies

In mobile architectures, we have bidirectional and unidirectional data


flow.

But, bidirectional data flow architectures don’t suit modern days


requirements (anymore)
Mobius Sneak Peek

Why does bidirectional not suit us?

App driven by the data

View changes has explicitly changes whenever data is transformed

Each component holds part of the domain state


Mobius Sneak Peek

... and why unidirectional?

Humans express themselves as per the senses they perceive.

The app is treated as another being that senses the inputs and expresses its
output.

The app listens to the user actions and modifies the data it has

The changes in the data are reflected through UI as an expression of the app
Mobius Sneak Peek

Objectives

Provides State, Event, and Data holder

Reactive in nature

Strong separation of concerns

Explicit about side-effects

Simpler code nor Unficiation


Mobius Sneak Peek

Criteria

Platform Agnostic

Scalability

Testability

Immutable States

Community Support
Comparison

Swift TCA
Comparison

Mobius
Comparison

Mobius v TCA

Architecture Name Loop Externals State Effect Event

Mobius Update() Effect Model Effect Event


Handler

TCA Reducer() Environment State Effect Action

Both architectures have the same way, each arch uses the Redux-like or MVI-like approaches to

maintain the data changes using State and Event as well as contain the side effects handlers.
Testing Benefit

Spec framework!

val model = Model(email = "isfa@devfest.com", pass = "rahasia", canLogin = true,


loggingIn = false)

spec.given(model)
.whenEvent(LoginRequested)
.then(assertThatNext(
hasModel(model.copy(loggingIn = true))
hasEffects(AttemptLogin("isfa@devfest.com", "rahasia"))
))
Project walkthrough
Walkthrough

Our sample project

Inspired: xxfast/NYTimes-KMP
Walkthrough

commonMain
Walkthrough

Router

@Parcelize
sealed class NavRouter : Parcelable {

data object Home : NavRouter()


data class Detail(val title: String) : NavRouter()
}
Walkthrough

Root level

@Composable
fun DevFestApp() {
val router: Router<NavRouter> = rememberRouter(NavRouter::class) {
listOf(NavRouter.Home)
}

RoutedContent(router = router) { screen ->


when (screen) {
is NavRouter.Home -> HomeScreen { router.push(NavRouter.Detail()) }
is NavRouter.Detail -> DetailScreen(screen.title) { router.pop() }
}
}
}
Walkthrough

Dynamic Grid cells

LazyVerticalGrid(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(24.dp),
columns = GridCells.Adaptive(minSize = 248.dp)
) {
items(items = chapters, key = { it.id }) {
ChapterCard(
chapter = it,
onClick = {
onCardClicked(it)
}
)
}
}
Walkthrough

Detect the Window Size

expect value class WindowWidthSizeClass private constructor(


private val value: Int
)

expect object WindowWidthSizeClasses {


val Compact: WindowWidthSizeClass
val Medium: WindowWidthSizeClass
val Expanded: WindowWidthSizeClass
}
Walkthrough

Detect the Window Size

import android.app.Activity
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable

@Composable
fun calculateWindowSizeClass(activity: Activity): WindowSizeClass
= calculateWindowSizeClass(activity)
Walkthrough

Detect the Window Size

@Composable
fun calculateWindowSizeClass(controller: UIViewController): WindowSizeClass {
val density = LocalDensity.current

val rect: Rect = controller.view.bounds.useContents {


val x = origin.x.toFloat()
val y = origin.y.toFloat()
val width = size.width.toFloat() / 2
val height = size.height.toFloat() / 2
Rect(x - width, y - height, x + width, y + height)
}

val size = with(density) { rect.size.toDpSize() }


return CommonWindowSizeClass.calculateFromSize(size)
}
Walkthrough

Custom Side-Panel Scaffold


Box(
modifier = modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxWidth(fraction)
.align(Alignment.CenterStart),
) {
body()
}

AnimatedVisibility(
visible = panelVisibility,
enter = animationSpec.enter,
exit = animationSpec.exit,
modifier = Modifier
.fillMaxWidth(1f - split)
.align(Alignment.CenterEnd)
) {
panel()
}
Walkthrough

Custom Side-Panel Scaffold

class TwoPanelScaffoldAnimationSpec(
val expand: AnimationSpec<Float>,
val enter: EnterTransition,
val exit: ExitTransition,
val finishedListener: ((Float) -> Unit)?,
)
Walkthrough

Custom Scaffold Usage

var router: NavRouter? by rememberSaveable { mutableStateOf(null) }


val details: NavRouter.Detail? = router as? NavRouter.Detail
val windowSizeClass: WindowSizeClass = LocalWindowSizeClass.current
var showSidePanel: Boolean by rememberSaveable { mutableStateOf(details != null) }

LaunchedEffect(windowSizeClass) {
router = router.takeIf {
windowSizeClass.widthSizeClass != WindowWidthSizeClasses.Compact
}
showSidePanel = router != null
}
Walkthrough

Custom Scaffold Usage

TwoPanelScaffold(
panelVisibility = showSidePanel,
animationSpec = TwoPanelScaffoldAnimationSpec(
finishedListener = { fraction -> if (fraction == 1f) router = null }
),
body = {
HomeScreenView()
},
panel = {
Surface(tonalElevation = 1.dp) {
DetailScreen()
}
}
)
Walkthrough
TwoPanelScaffold(
Custom Scaffold Usage
panelVisibility = showSidePanel,
animationSpec = TwoPanelScaffoldAnimationSpec(
finishedListener = { fraction -> if (fraction == 1f) router = null }
),
body = {
HomeScreenView(
state = state,
onCardClicked = {
val detailSelection = NavRouter.Detail(it)

if (windowSizeClass.widthSizeClass == WindowWidthSizeClasses.Compact) {
onCardClicked(it)
return@HomeScreenView
}

router = detailSelection
showSidePanel = true
}
)
},
panel = {
Surface(tonalElevation = 1.dp) {
if (details != null) {
DetailScreen(
github.com/isfaaghyth/devfest
Hands-on!
Still work in Progress!

Scroll Physics Navigation Contrast Settings Text to Voice

Gestures Transition Context Menu Accessibility


They are using KMP!
Scan here for Slides!
Matur Nuwun!
@isfaaghyth with.isfa.dev

You might also like