Slide 1

Slide 1 text

Mohit Sarveiya Building Multiplatform Apps with Compose @heyitsmohit@androiddev.social

Slide 2

Slide 2 text

Building Multiplatform Apps with Compose ● Setup Project

Slide 3

Slide 3 text

Building Multiplatform Apps with Compose ● Setup Project ● Share Compose UI

Slide 4

Slide 4 text

Building Multiplatform Apps with Compose ● Setup Project ● Share Compose UI ● SwiftUI & Compose Interop

Slide 5

Slide 5 text

Building Multiplatform Apps with Compose ● Setup Project ● Share Compose UI ● SwiftUI & Compose Interop ● Architecture & Navigation

Slide 6

Slide 6 text

Android Kotlin/JVM iOS Swift/LLVM Web JS Desktop Kotlin/JVM

Slide 7

Slide 7 text

Share

Slide 8

Slide 8 text

API Share

Slide 9

Slide 9 text

API Share Cache

Slide 10

Slide 10 text

API Share Cache Business Logic

Slide 11

Slide 11 text

API Share Cache Business Logic UI Components

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Compose Multiplatform • Android (via Jetpack Compose) • Desktop (Windows, Mac OS, Linux) • Web (Experimental)

Slide 14

Slide 14 text

Compose Multiplatform • Android (via Jetpack Compose) • Desktop (Windows, Mac OS, Linux) • Web (Experimental) • iOS (Alpha)

Slide 15

Slide 15 text

Compose Multiplatform

Slide 16

Slide 16 text

Compose Multiplatform

Slide 17

Slide 17 text

UI Structure

Slide 18

Slide 18 text

androidApp iOSApp desktopApp shared Structure

Slide 19

Slide 19 text

shared src commonMain androidMain iOSMain Shared Module

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

shared src commonMain androidMain iOSMain Shared Module

Slide 24

Slide 24 text

UI Structure App Root View Android iOS

Slide 25

Slide 25 text

UI Structure App Root View Android iOS

Slide 26

Slide 26 text

shared src commonMain androidMain iOSMain UI Structure

Slide 27

Slide 27 text

shared src commonMain androidMain iOSMain ImagesApp.commmon.kt UI Structure

Slide 28

Slide 28 text

fun ImagesAppCommon() { Scaffold( topBar = { TopAppBar( ... ) }, content = { ... } ) } UI Structure

Slide 29

Slide 29 text

fun ImagesAppCommon() { Scaffold( topBar = { TopAppBar( ... ) }, content = { ... } ) } UI Structure

Slide 30

Slide 30 text

shared src commonMain androidMain iOSMain ImagesApp.commmon.kt ImagesAppTheme.kt Shared Module

Slide 31

Slide 31 text

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 ) }

Slide 32

Slide 32 text

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 ) }

Slide 33

Slide 33 text

shared src commonMain androidMain iOSMain ImagesApp.android.kt Shared Module

Slide 34

Slide 34 text

UI Structure @Composable fun MainAndroid() { ImagesAppTheme { ImagesAppCommon() } }

Slide 35

Slide 35 text

androidApp iOSApp desktopApp shared UI Structure

Slide 36

Slide 36 text

UI Structure class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MainAndroid() } } }

Slide 37

Slide 37 text

UI Structure class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MainAndroid() } } }

Slide 38

Slide 38 text

shared src commonMain androidMain iOSMain ImagesApp.iOS.kt Shared Module

Slide 39

Slide 39 text

UI Structure fun MainiOS(): UIViewController ComposeUIViewController { ImagesAppCommon() }

Slide 40

Slide 40 text

UI Structure fun MainiOS(): UIViewController = ComposeUIViewController { ImagesAppCommon() }

Slide 41

Slide 41 text

androidApp iOSApp desktopApp shared UI Structure

Slide 42

Slide 42 text

UI Structure @main struct iOSApp: App { 
 var body: some Scene { WindowGroup { ContentView() } } 
 }

Slide 43

Slide 43 text

UI Structure @main struct iOSApp: App { 
 var body: some Scene { WindowGroup { ContentView() } } 
 }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

UI Structure App Root View Android iOS

Slide 54

Slide 54 text

UI Structure

Slide 55

Slide 55 text

Challenge: Image Loading

Slide 56

Slide 56 text

https: / / github.com/coil-kt/coil/issues/842 Coil-kt

Slide 57

Slide 57 text

