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

Building Multiplatform Apps with Compose

Mohit S
April 25, 2023

Building Multiplatform Apps with Compose

Mohit S

April 25, 2023
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Building Multiplatform Apps with Compose • Setup Project • Share

    Compose UI • SwiftUI & Compose Interop • Architecture & Navigation
  2. Compose Multiplatform • Android (via Jetpack Compose) • Desktop (Windows,

    Mac OS, Linux) • Web (Experimental) • iOS (Alpha)
  3. plugins { kotlin("multiplatform") } val commonMain by getting { dependencies

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

    { implementation(compose.ui) implementation(compose.foundation) implementation(compose.material) implementation(compose.runtime) } } Shared Module
  5. 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 ) }
  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. UI Structure class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState:

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

    Bundle?) { super.onCreate(savedInstanceState) setContent { MainAndroid() } } }
  9. UI Structure @main struct iOSApp: App { 
 var body:

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

    some Scene { WindowGroup { ContentView() } } 
 }
  11. UI Structure struct ComposeView: UIViewControllerRepresentable { 
 func makeUIViewController(context: Context)

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

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

    -> UIViewController { let controller = Main_iosKt.MainiOS() return controller } }
  14. UI Structure struct ContentView: View { 
 var body: some

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

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

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

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

    View { ZStack { ComposeView() ...
 } } 
 } UIWindowScene UIWindow ComposeWindow SkikkoUIView View Hierarchy
  19. @Composable fun ImagesList(images: List<ImageData>) { Column { LazyVerticalGrid( columns =

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

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

    GridCells.Fixed(3) ) { items(images) { SplashImage( imageData = it ) } } } } UI Structure
  22. @Composable fun SplashImage(imageData: ImageData) { val imageProvider = LocalImageProvider.current var

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

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

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

    imageBitmap by remember(imageData) { 
 mutableStateOf<ImageBitmap?>(null) 
 } LaunchedEffect(imageData) { imageBitmap = imageProvider.getImageBitmap(imageData) } imageBitmap ?. let { Image( bitmap = bitmap, 
 ... ) } } UI Structure
  26. App Architecture sealed class ImagesListUiState { object Loading: ImagesListUiState() data

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

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

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

    class Success( val images: List<ImageData> ): ImagesListUiState() data class Error( val errorMessage: String ): ImagesListUiState() }
  30. App Architecture class ImagesListViewModel { init { ... } val

    state = MutableStateFlow<ImagesListUiState>(ImagesListUiState.Loading) val viewModelScope = CoroutineScope(Dispatchers.Main) } init } {
  31. 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")) } }
  32. App Architecture @Composable fun ImagesAppCommon() { Scaffold( topBar = {

    ... }, content = { } ) } val uiState by viewModel.state.collectAsState() ImagesListScreen(uiState)
  33. App Architecture @Composable fun ImagesListScreen(uiState: UIState) { when (uiState) {

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

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

    ImagesListUiState.Loading -> CircularProgressIndicator() is ImagesListUiState.Success -> ImagesList(uiState.images) is ImagesListUiState.Error -> } }
  36. App Architecture sealed class Screen { object List : Screen()

    data class Details(val imageId: String) : Screen() }
  37. App Architecture @Composable fun ImagesAppCommon() { var screenState by remember

    { mutableStateOf<Screen>(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 } ) } }
  38. App Architecture @Composable fun ImagesAppCommon() { var screenState by remember

    { mutableStateOf<Screen>(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 } ) } }
  39. App Architecture @Composable fun ImagesAppCommon() { var screenState by remember

    { mutableStateOf<Screen>(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 } ) } }
  40. App Architecture @Composable fun ImagesAppCommon() { var screenState by remember

    { mutableStateOf<Screen>(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 } ) } }
  41. Decompose class ImagesListComponent( componentContext: ComponentContext, val onImageSelected: (imageId: String) ->

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

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

    Unit, ) : ListComponent { 
 override val uiState: Value<ListComponent.UiState> = MutableValue(UiState.Loading) override fun onItemClicked(imageId: String) { onImageSelected(imageId) } }
  44. @Composable fun ImagesList( component: ListComponent, images: List<ImageData>, onImageClicked: (Int) ->

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

    Unit ) { 
 
 val uiState by component.uiState.subscribeAsState() } Decompose
  46. interface RootComponent { val stack: Value<ChildStack <* , Child >>

    sealed class Child { class ListChild(val component: ListComponent) : Child() class DetailsChild(val component: DetailsComponent) : Child() } } App Architecture
  47. interface RootComponent { val stack: Value<ChildStack <* , Child >>

    sealed class Child { class ListChild(val component: ListComponent) : Child() class DetailsChild(val component: DetailsComponent) : Child() } } App Architecture
  48. interface RootComponent { val stack: Value<ChildStack <* , Child >>

    sealed class Child { class ListChild(val component: ListComponent) : Child() class DetailsChild(val component: DetailsComponent) : Child() } } App Architecture
  49. class DefaultRootComponent( ... ): RootComponent { @Parcelize sealed interface Config

    : Parcelable { object List : Config data class Details(val item: String) : Config } } App Architecture
  50. class DefaultRootComponent( ... ): RootComponent { 
 val navigation =

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

    StackNavigation<Config>() 
 @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, )
  52. class DefaultRootComponent( ... ): RootComponent { 
 val navigation =

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

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

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

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

    detailsComponent(): DetailsComponent = ImageDetailsComponent( image = config.image, onFinished = navigation :: pop, )
  57. @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
  58. @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
  59. 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
  60. 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
  61. class MainActivity : AppCompatActivity() { 
 override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) val root = DefaultRootComponent( componentContext = defaultComponentContext(), ) setContent { MaterialTheme { Surface { ImageAppCommon(component = root) } } } App Architecture
  62. @main struct app_iosApp: App { 
 var rootHolder: RootHolder {

    appDelegate.rootHolder } var body: some Scene { WindowGroup { ComposeView(rootHolder.root) } } } App Architecture
  63. Building Multiplatform Apps with Compose • Setup Project • Share

    Compose UI • SwiftUI & Compose Interop • Architecture & Navigation