Slide 1

Slide 1 text

Mohit Sarveiya Jetpack Compose vs SwiftUI @heyitsmohit

Slide 2

Slide 2 text

Jetpack Compose vs SwiftUI ● Concurrency ● Managing State ● Navigation

Slide 3

Slide 3 text

Benefits ● Understand different approaches in Compose & SwiftUI ● Collaborate with iOS Dev on architecture.

Slide 4

Slide 4 text

Building Android & iOS App Example App

Slide 5

Slide 5 text

iOS Android

Slide 6

Slide 6 text

• Display Images • Search

Slide 7

Slide 7 text

• Like an Image • Display location, tags

Slide 8

Slide 8 text

Jetpack Compose ● MutableStateOf ● Remember ● Side Effects ● @State ● @ObservedObject ● @Published SwiftUI

Slide 9

Slide 9 text

Jetpack Compose Views, Managing State

Slide 10

Slide 10 text

• Display Images • Search

Slide 11

Slide 11 text

Images List Screen View Model State

Slide 12

Slide 12 text

Images List Screen View Model State

Slide 13

Slide 13 text

val uiState = MutableStateFlow()

Slide 14

Slide 14 text

val uiState = MutableStateFlow()

Slide 15

Slide 15 text

sealed class ImagesUiState { 
 
 object Loading: ImagesUiState() }

Slide 16

Slide 16 text

sealed class ImagesUiState { 
 data class Success( val images: List ): ImagesUiState() }

Slide 17

Slide 17 text

sealed class ImagesUiState { 
 data class Error( val errorMessage: String ): ImagesUiState() }

Slide 18

Slide 18 text

val uiState = MutableStateFlow(ImageUiState.Loading)

Slide 19

Slide 19 text

Images List Screen View Model State

Slide 20

Slide 20 text

init { viewModelScope.launch { val images = repository.getImages() _uiState.value = ImagesUiState.Success(images = images) } }

Slide 21

Slide 21 text

init { viewModelScope.launch { val images = repository.getImages() _uiState.value = ImagesUiState.Success(images = images) } }

Slide 22

Slide 22 text

init { viewModelScope.launch { val images = repository.getImages() _uiState.value = ImagesUiState.Success(images = images) } }

Slide 23

Slide 23 text

init { viewModelScope.launch { val images = repository.getImages() uiState.value = ImagesUiState.Success(images = images) } }

Slide 24

Slide 24 text

init { viewModelScope.launch { val images = repository.getImages() uiState.value = ImagesUiState.Success(images = images) } }

Slide 25

Slide 25 text

Images List Screen View Model State

Slide 26

Slide 26 text

@Composable fun ImagesListScreen() { )

Slide 27

Slide 27 text

Images List Screen View Model State

Slide 28

Slide 28 text

Streams of data ● LiveData -> observeAsState ● Flow -> collectAsState ● Observable -> subscribeAsState

Slide 29

Slide 29 text

@Composable fun ImagesListScreen() { val uiState = uiState.collectAsState().value )

Slide 30

Slide 30 text

@Composable fun ImagesListScreen() { when (uiState) { } )

Slide 31

Slide 31 text

@Composable fun ImagesListScreen() { when (uiState) { ImagesUiState.Loading ImagesUiState.Success 
 ImagesUiState.Error } )

Slide 32

Slide 32 text

ImagesUiState.Loading -> { CircularProgressIndicator() }

Slide 33

Slide 33 text

ImagesUiState.Loading -> { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() } }

Slide 34

Slide 34 text

ImagesUiState.Loading -> { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() } }

Slide 35

Slide 35 text


 Grid Search

Slide 36

Slide 36 text

ImagesUiState.Success -> { }

Slide 37

Slide 37 text

ImagesUiState.Success -> { Column { } }

Slide 38

Slide 38 text

Column { SearchView { } LazyVerticalGrid() { } }

Slide 39

Slide 39 text

Column { SearchView { } LazyVerticalGrid() { } }

Slide 40

Slide 40 text

LazyVerticalGrid( cells = GridCells.Fixed(3) ) { }

Slide 41

Slide 41 text

LazyVerticalGrid( cells = GridCells.Fixed(3) ) { items(uiState.images) { 
 
 
 } }

Slide 42

Slide 42 text