shared src resource image1.jpeg image2.jpeg image3.jpeg Shared Module

Slide 58

Slide 58 text

val commonMain by getting { dependencies { implementation(compose.components.resources) } } Shared Module

Slide 59

Slide 59 text

val commonMain by getting { dependencies { implementation(compose.components.resources) } } Shared Module

Slide 60

Slide 60 text

class ImageProvider { suspend fun getImageBitmap(picture: ImageData): ImageBitmap = resource(picture.url).readBytes().toImageBitmap() } Shared Module

Slide 61

Slide 61 text

class ImageProvider { suspend fun getImageBitmap(picture: ImageData): ImageBitmap = resource(picture.url).readBytes() } Shared Module

Slide 62

Slide 62 text

class ImageProvider { suspend fun getImageBitmap(picture: ImageData): ImageBitmap = resource(picture.url).readBytes().toImageBitmap() } Shared Module

Slide 63

Slide 63 text

Except/Actual

Slide 64

Slide 64 text

shared src commonMain androidMain Shared Module iOSMain

Slide 65

Slide 65 text

expect fun ByteArray.toImageBitmap(): ImageBitmap Shared Module

Slide 66

Slide 66 text

shared src commonMain androidMain Shared Module iOSMain

Slide 67

Slide 67 text

actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap() fun ByteArray.toAndroidBitmap(): Bitmap { return BitmapFactory.decodeByteArray(this, 0, size) } Shared Module

Slide 68

Slide 68 text

actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap() fun ByteArray.toAndroidBitmap(): Bitmap { return BitmapFactory.decodeByteArray(this, 0, size) } Shared Module

Slide 69

Slide 69 text

actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap() fun ByteArray.toAndroidBitmap(): Bitmap { return BitmapFactory.decodeByteArray(this, 0, size) } Shared Module

Slide 70

Slide 70 text

actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap() fun ByteArray.toAndroidBitmap(): Bitmap { return BitmapFactory.decodeByteArray(this, 0, size) } Shared Module

Slide 71

Slide 71 text

shared src commonMain androidMain Shared Module iOSMain

Slide 72

Slide 72 text

actual fun ByteArray.toImageBitmap(): ImageBitmap = Image.makeFromEncoded(this).toComposeImageBitmap() Shared Module

Slide 73

Slide 73 text

actual fun ByteArray.toImageBitmap(): ImageBitmap = Image.makeFromEncoded(this).toComposeImageBitmap() Shared Module

Slide 74

Slide 74 text

actual fun ByteArray.toImageBitmap(): ImageBitmap = Image.makeFromEncoded(this).toComposeImageBitmap() Shared Module

Slide 75

Slide 75 text

class ImageProvider { suspend fun getImageBitmap(picture: ImageData): ImageBitmap = resource(picture.url).readBytes().toImageBitmap() } Shared Module

Slide 76

Slide 76 text

UI Structure

Slide 77

Slide 77 text

@Composable fun ImagesList(images: List) { Column { LazyVerticalGrid( columns = GridCells.Fixed(3) ) { items(images) { SplashImage( imageData = it ) } } } } UI Structure

Slide 78

Slide 78 text

@Composable fun ImagesList(images: List) { Column { LazyVerticalGrid( columns = GridCells.Fixed(3) ) { items(images) { SplashImage( imageData = it ) } } } } UI Structure

Slide 79

Slide 79 text

@Composable fun ImagesList(images: List) { Column { LazyVerticalGrid( columns = GridCells.Fixed(3) ) { items(images) { SplashImage( imageData = it ) } } } } UI Structure

Slide 80

Slide 80 text

@Composable fun SplashImage(imageData: ImageData) { val imageProvider = LocalImageProvider.current var imageBitmap by remember(imageData) { 
 mutableStateOf(null) 
 } LaunchedEffect(imageData) { imageBitmap = imageProvider.getImageBitmap(imageData) } imageBitmap ?. let { Image( bitmap = bitmap, 
 ... ) } } UI Structure

Slide 81

Slide 81 text

@Composable fun SplashImage(imageData: ImageData) { val imageProvider = LocalImageProvider.current var imageBitmap by remember(imageData) { 
 mutableStateOf(null) 
 } LaunchedEffect(imageData) { imageBitmap = imageProvider.getImageBitmap(imageData) } imageBitmap ?. let { Image( bitmap = bitmap, 
 ... ) } } UI Structure

Slide 82

Slide 82 text

