Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Building Shared UIs across Platforms with Compose
Search
Mohit S
September 16, 2023
Programming
730
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Building Shared UIs across Platforms with Compose
Mohit S
September 16, 2023
More Decks by Mohit S
See All by Mohit S
Guide to Improving Compose Performance
heyitsmohit
0
340
Building Multiplatform Apps with Compose
heyitsmohit
2
640
Building StateFlows with Jetpack Compose
heyitsmohit
6
2k
Building Android Testing Infrastructure
heyitsmohit
1
650
Migrating to Kotlin State & Shared Flows
heyitsmohit
1
900
Using Square Workflow for Android & iOS
heyitsmohit
1
550
Building Android Infrastructure Teams at Scale
heyitsmohit
3
440
Strategies for Migrating to Jetpack Compose
heyitsmohit
2
680
Challenges of Building Kotlin Multiplatform Libraries
heyitsmohit
1
530
Other Decks in Programming
See All in Programming
GDG Korea Android: 2026 I/O Extended ~ What's new in Android development tools
pluu
0
230
使いながら育てる Claude Code — 開発フローの1コマンド化 × 繰り返し指摘の自動仕組み化
shiki_kakaku
0
1.9k
Pythonの実行はどこまで賢くなったのか? CPythonとPyPyから見る最適化のしくみ
curekoshimizu
0
190
属人化した知識を、 AIが辿れる地図にする
pkshadeck
PRO
1
200
自動化したのに回らないテスト運用の壁ーAI時代の品質責任と生産性
mfunaki
1
150
AIが無かった頃の素敵な出会いの話
codmoninc
1
480
仕様駆動開発の消費期限
watany
20
8.8k
複数の Claude Code が"放置"されてしまう問題をCLI ダッシュボードを自作して解決した話
sumihiro3
1
710
まだ間に合う!今年の夏こそSchemeのマクロ展開器を完全理解!
omasanori
0
160
AIと壁打ちしながら進めるコスト管理
fufuhu
1
1.1k
バグを直したら useEffect が消えた
colorful12
3
660
SlackアプリとLambdaの 連携を構築した話
pawn_4_s
1
130
Featured
See All Featured
We Are The Robots
honzajavorek
0
300
Producing Creativity
orderedlist
PRO
348
40k
Making Projects Easy
brettharned
120
6.7k
Prompt Engineering for Job Search
mfonobong
0
410
Context Engineering - Making Every Token Count
addyosmani
9
1.1k
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
390
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
2.1k
My Coaching Mixtape
mlcsv
0
250
The Curse of the Amulet
leimatthew05
2
14k
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
170
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
Why Our Code Smells
bkeepers
PRO
340
58k
Transcript
Mohit Sarveiya Building Shared UIs Across Platforms with Compose @heyitsmohit
Building Shared UIs Across Platforms with Compose • Setup &
Architecture
Building Shared UIs Across Platforms with Compose • Setup &
Architecture • Internals
Building Shared UIs Across Platforms with Compose • Setup &
Architecture • Internals • Interop with iOS
Android Kotlin/JVM iOS Swift/LLVM Web JS Desktop Kotlin/JVM
API Share Cache Business Logic Platforms
API Share Cache Business Logic UI Components Platforms
https: / / github.com/JetBrains/compose-multiplatform compose-multiplatform
Approaches • Share all UI components Components (Compose)
Approaches UI Components (Compose) • Share individual UI components UI
Components (SwiftUI)
Approaches UI Components (Compose) • Share individual UI components UI
Components (SwiftUI) Shared Components (Compose)
Approaches • Share individual UI components • Share all UI
components
Example • SwiftUI App • Display list of images
Example • SwiftUI App • Display list of images •
Details page
Goal • Display list of images (Compose) • Details page
(SwiftUI)
Goal ZStack { LazyVGrid( ... ) { ForEach(id: .id) {
item in Image(item.url) .renderingMode(.original) .resizable() .scaledToFill() } }.task { await repository.getImages() } }
UI Structure Shared Component NavigationView { ZStack { ComposeView() }
}.toolbar { ... }
https: / / github.com/JetBrains/compose-multiplatform-template compose-multiplatform
androidApp iOSApp shared Structure
shared src commonMain androidMain iOSMain Shared Module
shared src commonMain androidMain iOSMain build.gradle.kts Shared Module
plugins { kotlin("multiplatform") } val commonMain by getting { dependencies
{ implementation(compose.ui) implementation(compose.foundation) implementation(compose.material) implementation(compose.runtime) } } Shared Module
plugins { kotlin("multiplatform") } val commonMain by getting { dependencies
{ implementation(compose.ui) implementation(compose.foundation) implementation(compose.material) implementation(compose.runtime) } } Shared Module
UI Structure
UI Structure Shared Component NavigationView { ZStack { ComposeView() }
}.toolbar { ... }
UI Structure Compose View Images List ViewController AppTheme NavigationView {
ZStack { ComposeView() } }.toolbar { ... }
shared src commonMain androidMain iOSMain ImagesAppTheme.kt Shared Module
App Theme
App Theme val LightColorPalette = lightColors( ... ) val DarkColorPalette
= darkColors( ... ) @Composable fun ImagesAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColorPalette else LightColorPalette, content = content )
App Theme @Composable fun ImagesAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content:
@Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColorPalette else LightColorPalette, content = content ) }
App Theme @Composable fun ImagesAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content:
@Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColorPalette else LightColorPalette, content = content ) }
UI Structure Compose View Images List ViewController AppTheme NavigationView {
ZStack { ComposeView() } }.toolbar { ... }
UI Structure struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context)
-> UIViewController { let controller = Main_iosKt.MainiOS() return controller } }
UI Structure struct ComposeView: UIViewControllerRepresentable { func makeUIViewController( ...
) -> UIViewController { let controller = Main_iosKt.MainiOS() return controller } }
UI Structure struct ComposeView: UIViewControllerRepresentable { func makeUIViewController( ...
) -> UIViewController { let controller = ImagesList() return controller } }
UI Structure fun ImagesList(): UIViewController = ComposeUIViewController { ImagesAppCommon() }
UI Structure fun ImagesList(): UIViewController = ComposeUIViewController { ... }
Compose Architecture Compose Multiplatform Compose Multiplatform Core
https: / / github.com/JetBrains/compose-multiplatform-core compose-multiplatform
Multiplatform Core fun ComposeUIViewController( content: @Composable () -> Unit ):
UIViewController = ComposeWindow().apply { configuration = ComposeUIViewControllerConfiguration() .apply(configure) setContent(content) }
Multiplatform Core fun ComposeUIViewController( content: @Composable () -> Unit ):
UIViewController = ComposeWindow().apply { setContent(content) }
UI Structure Compose View Images List ViewController AppTheme NavigationView {
ZStack { ComposeView() } }.toolbar { ... }
UI Structure fun ImagesList(): UIViewController = ComposeUIViewController { ... }
UI Structure fun ImagesList(): UIViewController = ComposeUIViewController { AppTheme {
} } Text("Hello World")
UI Structure struct ContentView: View { var body: some
View { ZStack { ComposeView() ... } } } Hello World
UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:
View { var body: some View { ZStack { ComposeView() ... } } } UIWindowScene UIWindow ComposeWindow SkikkoUIView
UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:
View { var body: some View { ZStack { ComposeView() ... } } } UIWindowScene UIWindow ComposeWindow SkikkoUIView
UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:
View { var body: some View { ZStack { ComposeView() ... } } } UIWindowScene UIWindow ComposeWindow SkikkoUIView
https: / / github.com/JetBrains/skiko Skiko
Compose Multiplatform Architecture Skia Skiko Compose UIViewController UIKit SwiftUI
UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:
View { var body: some View { ZStack { ComposeView() ... } } } UIWindowScene UIWindow ComposeWindow SkikkoUIView
UI Structure class ComposeWindow : UIViewController { var layer: ComposeLayer
var content: @Composable () -> Unit override fun loadView() { ... } }
UI Structure fun ImagesList(): UIViewController = ComposeUIViewController { AppTheme {
} } Text("Hello World")
Compose Multiplatform Architecture Skia Skiko Compose UIViewController UIKit SwiftUI
UI Structure class ComposeWindow : UIViewController { override fun loadView()
{ val skiaLayer = createSkiaLayer() val skikoUIView = SkikoUIView(skiaLayer = skiaLayer).load() val rootView = UIView() rootView.addSubview(skikoUIView) } }
UI Structure class ComposeWindow : UIViewController { override fun loadView()
{ val skiaLayer = createSkiaLayer() val skikoUIView = SkikoUIView(skiaLayer = skiaLayer).load() val rootView = UIView() rootView.addSubview(skikoUIView) layer = ComposeLayer(layer = skiaLayer) layer.setContent( CompositionLocalProvider( ... ) { content() } } ) } }
UI Structure struct ContentView: View { var body: some
View { ZStack { ComposeView() ... } } } Hello World
Architecture
UI Structure struct ContentView: View { var body: some
View { ZStack { ComposeView() ... } } }
Architecture View Repo View Model SwiftUI ComposeView Shared
View Model Repository View Request Response UI State Event
https: / / github.com/cashapp/molecule Molecule
@Composable fun Presenter(): Model State Flow Compose Runtime Recomposition
@Composable fun Presenter(): Model State Flow Recomposition Monotomic Frame Clock
Molecule Muiltiplatform Support • Android (all versions) • JS (0.3.0
and newer) • JVM (0.3.0 and newer) • iOS (0.5.0-beta01 and newer) • MacOS (0.5.0-beta01 and newer)
https: / / github.com/icerockdev/moko-mvvm Moko
Architecture sealed class UiState { object Loading: UiState() data class
Success( val images: List<ImageData> ): UiState() data class Error( val errorMessage: String ): UiState() }
Architecture abstract class MoleculeViewModel <> : ViewModel() { }
Architecture abstract class MoleculeViewModel<Model, Event >: ViewModel() { }
Architecture abstract class MoleculeViewModel<Model, Event >: ViewModel() { }
Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(
viewModelScope.coroutineContext ) }
Architecture Frame Clock DisplayLinkClock iOS AndroidUiFrameClock Android
https: / / developer.apple.com/documentation/quartzcore/cadisplaylink CADisplayLink
Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(
viewModelScope.coroutineContext + DisplayLinkClock ) }
Architecture object DisplayLinkClock : MonotonicFrameClock { val displayLink: CADisplayLink =
val clock = BroadcastFrameClock { ... } override suspend fun withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R { return clock.withFrameNanos(onFrame) } }
Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(…)
val models: StateFlow<Model> by lazy(…) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { models(…) } } }
Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(…)
val models: StateFlow<Model> by lazy(…) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { models(…) } } }
Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(…)
val models: StateFlow<Model> by lazy(…) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { models(…) } } }
View Model View UI State Event
Architecture abstract class MoleculeViewModel: ViewModel() { val events = MutableSharedFlow<Event>(extraBufferCapacity
= 20) fun take(event: Event) { if (!events.tryEmit(event)) { error("Event buffer overflow.") } } }
Architecture abstract class MoleculeViewModel: ViewModel() { val events = MutableSharedFlow<Event>(extraBufferCapacity
= 20) fun take(event: Event) { if (!events.tryEmit(event)) { error("Event buffer overflow.") } } }
Architecture View Repo View Model SwiftUI ComposeView Shared
Architecture class ImagesViewModel: MoleculeViewModel() { }
Architecture class ImagesViewModel: MoleculeViewModel() { @Composable override fun models(events: Flow<Event>):
UiState { } }
Architecture @Composable override fun models(events: Flow<Event>): UiState { var uiState
by remember { mutableStateOf(UIState.Loading) } }
Architecture @Composable override fun models(events: Flow<Event>): UiState { var uiState
by remember { mutableStateOf(UIState.Loading) } LaunchedEffect(Unit) { } }
Architecture @Composable override fun models(events: Flow<Event>): UiState { var uiState
by remember { mutableStateOf(UIState.Loading) } LaunchedEffect(Unit) { val imagesList = imagesRepository.getImages() uiState = UIState.Success(imagesList) } }
Architecture @Composable override fun models(events: Flow<Event>): UiState { var uiState
by remember { mutableStateOf(UIState.Loading) } LaunchedEffect(Unit) { val imagesList = imagesRepository.getImages() uiState = UIState.Success(imagesList) } return uiState }
Architecture fun ImagesList(): UIViewController = ComposeUIViewController { AppTheme {
} }
Architecture ComposeUIViewController { AppTheme { val viewModel = getViewModel(…,
viewModelFactory { ImagesViewModel() }) } }
Architecture ComposeUIViewController { AppTheme { val viewModel = getViewModel(…)
val model by viewModel.models.collectAsState() } }
Architecture ComposeUIViewController { AppTheme { val viewModel = getViewModel(…)
val model by viewModel.models.collectAsState() ImagesList(model) } }
Architecture View Repo View Model SwiftUI ComposeView Shared
Architecture fun ImagesList(model: UiState) { Column { LazyVerticalGrid { items(images)
{ ... } } } }
https: / / github.com/Kamel-Media/Kamel Kamel
Architecture fun ImagesList(model: UiState) { Column { LazyVerticalGrid { items(images)
{ KamelImage( asyncPainterResource(image.path), contentScale = ContentScale.Crop, ) } }
UI Structure struct ContentView: View { var body: some
View { ZStack { ComposeView() ... } } }
Architecture View Repo View Model SwiftUI ComposeView Shared
iOS Interop
• Compose in SwiftUI Interop
• Compose in SwiftUI • SwiftUI in Compose Interop
Interop View in Compose
Interop SwiftUI View View in Compose
Interop Compose View SwiftUI View Provide
Interop shared src commonMain androidMain iOSMain App Screen
Interop fun AppScreen(createUIView: () -> UIView): UIViewController = ComposeUIViewController {
Column( horizontalAlignment = Alignment.CenterHorizontally ) { Text("How to use SwiftUI inside Compose") UIKitView( factory = createUIView, modifier = Modifier.size(300.dp).border(2.dp, Color.Blue), ) } }
Interop fun AppScreen(createUIView: () -> UIView): UIViewController = ComposeUIViewController {
Column( horizontalAlignment = Alignment.CenterHorizontally ) { Text("How to use SwiftUI inside Compose") UIKitView( factory = createUIView, modifier = Modifier.size(300.dp).border(2.dp, Color.Blue), ) } }
Interop fun AppScreen(createUIView: () -> UIView): UIViewController = ComposeUIViewController {
Column( horizontalAlignment = Alignment.CenterHorizontally ) { Text(“View in Compose”) UIKitView( factory = createUIView, modifier = Modifier.size(300.dp).border(2.dp, Color.Blue), ) } }
Interop fun AppScreen(createUIView: () -> UIView): UIViewController = ComposeUIViewController {
Column( horizontalAlignment = Alignment.CenterHorizontally ) { Text(“View in Compose”) UIKitView( factory = createUIView, modifier = Modifier.size(300.dp).border(2.dp, Color.Blue), ) } }
Interop App Screen SwiftUI View Provide
androidApp iOSApp desktopApp shared Interop
Interop struct ComposeView: UIViewControllerRepresentable { func makeUIViewController( ... )
-> UIViewController { AppScreen( VStack { Text(“Compose View”) } ) } }
Interop struct ComposeView: UIViewControllerRepresentable { func makeUIViewController( ... )
-> UIViewController { AppScreen( VStack { Text(“Compose View”) } ) } }
Interop struct ComposeView: UIViewControllerRepresentable { func makeUIViewController( ... )
-> UIViewController { AppScreen( VStack { Text(“SwiftUI in Compose”) } ) } } SwiftUI in Compose Compose View
• Compose in SwiftUI • SwiftUI in Compose • UIKit
in Compose Interop
https: / / github.com/chrisbanes/tivi Tivi
Problem • Tivi App • Modal in Compose
Problem • Tivi App • Modal in Compose • Show
iOS date picker from Compose
Interop shared src commonMain androidMain iOSMain Expect Declaration
Interop shared src commonMain androidMain iOSMain Actual Declaration Actual Declaration
Interop @Composable expect fun TimePickerDialog( onDismissRequest: () -> Unit, onTimeChanged:
(LocalTime) -> Unit, selectedTime: LocalTime )
Interop shared src commonMain androidMain iOSMain Actual Declaration
Interop @Composable actual fun TimePickerDialog(…) { DatePickerViewController(backgroundColor).apply { ... confirmButton.setTitle(confirmLabel,
UIControlStateNormal) } }
Interop @Composable actual fun TimePickerDialog(…) { DatePickerViewController(backgroundColor).apply { ... confirmButton.setTitle(confirmLabel)
} }
Interop class DatePickerViewController( ... ) : UIViewController { }
Interop class DatePickerViewController( ... ) : UIViewController { val datePicker
= UIDatePicker() val stack = UIStackView() override fun viewDidLoad() { super.viewDidLoad() . .. view.addSubview(stack) } }
Interop class DatePickerViewController( ... ) : UIViewController { val datePicker
= UIDatePicker() val stack = UIStackView() override fun viewDidLoad() { super.viewDidLoad() . .. view.addSubview(stack) } }
Interop shared src commonMain androidMain iOSMain Actual Declaration
Interop @Composable actual fun TimePickerDialog(…) { }
Interop @Composable actual fun TimePickerDialog(…) { androidx.compose.material3.DatePickerDialog(…) { TimePicker( state
= timePickerState, modifier = Modifier .padding(top = 32.dp) .align(Alignment.CenterHorizontally), ) } }
Problem • Tivi App • Modal in Compose • Show
iOS date picker from Compose
• Compose in SwiftUI • SwiftUI in Compose • UIKit
in Compose Interop
None
Roadmap • Navigation • Transitions • Text selection and input
• Accessibility • Dialogs and popups
Building Shared UIs Across Platforms with Compose • Setup &
Architecture • Internals • Interop with iOS
Thank You! www.codingwithmohit.com @heyitsmohit