Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Shared UIs across Platforms with Compose

Mohit S
September 16, 2023

Building Shared UIs across Platforms with Compose

Mohit S

September 16, 2023
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Mohit Sarveiya
    Building Shared UIs Across Platforms with Compose
    @heyitsmohit

    View Slide

  2. Building Shared UIs Across Platforms with Compose
    ● Setup & Architecture

    View Slide

  3. Building Shared UIs Across Platforms with Compose
    ● Setup & Architecture

    ● Internals

    View Slide

  4. Building Shared UIs Across Platforms with Compose
    ● Setup & Architecture

    ● Internals

    ● Interop with iOS

    View Slide

  5. Android

    Kotlin/JVM
    iOS

    Swift/LLVM
    Web

    JS
    Desktop

    Kotlin/JVM

    View Slide

  6. API
    Share
    Cache Business Logic
    Platforms

    View Slide

  7. API
    Share
    Cache Business Logic UI Components
    Platforms

    View Slide

  8. https:
    /
    /
    github.com/JetBrains/compose-multiplatform
    compose-multiplatform

    View Slide

  9. Approaches
    • Share all UI components
    Components (Compose)

    View Slide

  10. Approaches
    UI Components (Compose)
    • Share individual UI components
    UI Components (SwiftUI)

    View Slide

  11. Approaches
    UI Components (Compose)
    • Share individual UI components
    UI Components (SwiftUI)
    Shared Components (Compose)

    View Slide

  12. Approaches
    • Share individual UI components

    • Share all UI components

    View Slide

  13. Example
    • SwiftUI App

    • Display list of images

    View Slide

  14. Example
    • SwiftUI App

    • Display list of images

    • Details page

    View Slide

  15. Goal
    • Display list of images (Compose)

    • Details page (SwiftUI)

    View Slide

  16. Goal
    ZStack {

    LazyVGrid(
    ...
    ) {

    ForEach(id: .id) { item in

    Image(item.url)

    .renderingMode(.original)

    .resizable()

    .scaledToFill()

    }

    }.task {

    await repository.getImages()

    }

    }

    View Slide

  17. UI Structure
    Shared Component
    NavigationView {

    ZStack {

    ComposeView()

    }

    }.toolbar {

    ...


    }

    View Slide

  18. https:
    /
    /
    github.com/JetBrains/compose-multiplatform-template
    compose-multiplatform

    View Slide

  19. androidApp
    iOSApp
    shared
    Structure

    View Slide

  20. shared
    src
    commonMain
    androidMain
    iOSMain
    Shared Module

    View Slide

  21. shared
    src
    commonMain
    androidMain
    iOSMain
    build.gradle.kts
    Shared Module

    View Slide

  22. plugins {

    kotlin("multiplatform")

    }

    val commonMain by getting {

    dependencies {

    implementation(compose.ui)

    implementation(compose.foundation)

    implementation(compose.material)

    implementation(compose.runtime)

    }

    }

    Shared Module

    View Slide

  23. plugins {

    kotlin("multiplatform")

    }

    val commonMain by getting {

    dependencies {

    implementation(compose.ui)

    implementation(compose.foundation)

    implementation(compose.material)

    implementation(compose.runtime)

    }

    }

    Shared Module

    View Slide

  24. UI Structure

    View Slide

  25. UI Structure
    Shared Component
    NavigationView {

    ZStack {

    ComposeView()

    }

    }.toolbar {

    ...


    }

    View Slide

  26. UI Structure
    Compose View
    Images List
    ViewController
    AppTheme
    NavigationView {

    ZStack {

    ComposeView()

    }

    }.toolbar {

    ...


    }

    View Slide

  27. shared
    src
    commonMain
    androidMain
    iOSMain
    ImagesAppTheme.kt
    Shared Module

    View Slide

  28. App Theme

    View Slide

  29. 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

    )

    View Slide

  30. App Theme
    @Composable

    fun ImagesAppTheme(

    darkTheme: Boolean = isSystemInDarkTheme(),

    content: @Composable ()
    ->
    Unit

    ) {

    MaterialTheme(

    colors = if (darkTheme) DarkColorPalette else LightColorPalette,

    content = content

    )

    }

    View Slide

  31. App Theme
    @Composable

    fun ImagesAppTheme(

    darkTheme: Boolean = isSystemInDarkTheme(),

    content: @Composable ()
    ->
    Unit

    ) {

    MaterialTheme(

    colors = if (darkTheme) DarkColorPalette else LightColorPalette,

    content = content

    )

    }

    View Slide

  32. UI Structure
    Compose View
    Images List
    ViewController
    AppTheme
    NavigationView {

    ZStack {

    ComposeView()

    }

    }.toolbar {

    ...


    }

    View Slide

  33. UI Structure
    struct ComposeView: UIViewControllerRepresentable {

    func makeUIViewController(context: Context)
    ->
    UIViewController {

    let controller = Main_iosKt.MainiOS()

    return controller

    }

    }

    View Slide

  34. UI Structure
    struct ComposeView: UIViewControllerRepresentable {

    func makeUIViewController(
    ...
    )
    ->
    UIViewController {

    let controller = Main_iosKt.MainiOS()

    return controller

    }

    }

    View Slide

  35. UI Structure
    struct ComposeView: UIViewControllerRepresentable {

    func makeUIViewController(
    ...
    )
    ->
    UIViewController {

    let controller = ImagesList()

    return controller

    }

    }

    View Slide

  36. UI Structure
    fun ImagesList(): UIViewController =

    ComposeUIViewController {

    ImagesAppCommon()

    }

    View Slide

  37. UI Structure
    fun ImagesList(): UIViewController =

    ComposeUIViewController {

    ...


    }

    View Slide

  38. Compose Architecture
    Compose Multiplatform
    Compose Multiplatform Core

    View Slide

  39. https:
    /
    /
    github.com/JetBrains/compose-multiplatform-core
    compose-multiplatform

    View Slide

  40. Multiplatform Core
    fun ComposeUIViewController(

    content: @Composable ()
    ->
    Unit

    ): UIViewController =

    ComposeWindow().apply {

    configuration = ComposeUIViewControllerConfiguration()

    .apply(configure)

    setContent(content)

    }

    View Slide

  41. Multiplatform Core
    fun ComposeUIViewController(

    content: @Composable ()
    ->
    Unit

    ): UIViewController =

    ComposeWindow().apply {

    setContent(content)

    }

    View Slide

  42. UI Structure
    Compose View
    Images List
    ViewController
    AppTheme
    NavigationView {

    ZStack {

    ComposeView()

    }

    }.toolbar {

    ...


    }

    View Slide

  43. UI Structure
    fun ImagesList(): UIViewController =

    ComposeUIViewController {

    ...


    }

    View Slide

  44. UI Structure
    fun ImagesList(): UIViewController =

    ComposeUIViewController {

    AppTheme {

    }

    }

    Text("Hello World")

    View Slide

  45. UI Structure
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    Hello World

    View Slide

  46. UI Structure
    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView
    View Hierarchy
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView

    View Slide

  47. UI Structure
    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView
    View Hierarchy
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView

    View Slide

  48. UI Structure
    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView
    View Hierarchy
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView

    View Slide

  49. https:
    /
    /
    github.com/JetBrains/skiko
    Skiko

    View Slide

  50. Compose Multiplatform Architecture
    Skia
    Skiko
    Compose
    UIViewController
    UIKit SwiftUI

    View Slide

  51. UI Structure
    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView
    View Hierarchy
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    UIWindowScene
    UIWindow
    ComposeWindow
    SkikkoUIView

    View Slide

  52. UI Structure
    class ComposeWindow : UIViewController {

    var layer: ComposeLayer

    var content: @Composable ()
    ->
    Unit

    override fun loadView() {

    ...


    }

    }

    View Slide

  53. UI Structure
    fun ImagesList(): UIViewController =

    ComposeUIViewController {

    AppTheme {

    }

    }

    Text("Hello World")

    View Slide

  54. Compose Multiplatform Architecture
    Skia
    Skiko
    Compose
    UIViewController
    UIKit SwiftUI

    View Slide

  55. UI Structure
    class ComposeWindow : UIViewController {

    override fun loadView() {

    val skiaLayer = createSkiaLayer()

    val skikoUIView = SkikoUIView(skiaLayer = skiaLayer).load()

    val rootView = UIView()

    rootView.addSubview(skikoUIView)

    }

    }

    View Slide

  56. 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() }

    }

    )

    }

    }

    View Slide

  57. UI Structure
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    Hello World

    View Slide

  58. Architecture

    View Slide

  59. UI Structure
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    View Slide

  60. Architecture
    View
    Repo
    View Model

    SwiftUI
    ComposeView
    Shared

    View Slide

  61. View Model Repository
    View
    Request
    Response
    UI State
    Event

    View Slide

  62. https:
    /
    /
    github.com/cashapp/molecule
    Molecule

    View Slide

  63. @Composable

    fun Presenter(): Model State Flow
    Compose Runtime
    Recomposition

    View Slide

  64. @Composable

    fun Presenter(): Model State Flow
    Recomposition
    Monotomic Frame Clock

    View Slide

  65. 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)

    View Slide

  66. https:
    /
    /
    github.com/icerockdev/moko-mvvm
    Moko

    View Slide

  67. Architecture
    sealed class UiState {

    object Loading: UiState()

    data class Success(

    val images: List

    ): UiState()

    data class Error(

    val errorMessage: String

    ): UiState()

    }

    View Slide

  68. Architecture
    abstract class MoleculeViewModel
    <>
    : ViewModel() {

    }

    View Slide

  69. Architecture
    abstract class MoleculeViewModel>:
    ViewModel() {

    }

    View Slide

  70. Architecture
    abstract class MoleculeViewModel>:
    ViewModel() {

    }

    View Slide

  71. Architecture
    abstract class MoleculeViewModel: ViewModel() {

    val scope = CoroutineScope(

    viewModelScope.coroutineContext

    )

    }

    View Slide

  72. Architecture
    Frame Clock
    DisplayLinkClock
    iOS
    AndroidUiFrameClock
    Android

    View Slide

  73. https:
    /
    /
    developer.apple.com/documentation/quartzcore/cadisplaylink
    CADisplayLink

    View Slide

  74. Architecture
    abstract class MoleculeViewModel: ViewModel() {

    val scope = CoroutineScope(

    viewModelScope.coroutineContext + DisplayLinkClock

    )

    }

    View Slide

  75. Architecture
    object DisplayLinkClock : MonotonicFrameClock {

    val displayLink: CADisplayLink =

    val clock = BroadcastFrameClock {
    ...
    }

    override

    suspend fun withFrameNanos(onFrame: (frameTimeNanos: Long)
    ->
    R): R {

    return clock.withFrameNanos(onFrame)

    }

    }

    View Slide

  76. Architecture
    abstract class MoleculeViewModel: ViewModel() {

    val scope = CoroutineScope(…)

    val models: StateFlow by lazy(…) {

    scope.launchMolecule(mode = RecompositionMode.ContextClock) {

    models(…)

    }

    }

    }

    View Slide

  77. Architecture
    abstract class MoleculeViewModel: ViewModel() {

    val scope = CoroutineScope(…)

    val models: StateFlow by lazy(…) {

    scope.launchMolecule(mode = RecompositionMode.ContextClock) {

    models(…)

    }

    }

    }

    View Slide

  78. Architecture
    abstract class MoleculeViewModel: ViewModel() {

    val scope = CoroutineScope(…)

    val models: StateFlow by lazy(…) {

    scope.launchMolecule(mode = RecompositionMode.ContextClock) {

    models(…)

    }

    }

    }

    View Slide

  79. View Model
    View
    UI State
    Event

    View Slide

  80. Architecture
    abstract class MoleculeViewModel: ViewModel() {

    val events = MutableSharedFlow(extraBufferCapacity = 20)

    fun take(event: Event) {

    if (!events.tryEmit(event)) {

    error("Event buffer overflow.")

    }

    }

    }

    View Slide

  81. Architecture
    abstract class MoleculeViewModel: ViewModel() {

    val events = MutableSharedFlow(extraBufferCapacity = 20)

    fun take(event: Event) {

    if (!events.tryEmit(event)) {

    error("Event buffer overflow.")

    }

    }

    }

    View Slide

  82. Architecture
    View
    Repo
    View Model

    SwiftUI
    ComposeView
    Shared

    View Slide

  83. Architecture
    class ImagesViewModel: MoleculeViewModel() {

    }

    View Slide

  84. Architecture
    class ImagesViewModel: MoleculeViewModel() {

    @Composable

    override fun models(events: Flow): UiState {

    }

    }

    View Slide

  85. Architecture
    @Composable

    override fun models(events: Flow): UiState {

    var uiState by remember { mutableStateOf(UIState.Loading) }

    }

    View Slide

  86. Architecture
    @Composable

    override fun models(events: Flow): UiState {

    var uiState by remember { mutableStateOf(UIState.Loading) }

    LaunchedEffect(Unit) {

    }

    }

    View Slide

  87. Architecture
    @Composable

    override fun models(events: Flow): UiState {

    var uiState by remember { mutableStateOf(UIState.Loading) }

    LaunchedEffect(Unit) {

    val imagesList = imagesRepository.getImages()

    uiState = UIState.Success(imagesList)

    }

    }

    View Slide

  88. Architecture
    @Composable

    override fun models(events: Flow): UiState {

    var uiState by remember { mutableStateOf(UIState.Loading) }

    LaunchedEffect(Unit) {

    val imagesList = imagesRepository.getImages()

    uiState = UIState.Success(imagesList)

    }

    return uiState

    }

    View Slide

  89. Architecture
    fun ImagesList(): UIViewController =

    ComposeUIViewController {

    AppTheme {

    }

    }

    View Slide

  90. Architecture
    ComposeUIViewController {

    AppTheme {

    val viewModel = getViewModel(…, viewModelFactory {

    ImagesViewModel()

    })

    }

    }

    View Slide

  91. Architecture
    ComposeUIViewController {

    AppTheme {

    val viewModel = getViewModel(…)

    val model by viewModel.models.collectAsState()

    }

    }

    View Slide

  92. Architecture
    ComposeUIViewController {

    AppTheme {

    val viewModel = getViewModel(…)

    val model by viewModel.models.collectAsState()

    ImagesList(model)

    }

    }

    View Slide

  93. Architecture
    View
    Repo
    View Model

    SwiftUI
    ComposeView
    Shared

    View Slide

  94. Architecture
    fun ImagesList(model: UiState) {

    Column {

    LazyVerticalGrid {

    items(images) {

    ...


    }

    }

    }

    }

    View Slide

  95. https:
    /
    /
    github.com/Kamel-Media/Kamel
    Kamel

    View Slide

  96. Architecture
    fun ImagesList(model: UiState) {

    Column {

    LazyVerticalGrid {

    items(images) {

    KamelImage(

    asyncPainterResource(image.path),

    contentScale = ContentScale.Crop,

    )

    }

    }

    View Slide

  97. UI Structure
    struct ContentView: View {

    var body: some View {

    ZStack {

    ComposeView()

    ...

    }

    }

    }

    View Slide

  98. Architecture
    View
    Repo
    View Model

    SwiftUI
    ComposeView
    Shared

    View Slide

  99. iOS Interop

    View Slide

  100. ● Compose in SwiftUI
    Interop

    View Slide

  101. ● Compose in SwiftUI

    ● SwiftUI in Compose
    Interop

    View Slide

  102. Interop
    View in
    Compose

    View Slide

  103. Interop
    SwiftUI
    View
    View in
    Compose

    View Slide

  104. Interop
    Compose View
    SwiftUI View
    Provide

    View Slide

  105. Interop
    shared
    src
    commonMain
    androidMain
    iOSMain
    App Screen

    View Slide

  106. 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),

    )

    }

    }

    View Slide

  107. 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),

    )

    }

    }

    View Slide

  108. 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),

    )

    }

    }

    View Slide

  109. 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),

    )

    }

    }

    View Slide

  110. Interop
    App Screen
    SwiftUI View
    Provide

    View Slide

  111. androidApp
    iOSApp
    desktopApp
    shared
    Interop

    View Slide

  112. Interop
    struct ComposeView: UIViewControllerRepresentable {

    func makeUIViewController(
    ...
    )
    ->
    UIViewController {

    AppScreen(

    VStack {

    Text(“Compose View”)

    }

    )

    }

    }

    View Slide

  113. Interop
    struct ComposeView: UIViewControllerRepresentable {

    func makeUIViewController(
    ...
    )
    ->
    UIViewController {

    AppScreen(

    VStack {

    Text(“Compose View”)

    }

    )

    }

    }

    View Slide

  114. Interop
    struct ComposeView: UIViewControllerRepresentable {

    func makeUIViewController(
    ...
    )
    ->
    UIViewController {

    AppScreen(

    VStack {

    Text(“SwiftUI in Compose”)

    }

    )

    }

    }
    SwiftUI in
    Compose
    Compose
    View

    View Slide

  115. ● Compose in SwiftUI

    ● SwiftUI in Compose

    ● UIKit in Compose
    Interop

    View Slide

  116. https:
    /
    /
    github.com/chrisbanes/tivi
    Tivi

    View Slide

  117. Problem
    ● Tivi App

    ● Modal in Compose

    View Slide

  118. Problem
    ● Tivi App

    ● Modal in Compose

    ● Show iOS date picker from Compose

    View Slide

  119. Interop
    shared
    src
    commonMain
    androidMain
    iOSMain
    Expect Declaration

    View Slide

  120. Interop
    shared
    src
    commonMain
    androidMain
    iOSMain
    Actual Declaration
    Actual Declaration

    View Slide

  121. Interop
    @Composable

    expect fun TimePickerDialog(

    onDismissRequest: ()
    ->
    Unit,

    onTimeChanged: (LocalTime)
    ->
    Unit,

    selectedTime: LocalTime

    )

    View Slide

  122. Interop
    shared
    src
    commonMain
    androidMain
    iOSMain Actual Declaration

    View Slide

  123. Interop
    @Composable

    actual fun TimePickerDialog(…) {

    DatePickerViewController(backgroundColor).apply {

    ...


    confirmButton.setTitle(confirmLabel, UIControlStateNormal)

    }

    }

    View Slide

  124. Interop
    @Composable

    actual fun TimePickerDialog(…) {

    DatePickerViewController(backgroundColor).apply {

    ...


    confirmButton.setTitle(confirmLabel)

    }

    }

    View Slide

  125. Interop
    class DatePickerViewController(
    ...
    ) : UIViewController {

    }

    View Slide

  126. Interop
    class DatePickerViewController(
    ...
    ) : UIViewController {

    val datePicker = UIDatePicker()

    val stack = UIStackView()

    override fun viewDidLoad() {

    super.viewDidLoad()

    .
    ..


    view.addSubview(stack)

    }

    }

    View Slide

  127. Interop
    class DatePickerViewController(
    ...
    ) : UIViewController {

    val datePicker = UIDatePicker()

    val stack = UIStackView()

    override fun viewDidLoad() {

    super.viewDidLoad()

    .
    ..


    view.addSubview(stack)

    }

    }

    View Slide

  128. Interop
    shared
    src
    commonMain
    androidMain
    iOSMain
    Actual Declaration

    View Slide

  129. Interop
    @Composable

    actual fun TimePickerDialog(…) {

    }

    View Slide

  130. Interop
    @Composable

    actual fun TimePickerDialog(…) {

    androidx.compose.material3.DatePickerDialog(…) {

    TimePicker(

    state = timePickerState,

    modifier = Modifier

    .padding(top = 32.dp)

    .align(Alignment.CenterHorizontally),

    )

    }

    }

    View Slide

  131. Problem
    ● Tivi App

    ● Modal in Compose

    ● Show iOS date picker from Compose

    View Slide

  132. ● Compose in SwiftUI

    ● SwiftUI in Compose

    ● UIKit in Compose
    Interop

    View Slide

  133. View Slide

  134. Roadmap
    ● Navigation

    ● Transitions

    ● Text selection and input

    ● Accessibility

    ● Dialogs and popups

    View Slide

  135. Building Shared UIs Across Platforms with Compose
    ● Setup & Architecture

    ● Internals

    ● Interop with iOS

    View Slide

  136. Thank You!
    www.codingwithmohit.com
    @heyitsmohit

    View Slide