@Composable fun SplashImage(imageData: ImageData) { val imageProvider = LocalImageProvider.current var imageBitmap by remember(imageData) { 
 mutableStateOf(null) 
 } LaunchedEffect(imageData) { imageBitmap = imageProvider.getImageBitmap(imageData) } imageBitmap ?. let { Image( bitmap = bitmap, 
 ... ) } } UI Structure

Slide 83

Slide 83 text

@Composable fun SplashImage(imageData: ImageData) { val imageProvider = LocalImageProvider.current var imageBitmap by remember(imageData) { 
 mutableStateOf(null) 
 } LaunchedEffect(imageData) { imageBitmap = imageProvider.getImageBitmap(imageData) } imageBitmap ?. let { Image( bitmap = bitmap, 
 ... ) } } UI Structure

Slide 84

Slide 84 text

UI Structure

Slide 85

Slide 85 text

UI Structure App Root View Android iOS Images List Images Details Shared

Slide 86

Slide 86 text

Challenges: App Architecture, Navigation

Slide 87

Slide 87 text

Compose UI View Model UI State Events

Slide 88

Slide 88 text

UI Structure App Root View Android iOS Images List Images Details Shared

Slide 89

Slide 89 text

UI Structure App Root View Android iOS Images List Images Details Shared View Model

Slide 90

Slide 90 text

Shared Module shared src commonMain androidMain iOSMain

Slide 91

Slide 91 text

Shared Module shared src commonMain androidMain iOSMain ImagesListViewModel.kt

Slide 92

Slide 92 text

App Architecture sealed class ImagesListUiState { object Loading: ImagesListUiState() data class Success( val images: List ): ImagesListUiState() data class Error( val errorMessage: String ): ImagesListUiState() }

Slide 93

Slide 93 text

App Architecture sealed class ImagesListUiState { object Loading: ImagesListUiState() data class Success( val images: List ): ImagesListUiState() data class Error( val errorMessage: String ): ImagesListUiState() }

Slide 94

Slide 94 text

App Architecture sealed class ImagesListUiState { object Loading: ImagesListUiState() data class Success( val images: List ): ImagesListUiState() data class Error( val errorMessage: String ): ImagesListUiState() }

Slide 95

Slide 95 text

App Architecture sealed class ImagesListUiState { object Loading: ImagesListUiState() data class Success( val images: List ): ImagesListUiState() data class Error( val errorMessage: String ): ImagesListUiState() }

Slide 96

Slide 96 text

App Architecture class ImagesListViewModel { init { ... } val state = MutableStateFlow(ImagesListUiState.Loading) val viewModelScope = CoroutineScope(Dispatchers.Main) } init } {

Slide 97

Slide 97 text

App Architecture class ImagesListViewModel { init { ... } } init } { viewModelScope.launch(Dispatchers.Main) { try { val imagesList = imagesRepository.getImages() state.emit(uiState.Success(images = imagesList)) } catch (e: Exception) { state.emit(uiState.Error("Something went wrong")) } }

Slide 98

Slide 98 text

App Architecture @Composable fun ImagesAppCommon() { Scaffold( topBar = { ... }, content = { } ) }

Slide 99

Slide 99 text

App Architecture @Composable fun ImagesAppCommon() { Scaffold( topBar = { ... }, content = { } ) } val uiState by viewModel.state.collectAsState() ImagesListScreen(uiState)

Slide 100

Slide 100 text

App Architecture @Composable fun ImagesListScreen(uiState: UIState) { } }

Slide 101

Slide 101 text

App Architecture @Composable fun ImagesListScreen(uiState: UIState) { when (uiState) { ImagesListUiState.Loading -> is ImagesListUiState.Success -> is ImagesListUiState.Error -> } }

Slide 102

Slide 102 text

App Architecture @Composable fun ImagesListScreen(uiState: UIState) { when (uiState) { ImagesListUiState.Loading -> CircularProgressIndicator() is ImagesListUiState.Success -> is ImagesListUiState.Error -> } }

Slide 103

Slide 103 text

App Architecture @Composable fun ImagesListScreen(uiState: UIState) { when (uiState) { ImagesListUiState.Loading -> CircularProgressIndicator() is ImagesListUiState.Success -> ImagesList(uiState.images) is ImagesListUiState.Error -> } }

Slide 104

Slide 104 text

Navigation App Root View List Details

Slide 105

Slide 105 text

App Architecture sealed class Screen { }

Slide 106

Slide 106 text