LazyVerticalGrid( cells = GridCells.Fixed(3) ) { items(uiState.images) { Image( painter = rememberImagePainter(it.url) ) }

Slide 43

Slide 43 text

Image( painter = rememberImagePainter(it.url), modifier = Modifier .size(128.dp) .padding(8.dp) .contentScale = ContentScale.Crop )

Slide 44

Slide 44 text

Image( painter = rememberImagePainter(it.url), modifier = Modifier .size(128.dp) .padding(8.dp) .contentScale = ContentScale.Crop )

Slide 45

Slide 45 text

• Search for images • Update grid with results

Slide 46

Slide 46 text

Search View View Model Event

Slide 47

Slide 47 text

Search View View Model State

Slide 48

Slide 48 text


 Grid Search

Slide 49

Slide 49 text

Search View ● Remember and update search text ● Perform Search

Slide 50

Slide 50 text

@Composable fun SearchView() { OutlinedTextField( value = , onValueChange = { } ) }

Slide 51

Slide 51 text

OutlinedTextField( value = , onValueChange = { } )

Slide 52

Slide 52 text

OutlinedTextField( value = , onValueChange = { } )

Slide 53

Slide 53 text

val queryState = remember { mutableStateOf("") }

Slide 54

Slide 54 text

val queryState = remember { mutableStateOf("") } OutlinedTextField( value = queryState.value, onValueChange = { queryState.value = it } )

Slide 55

Slide 55 text

val queryState = remember { mutableStateOf("") } OutlinedTextField( value = queryState.value, onValueChange = { queryState.value = it } )

Slide 56

Slide 56 text

Search View ● Remember and update search text ● Perform Search

Slide 57

Slide 57 text

Search View View Model Event

Slide 58

Slide 58 text

@Composable fun SearchView( onSearch: (String) -> Unit ) { }

Slide 59

Slide 59 text

OutlinedTextField( keyboardActions = KeyboardActions( onDone = { onSearch(queryState.value) } ) )

Slide 60

Slide 60 text

OutlinedTextField( keyboardActions = KeyboardActions( onDone = { onSearch(queryState.value) } ) )

Slide 61

Slide 61 text

Column { SearchView { imagesViewModel.searchImages(query) } }

Slide 62

Slide 62 text

class ImagesViewModel() fun searchImages(query: String) { } }

Slide 63

Slide 63 text

viewModelScope.launch { uiState.value = ImagesUiState.Loading val images = repository.searchImages(query = query) uiState.value = ImagesUiState.Success(images = images) }

Slide 64

Slide 64 text

viewModelScope.launch { uiState.value = ImagesUiState.Loading val images = repo.searchImages(query = query) uiState.value = ImagesUiState.Success(images = images) }

Slide 65

Slide 65 text

viewModelScope.launch { uiState.value = ImagesUiState.Loading val images = repo.searchImages(query = query) uiState.value = ImagesUiState.Success(images = images) }

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

• Display location, tags • Like an Image

Slide 68

Slide 68 text

@Composable fun ImageDetailsScreen() { }

Slide 69

Slide 69 text

@Composable fun ImageDetailsScreen(imageId: Int?) { }

Slide 70

Slide 70 text

Image Details Screen View Model Side Effect

Slide 71

Slide 71 text

@Composable fun ImageDetailsScreen(imageId: Int?) { }

Slide 72

Slide 72 text

LaunchedEffect(key1 = imageId) { }

Slide 73

Slide 73 text

LaunchedEffect(key1 = imageId) { viewModel.getImage(imageId) }

Slide 74

Slide 74 text

val uiState = uiState.collectAsState().value

Slide 75

Slide 75 text

when (uiState) { ImageDetailsUiState.Loading ImageDetailsUiState.Success ImageDetailsUiState.Error }

Slide 76

Slide 76 text

• Display location, tags • Like an Image

Slide 77

Slide 77 text


 Image Toolbar

Slide 78

Slide 78 text

Scaffold( topBar = { 
 }, content = { } )

Slide 79

Slide 79 text

