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. Building Shared UIs Across Platforms with Compose • Setup &

    Architecture • Internals • Interop with iOS
  2. Approaches UI Components (Compose) • Share individual UI components UI

    Components (SwiftUI) Shared Components (Compose)
  3. Goal ZStack { LazyVGrid( ... ) { ForEach(id: .id) {

    item in Image(item.url) .renderingMode(.original) .resizable() .scaledToFill() } }.task { await repository.getImages() } }
  4. plugins { kotlin("multiplatform") } val commonMain by getting { dependencies

    { implementation(compose.ui) implementation(compose.foundation) implementation(compose.material) implementation(compose.runtime) } } Shared Module
  5. plugins { kotlin("multiplatform") } val commonMain by getting { dependencies

    { implementation(compose.ui) implementation(compose.foundation) implementation(compose.material) implementation(compose.runtime) } } Shared Module
  6. 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 )
  7. App Theme @Composable fun ImagesAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content:

    @Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColorPalette else LightColorPalette, content = content ) }
  8. App Theme @Composable fun ImagesAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content:

    @Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColorPalette else LightColorPalette, content = content ) }
  9. UI Structure struct ComposeView: UIViewControllerRepresentable { 
 func makeUIViewController(context: Context)

    -> UIViewController { let controller = Main_iosKt.MainiOS() return controller } }
  10. UI Structure struct ComposeView: UIViewControllerRepresentable { 
 func makeUIViewController( ...

    ) -> UIViewController { let controller = Main_iosKt.MainiOS() return controller } }
  11. UI Structure struct ComposeView: UIViewControllerRepresentable { 
 func makeUIViewController( ...

    ) -> UIViewController { let controller = ImagesList() return controller } }
  12. Multiplatform Core fun ComposeUIViewController( content: @Composable () -> Unit ):

    UIViewController = ComposeWindow().apply { configuration = ComposeUIViewControllerConfiguration() .apply(configure) setContent(content) }
  13. Multiplatform Core fun ComposeUIViewController( content: @Composable () -> Unit ):

    UIViewController = ComposeWindow().apply { setContent(content) }
  14. UI Structure struct ContentView: View { 
 var body: some

    View { ZStack { ComposeView() ...
 } } 
 } Hello World
  15. UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:

    View { 
 var body: some View { ZStack { ComposeView() ...
 } } 
 } UIWindowScene UIWindow ComposeWindow SkikkoUIView
  16. UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:

    View { 
 var body: some View { ZStack { ComposeView() ...
 } } 
 } UIWindowScene UIWindow ComposeWindow SkikkoUIView
  17. UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:

    View { 
 var body: some View { ZStack { ComposeView() ...
 } } 
 } UIWindowScene UIWindow ComposeWindow SkikkoUIView
  18. UI Structure UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy struct ContentView:

    View { 
 var body: some View { ZStack { ComposeView() ...
 } } 
 } UIWindowScene UIWindow ComposeWindow SkikkoUIView
  19. UI Structure class ComposeWindow : UIViewController { var layer: ComposeLayer

    var content: @Composable () -> Unit 
 override fun loadView() { ... } }
  20. UI Structure class ComposeWindow : UIViewController { override fun loadView()

    { val skiaLayer = createSkiaLayer() val skikoUIView = SkikoUIView(skiaLayer = skiaLayer).load() val rootView = UIView() rootView.addSubview(skikoUIView) } }
  21. 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() } } ) } }
  22. UI Structure struct ContentView: View { 
 var body: some

    View { ZStack { ComposeView() ...
 } } 
 } Hello World
  23. UI Structure struct ContentView: View { 
 var body: some

    View { ZStack { ComposeView() ...
 } } 
 }
  24. 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)
  25. Architecture sealed class UiState { object Loading: UiState() data class

    Success( val images: List<ImageData> ): UiState() data class Error( val errorMessage: String ): UiState() }
  26. Architecture object DisplayLinkClock : MonotonicFrameClock { val displayLink: CADisplayLink =

    val clock = BroadcastFrameClock { ... } override suspend fun withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R { return clock.withFrameNanos(onFrame) } }
  27. Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(…)

    val models: StateFlow<Model> by lazy(…) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { models(…) } } }
  28. Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(…)

    val models: StateFlow<Model> by lazy(…) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { models(…) } } }
  29. Architecture abstract class MoleculeViewModel: ViewModel() { val scope = CoroutineScope(…)

    val models: StateFlow<Model> by lazy(…) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { models(…) } } }
  30. Architecture abstract class MoleculeViewModel: ViewModel() { val events = MutableSharedFlow<Event>(extraBufferCapacity

    = 20) fun take(event: Event) { if (!events.tryEmit(event)) { error("Event buffer overflow.") } } }
  31. Architecture abstract class MoleculeViewModel: ViewModel() { val events = MutableSharedFlow<Event>(extraBufferCapacity

    = 20) fun take(event: Event) { if (!events.tryEmit(event)) { error("Event buffer overflow.") } } }
  32. Architecture @Composable override fun models(events: Flow<Event>): UiState { var uiState

    by remember { mutableStateOf(UIState.Loading) } LaunchedEffect(Unit) { } }
  33. 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) } }
  34. 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 }
  35. Architecture ComposeUIViewController { AppTheme { 
 val viewModel = getViewModel(…)

    val model by viewModel.models.collectAsState() ImagesList(model) } }
  36. Architecture fun ImagesList(model: UiState) { Column { LazyVerticalGrid { items(images)

    { KamelImage( asyncPainterResource(image.path), contentScale = ContentScale.Crop, ) } }
  37. UI Structure struct ContentView: View { 
 var body: some

    View { ZStack { ComposeView() ...
 } } 
 }
  38. 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), ) } }
  39. 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), ) } }
  40. 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), ) } }
  41. 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), ) } }
  42. Interop struct ComposeView: UIViewControllerRepresentable { 
 func makeUIViewController( ... )

    -> UIViewController { AppScreen( VStack { Text(“Compose View”) } ) } }
  43. Interop struct ComposeView: UIViewControllerRepresentable { 
 func makeUIViewController( ... )

    -> UIViewController { AppScreen( VStack { Text(“Compose View”) } ) } }
  44. Interop struct ComposeView: UIViewControllerRepresentable { 
 func makeUIViewController( ... )

    -> UIViewController { AppScreen( VStack { Text(“SwiftUI in Compose”) } ) } } SwiftUI in Compose Compose View
  45. Interop class DatePickerViewController( ... ) : UIViewController { val datePicker

    = UIDatePicker() val stack = UIStackView() override fun viewDidLoad() { super.viewDidLoad() . .. view.addSubview(stack) } }
  46. Interop class DatePickerViewController( ... ) : UIViewController { val datePicker

    = UIDatePicker() val stack = UIStackView() override fun viewDidLoad() { super.viewDidLoad() . .. view.addSubview(stack) } }
  47. Interop @Composable actual fun TimePickerDialog(…) { androidx.compose.material3.DatePickerDialog(…) { TimePicker( state

    = timePickerState, modifier = Modifier .padding(top = 32.dp) .align(Alignment.CenterHorizontally), ) } }
  48. Building Shared UIs Across Platforms with Compose • Setup &

    Architecture • Internals • Interop with iOS