App Architecture sealed class Screen { object List : Screen() }

Slide 107

Slide 107 text

App Architecture sealed class Screen { object List : Screen() data class Details(val imageId: String) : Screen() }

Slide 108

Slide 108 text

App Architecture @Composable fun ImagesAppCommon() { var screenState by remember { mutableStateOf(Screen.List) } when (val screen = screenState) { is Screen.List -> List( onItemClick = { screenState = Screen.Details(imageId = it) } ) is Screen.Details -> Details( text = screen.text, onBack = { screenState = Screen.List } ) } }

Slide 109

Slide 109 text

App Architecture @Composable fun ImagesAppCommon() { var screenState by remember { mutableStateOf(Screen.List) } when (val screen = screenState) { is Screen.List -> List( onItemClick = { screenState = Screen.Details(imageId = it) } ) is Screen.Details -> Details( text = screen.text, onBack = { screenState = Screen.List } ) } }

Slide 110

Slide 110 text

App Architecture @Composable fun ImagesAppCommon() { var screenState by remember { mutableStateOf(Screen.List) } when (val screen = screenState) { is Screen.List -> List( onItemClick = { screenState = Screen.Details(imageId = it) } ) is Screen.Details -> Details( text = screen.text, onBack = { screenState = Screen.List } ) } }

Slide 111

Slide 111 text

App Architecture @Composable fun ImagesAppCommon() { var screenState by remember { mutableStateOf(Screen.List) } when (val screen = screenState) { is Screen.List -> List( onItemClick = { screenState = Screen.Details(imageId = it) } ) is Screen.Details -> Details( text = screen.imageId, onBack = { screenState = Screen.List } ) } }

Slide 112

Slide 112 text

App Architecture ● Lifecycle Aware ● Navigation

Slide 113

Slide 113 text

https: / / github.com/arkivanov/Decompose Decompose

Slide 114

Slide 114 text

Decompose ● Lifecycle Aware Components ● Back stack management

Slide 115

Slide 115 text

Decompose interface ListComponent { 
 val uiState: Value fun onImageClicked(imageId: String) data class UiState( val images: List, ) }

Slide 116

Slide 116 text

Decompose interface ListComponent { 
 val uiState: Value fun onImageClicked(imageId: String) data class UiState( val images: List, ) }

Slide 117

Slide 117 text

Decompose interface ListComponent { 
 val uiState: Value fun onImageClicked(imageId: String) data class UiState( val images: List, ) }

Slide 118

Slide 118 text

Decompose class ImagesListComponent( componentContext: ComponentContext, val onImageSelected: (imageId: String) -> Unit, ) : ListComponent { 
 override val uiState: Value = MutableValue(UiState.Loading) override fun onItemClicked(imageId: String) { onImageSelected(imageId) } }

Slide 119

Slide 119 text

Decompose class ImagesListComponent( componentContext: ComponentContext, val onImageSelected: (imageId: String) -> Unit, ) : ListComponent { 
 override val uiState: Value = MutableValue(UiState.Loading) override fun onItemClicked(imageId: String) { onImageSelected(imageId) } }

Slide 120

Slide 120 text

Decompose class ImagesListComponent( componentContext: ComponentContext, val onImageSelected: (imageId: String) -> Unit, ) : ListComponent { 
 override val uiState: Value = MutableValue(UiState.Loading) override fun onItemClicked(imageId: String) { onImageSelected(imageId) } }

Slide 121

Slide 121 text

@Composable fun ImagesList( component: ListComponent, images: List, onImageClicked: (Int) -> Unit ) { 
 
 val uiState by component.uiState.subscribeAsState() } Decompose

Slide 122

Slide 122 text

@Composable fun ImagesList( component: ListComponent, images: List, onImageClicked: (Int) -> Unit ) { 
 
 val uiState by component.uiState.subscribeAsState() } Decompose

Slide 123

Slide 123 text

Navigation App Root View List Details

Slide 124

Slide 124 text

interface RootComponent { val stack: Value> sealed class Child { class ListChild(val component: ListComponent) : Child() class DetailsChild(val component: DetailsComponent) : Child() } } App Architecture

Slide 125

Slide 125 text

interface RootComponent { val stack: Value> sealed class Child { class ListChild(val component: ListComponent) : Child() class DetailsChild(val component: DetailsComponent) : Child() } } App Architecture

Slide 126

Slide 126 text

