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
1
380
Building Shared UIs across Platforms with Compose
Mohit S
September 16, 2023
Tweet
Share
More Decks by Mohit S
See All by Mohit S
Building Multiplatform Apps with Compose
heyitsmohit
2
310
Building StateFlows with Jetpack Compose
heyitsmohit
6
1.6k
Building Android Testing Infrastructure
heyitsmohit
1
320
Migrating to Kotlin State & Shared Flows
heyitsmohit
1
560
Using Square Workflow for Android & iOS
heyitsmohit
1
320
Building Android Infrastructure Teams at Scale
heyitsmohit
2
230
Strategies for Migrating to Jetpack Compose
heyitsmohit
2
440
Challenges of Building Kotlin Multiplatform Libraries
heyitsmohit
1
370
Introduction to Jetpack Compose
heyitsmohit
2
370
Other Decks in Programming
See All in Programming
Long journey of Ruby standard library RubyKaigi 2024
andpad
2
250
哲学史とモデリング
tanakahisateru
2
430
欠陥を早期に発見するための Software Engineer in Test とその重要性 / What is Software Engineer in Test and How they works
orgachem
PRO
18
2.4k
Implementing Design Systems in Swift
seyfoyun
2
530
Productivity is Messing Around and Having Fun
hollycummins
1
180
WebGLで始める コンピュータグラフィックス入門
heller77
0
390
Effectで作る堅牢でスケーラブルなAPIゲートウェイ / Robust and Scalable API Gateway Built on Effect
yasaichi
7
1.2k
Using "modern" Ruby to build a better, faster Homebrew
mikemcquaid
2
280
Slackワークフローで感謝を伝える機能/WiFi 自動接続/Figma to React Component/障害レポート君 Team3@NOT A HOTEL
nakaohiroshi
0
110
TypeScriptでもLLMアプリケーション開発 / LLM Application In Typescript
rkaga
5
1.3k
Exploring Type-Informed Lint Rules in Rust based TypeScript Linters
unvalley
3
650
otelcol receiver 自作RTA / Pepabo Tech Conference #22 春のSREまつり
arthur1
0
1k
Featured
See All Featured
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
21
2k
Designing Experiences People Love
moore
136
23k
Docker and Python
trallard
35
2.7k
What’s in a name? Adding method to the madness
productmarketing
PRO
17
2.7k
Build your cross-platform service in a week with App Engine
jlugia
226
17k
10 Git Anti Patterns You Should be Aware of
lemiorhan
649
58k
Adopting Sorbet at Scale
ufuk
69
8.6k
Scaling GitHub
holman
457
140k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
41
4.5k
Why Our Code Smells
bkeepers
PRO
331
56k
WebSockets: Embracing the real-time Web
robhawkes
59
7k
Become a Pro
speakerdeck
PRO
13
4.6k
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