Scaffold( topBar = { TopAppBar( title = { Text(text = "Images") } } )

Slide 80

Slide 80 text

Scaffold( actions = { Icon( Icons.Default.FavoriteBorder ) } )

Slide 81

Slide 81 text

• Like Image -> • Unlike Image ->

Slide 82

Slide 82 text

class ImageDetailsViewModel { var isLiked = mutableStateOf(false) }

Slide 83

Slide 83 text

fun likeImage(imageId: Int) { }

Slide 84

Slide 84 text

fun likeImage(imageId: Int) { isLiked.value = true }

Slide 85

Slide 85 text

fun likeImage(imageId: Int) { isLiked.value = true viewModelScope.launch { val result = repo.likeImage(imageId) result.onFailure { isLiked.value = false } } }

Slide 86

Slide 86 text

fun likeImage(imageId: Int) { isLiked.value = true viewModelScope.launch { val result = repo.likeImage(imageId) result.onFailure { isLiked.value = false } } }

Slide 87

Slide 87 text

fun likeImage(imageId: Int) { isLiked.value = true viewModelScope.launch { val result = repo.likeImage(imageId) result.onFailure { isLiked.value = false } } }

Slide 88

Slide 88 text

val isLikeState = remember { viewModel.isLiked }

Slide 89

Slide 89 text

Icon( if (isLikeState.value.not()) Icons.Default.FavoriteBorder else Icons.Filled.Favorite )

Slide 90

Slide 90 text

Icon( modifier = Modifier.clickable { viewModel.likeImage(imageId) } )

Slide 91

Slide 91 text

• Display location, tags • Like an Image

Slide 92

Slide 92 text

No content

Slide 93

Slide 93 text

Nav Host Images List View Images Details View

Slide 94

Slide 94 text

Images List View Images Details View

Slide 95

Slide 95 text

val navController = rememberNavController()

Slide 96

Slide 96 text

val navController = rememberNavController() NavHost(navController) { }

Slide 97

Slide 97 text

NavHost(navController) { 
 composable("images_list") { } composable(“images_details") { } }

Slide 98

Slide 98 text

NavHost(navController) { 
 composable("images_list") { ImagesListScreen() } composable(“images_details") { ImageDetailsScreen() } }

Slide 99

Slide 99 text

composable("images_list") { ImagesListScreen { } }

Slide 100

Slide 100 text

composable("images_list") { ImagesListScreen { navController.navigate(“images_details/{imageId}“) } }

Slide 101

Slide 101 text

composable(“images_details") { ImagesDetailsScreen( backStackEntry.arguments ?. getString(“imageId") ?. toInt(), ... ) }

Slide 102

Slide 102 text

No content

Slide 103

Slide 103 text

Jetpack Compose ● MutableStateOf ● Remember ● Side Effects ● @State ● @ObservedObject ● @Published SwiftUI

Slide 104

Slide 104 text

Jetpack Compose ● MutableStateOf ● Remember ● Side Effects

Slide 105

Slide 105 text

SwiftUI Views, Concurrency, Managing State, Navigation

Slide 106

Slide 106 text

• Display Images • Search

Slide 107

Slide 107 text

Observed Object View Observe

Slide 108

Slide 108 text

Observed Object View Update

Slide 109

Slide 109 text

class ImagesRepository: ObservableObject { }

Slide 110

Slide 110 text

class ImagesRepository: ObservableObject { @Published var images: [ImageData] = [] }

Slide 111

Slide 111 text

Swift Concurrency ● Async/Await ● Tasks

Slide 112

Slide 112 text

func getImages() async { }

Slide 113

Slide 113 text

func getImages() async { images = apiService.getImages() }

Slide 114

Slide 114 text

func getImages() async { images = apiService.getImages() } Publishing changes from background threads is not allowed; make sure to publish values from the main thread !

Slide 115

Slide 115 text

@MainActor func getImages() async { images = apiService.getImages() }

Slide 116

Slide 116 text

Observed Object View Observe

Slide 117

Slide 117 text

struct ContentView: View { }

Slide 118

Slide 118 text

struct ContentView: View { }

Slide 119

Slide 119 text

struct ContentView: View { var body: some View { } }

Slide 120

Slide 120 text

struct ContentView: View { @StateObject var repository = ImagesRepository() }

Slide 121

Slide 121 text

struct ContentView: View { @StateObject var repository = ImagesRepository() } Best Practice Use StateObject when creating objects.

Slide 122

Slide 122 text

struct ContentView: View { 
 @StateObject var repository = ImagesRepository() 
 }.task { }

Slide 123

Slide 123 text

struct ContentView: View { 
 @StateObject var repository = ImagesRepository() 
 }.task { await repository.getImages() }

Slide 124

Slide 124 text

struct ContentView: View { 
 @State var showProgressBar = true 
 }

Slide 125

Slide 125 text

ZStack { }

Slide 126

Slide 126 text

ZStack { if (showProgressBar) { } }

Slide 127

Slide 127 text

ZStack { if (showProgressBar) { ProgressView() .progressViewStyle( CircularProgressViewStyle(tint: .blue) ) } }

Slide 128

Slide 128 text

struct ContentView: View { 
 @State var showProgressBar = true 
 }.task { await repository.getImages() showProgressBar = false }

Slide 129

Slide 129 text

struct ContentView: View { 


Slide 130

Slide 130 text

• Display Images • Search • Like an image

Slide 131

Slide 131 text

LazyVGrid(spacing: 10) { }

Slide 132

Slide 132 text

LazyVGrid(spacing: 10) { ForEach(repo.images, id: \.id) { item in } }

Slide 133

Slide 133 text

LazyVGrid(spacing: 10) { ForEach(repo.images, id: \.id) { item in Image(item.url) .resizable() .frame(minWidth: 0, maxWidth: .infinity) } }

Slide 134

Slide 134 text

• Display Images • Search • Like an image

Slide 135

Slide 135 text

@State var searchText = ""

Slide 136

Slide 136 text

LazyVGrid(spacing: 10) { }.searchable()

Slide 137

Slide 137 text

LazyVGrid(spacing: 10) { }.searchable(text: $searchText)

Slide 138

Slide 138 text

LazyVGrid(spacing: 10) { }.searchable(text: $searchText) Two way binding

Slide 139

Slide 139 text

@State var searchText = “" var searchResults: [ImageData] { if searchText.isEmpty { return repository.images } else { return filterImages(searchText) }

Slide 140

Slide 140 text

• Display Images • Search • Like an image

Slide 141

Slide 141 text

struct ImageDetailsView: View { let imageData: ImageData }

Slide 142

Slide 142 text

struct ImageDetailsView: View { @State var isLiked: Bool = false }

Slide 143

Slide 143 text

struct ImageDetailsView: View { 
 @State var isLiked: Bool = false 
 var body: some View { 
 }.onAppear { } }

Slide 144

Slide 144 text

struct ImageDetailsView: View { 
 @State var isLiked: Bool = false 
 var body: some View { 
 }.onAppear { isLiked = imageData.isLiked } }

Slide 145

Slide 145 text

struct ImageDetailsView: View { 


Slide 146

Slide 146 text

Button(action: { isLiked.toggle() }) { Image(systemName: isLiked ? "heart.fill" : "heart") }

Slide 147

Slide 147 text

struct ImageDetailsView: View { 
 @State var isLiked: Bool = false 
 var body: some View { 
 }.onChange(of: isLiked) { } }

Slide 148

Slide 148 text

@State var likeTask: Task? = nil

Slide 149

Slide 149 text

.onChange(of: isLiked) { newValue in if (newValue) { likeTask ?. cancel() likeTask = Task { await repository.likeImage(id: imageData.id) } } }

Slide 150

Slide 150 text

.onChange(of: isLiked) { newValue in if (newValue) { likeTask ?. cancel() likeTask = Task { await repository.likeImage(id: imageData.id) } } }

Slide 151

Slide 151 text

.onChange(of: isLiked) { newValue in if (newValue) { likeTask ?. cancel() likeTask = Task { await repository.likeImage(id: imageData.id) } } }

Slide 152

Slide 152 text

• Display Images • Search • Like an image

Slide 153

Slide 153 text

No content

Slide 154

Slide 154 text

NavigationView { }

Slide 155

Slide 155 text

NavigationView { NavigationLink( destination: ImageDetailsView( repository: repository, imageData: item ) ) }

Slide 156

Slide 156 text

No content

Slide 157

Slide 157 text

toolbar { ToolbarItem { Button(action: { 
 }) { Image(systemName: "square.grid.2x2") } } }

Slide 158

Slide 158 text

Jetpack Compose ● MutableStateOf ● Remember ● Side Effects ● @State ● @ObservedObject ● @Published SwiftUI

Slide 159

Slide 159 text

Resources ● github.com/msya/ImagesComposeApp ● github.com/msya/ImagesSwiftUIApp

Slide 160

Slide 160 text

Thank You! www.codingwithmohit.com @heyitsmohit