interface RootComponent { val stack: Value> sealed class Child { class ListChild(val component: ListComponent) : Child() class DetailsChild(val component: DetailsComponent) : Child() } } App Architecture

Slide 127

Slide 127 text

class DefaultRootComponent( ... ): RootComponent { @Parcelize sealed interface Config : Parcelable { object List : Config data class Details(val item: String) : Config } } App Architecture

Slide 128

Slide 128 text

class DefaultRootComponent( ... ): RootComponent { 
 val navigation = StackNavigation() 
 @Parcelize sealed interface Config : Parcelable { object List : Config data class Details(val item: String) : Config } } App Architecture

Slide 129

Slide 129 text

class DefaultRootComponent( ... ): RootComponent { 
 val navigation = StackNavigation() 
 @Parcelize sealed interface Config : Parcelable { object List : Config data class Details(val item: String) : Config } } App Architecture val stack = childStack( source = navigation, initialConfiguration = Config.List, handleBackButton = true, childFactory = :: child, )

Slide 130

Slide 130 text

class DefaultRootComponent( ... ): RootComponent { 
 val navigation = StackNavigation() 
 @Parcelize sealed interface Config : Parcelable { object List : Config data class Details(val item: String) : Config } } App Architecture

Slide 131

Slide 131 text

class DefaultRootComponent( ... ): RootComponent { } App Architecture fun listComponent(): ListComponent = ImagesListComponent( onItemSelected = { imageId: String -> navigation.push(Config.Details(item = imageId)) }, )

Slide 132

Slide 132 text

class DefaultRootComponent( ... ): RootComponent { } App Architecture fun listComponent(): ListComponent = ImagesListComponent( onItemSelected = { imageId: String -> navigation.push(Config.Details(item = imageId)) }, )

Slide 133

Slide 133 text

class DefaultRootComponent( ... ): RootComponent { } App Architecture fun detailsComponent(): DetailsComponent = ImageDetailsComponent( image = config.image, onFinished = navigation :: pop, )

Slide 134

Slide 134 text

class DefaultRootComponent( ... ): RootComponent { } App Architecture fun detailsComponent(): DetailsComponent = ImageDetailsComponent( image = config.image, onFinished = navigation :: pop, )

Slide 135

Slide 135 text

Navigation App Root View List Details

Slide 136

Slide 136 text

@Composable fun ImagesAppCommon(component: RootComponent) { Children( stack = component.stack ) { when (val child = it.instance) { is ListChild -> ListContent(component = child.component) is DetailsChild -> DetailsContent(component = child.component) } } } App Architecture

Slide 137

Slide 137 text

@Composable fun ImagesAppCommon(component: RootComponent) { Children( stack = component.stack ) { when (val child = it.instance) { is ListChild -> ListContent(component = child.component) is DetailsChild -> DetailsContent(component = child.component) } } } App Architecture

Slide 138

Slide 138 text

class MainActivity : AppCompatActivity() { 
 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val root = DefaultRootComponent( componentContext = defaultComponentContext(), ) setContent { MaterialTheme { Surface { RootContent(component = root, modifier = Modifier.fillMaxSize()) } } } App Architecture

Slide 139

Slide 139 text

class MainActivity : AppCompatActivity() { 
 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val root = DefaultRootComponent( componentContext = defaultComponentContext(), ) setContent { MaterialTheme { Surface { RootContent(component = root, modifier = Modifier.fillMaxSize()) } } } App Architecture

Slide 140

Slide 140 text

class MainActivity : AppCompatActivity() { 
 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val root = DefaultRootComponent( componentContext = defaultComponentContext(), ) setContent { MaterialTheme { Surface { ImageAppCommon(component = root) } } } App Architecture

Slide 141

Slide 141 text

class AppDelegate: NSObject, UIApplicationDelegate { let rootHolder: RootHolder = RootHolder() } App Architecture

Slide 142

Slide 142 text

@main struct app_iosApp: App { 
 var rootHolder: RootHolder { appDelegate.rootHolder } var body: some Scene { WindowGroup { ComposeView(rootHolder.root) } } } App Architecture

Slide 143

Slide 143 text

Decompose ● Lifecycle Aware Components ● Back stack management

Slide 144

Slide 144 text

Building Multiplatform Apps with Compose ● Setup Project ● Share Compose UI ● SwiftUI & Compose Interop ● Architecture & Navigation

Slide 145

Slide 145 text

Thank You! www.codingwithmohit.com @heyitsmohit@androiddev.social