Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Jetpack Compose vs SwiftUI (Android Worldwide 2...
Search
Mohit S
October 26, 2021
Programming
2
920
Jetpack Compose vs SwiftUI (Android Worldwide 2021)
Mohit S
October 26, 2021
Tweet
Share
More Decks by Mohit S
See All by Mohit S
Guide to Improving Compose Performance
heyitsmohit
0
160
Building Shared UIs across Platforms with Compose
heyitsmohit
1
550
Building Multiplatform Apps with Compose
heyitsmohit
2
420
Building StateFlows with Jetpack Compose
heyitsmohit
6
1.7k
Building Android Testing Infrastructure
heyitsmohit
1
390
Migrating to Kotlin State & Shared Flows
heyitsmohit
1
690
Using Square Workflow for Android & iOS
heyitsmohit
1
380
Building Android Infrastructure Teams at Scale
heyitsmohit
3
290
Strategies for Migrating to Jetpack Compose
heyitsmohit
2
510
Other Decks in Programming
See All in Programming
数十万行のプロジェクトを Scala 2から3に完全移行した
xuwei_k
0
260
14 Years of iOS: Lessons and Key Points
seyfoyun
1
770
良いユニットテストを書こう
mototakatsu
4
1.8k
競技プログラミングへのお誘い@阪大BOOSTセミナー
kotamanegi
0
350
CSC509 Lecture 14
javiergs
PRO
0
130
テストケースの名前はどうつけるべきか?
orgachem
PRO
0
130
HTTP compression in PHP and Symfony apps
dunglas
2
1.7k
Semantic Kernelのネイティブプラグインで知識拡張をしてみる
tomokusaba
0
180
From Translations to Multi Dimension Entities
alexanderschranz
2
130
17年周年のWebアプリケーションにTanStack Queryを導入する / Implementing TanStack Query in a 17th Anniversary Web Application
saitolume
0
250
プロダクトの品質に コミットする / Commit to Product Quality
pekepek
2
770
これが俺の”自分戦略” プロセスを楽しんでいこう! - Developers CAREER Boost 2024
niftycorp
PRO
0
190
Featured
See All Featured
A designer walks into a library…
pauljervisheath
204
24k
Site-Speed That Sticks
csswizardry
2
190
Designing Dashboards & Data Visualisations in Web Apps
destraynor
229
52k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
127
18k
Build your cross-platform service in a week with App Engine
jlugia
229
18k
We Have a Design System, Now What?
morganepeng
51
7.3k
The Invisible Side of Design
smashingmag
298
50k
Git: the NoSQL Database
bkeepers
PRO
427
64k
Build The Right Thing And Hit Your Dates
maggiecrowley
33
2.4k
Gamification - CAS2011
davidbonilla
80
5.1k
Raft: Consensus for Rubyists
vanstee
137
6.7k
YesSQL, Process and Tooling at Scale
rocio
169
14k
Transcript
Mohit Sarveiya Jetpack Compose vs SwiftUI @heyitsmohit
Jetpack Compose vs SwiftUI • Concurrency • Managing State •
Navigation
Benefits • Understand different approaches in Compose & SwiftUI •
Collaborate with iOS Dev on architecture.
Building Android & iOS App Example App
iOS Android
• Display Images • Search
• Like an Image • Display location, tags
Jetpack Compose • MutableStateOf • Remember • Side Effects •
@State • @ObservedObject • @Published SwiftUI
Jetpack Compose Views, Managing State
• Display Images • Search
Images List Screen View Model State
Images List Screen View Model State
val uiState = MutableStateFlow<ImagesUiState>()
val uiState = MutableStateFlow<ImagesUiState>()
sealed class ImagesUiState { object Loading: ImagesUiState() }
sealed class ImagesUiState { data class Success( val images:
List<ImageData> ): ImagesUiState() }
sealed class ImagesUiState { data class Error( val errorMessage:
String ): ImagesUiState() }
val uiState = MutableStateFlow(ImageUiState.Loading)
Images List Screen View Model State
init { viewModelScope.launch { val images = repository.getImages() _uiState.value =
ImagesUiState.Success(images = images) } }
init { viewModelScope.launch { val images = repository.getImages() _uiState.value =
ImagesUiState.Success(images = images) } }
init { viewModelScope.launch { val images = repository.getImages() _uiState.value =
ImagesUiState.Success(images = images) } }
init { viewModelScope.launch { val images = repository.getImages() uiState.value =
ImagesUiState.Success(images = images) } }
init { viewModelScope.launch { val images = repository.getImages() uiState.value =
ImagesUiState.Success(images = images) } }
Images List Screen View Model State
@Composable fun ImagesListScreen() { )
Images List Screen View Model State
Streams of data • LiveData -> observeAsState • Flow ->
collectAsState • Observable -> subscribeAsState
@Composable fun ImagesListScreen() { val uiState = uiState.collectAsState().value )
@Composable fun ImagesListScreen() { when (uiState) { } )
@Composable fun ImagesListScreen() { when (uiState) { ImagesUiState.Loading ImagesUiState.Success
ImagesUiState.Error } )
ImagesUiState.Loading -> { CircularProgressIndicator() }
ImagesUiState.Loading -> { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() } }
ImagesUiState.Loading -> { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() } }
Grid Search
ImagesUiState.Success -> { }
ImagesUiState.Success -> { Column { } }
Column { SearchView { } LazyVerticalGrid() { } }
Column { SearchView { } LazyVerticalGrid() { } }
LazyVerticalGrid( cells = GridCells.Fixed(3) ) { }
LazyVerticalGrid( cells = GridCells.Fixed(3) ) { items(uiState.images) {
} }
LazyVerticalGrid( cells = GridCells.Fixed(3) ) { items(uiState.images) { Image( painter
= rememberImagePainter(it.url) ) }
Image( painter = rememberImagePainter(it.url), modifier = Modifier .size(128.dp) .padding(8.dp) .contentScale
= ContentScale.Crop )
Image( painter = rememberImagePainter(it.url), modifier = Modifier .size(128.dp) .padding(8.dp) .contentScale
= ContentScale.Crop )
• Search for images • Update grid with results
Search View View Model Event
Search View View Model State
Grid Search
Search View • Remember and update search text • Perform
Search
@Composable fun SearchView() { OutlinedTextField( value = , onValueChange =
{ } ) }
OutlinedTextField( value = , onValueChange = { } )
OutlinedTextField( value = , onValueChange = { } )
val queryState = remember { mutableStateOf("") }
val queryState = remember { mutableStateOf("") } OutlinedTextField( value =
queryState.value, onValueChange = { queryState.value = it } )
val queryState = remember { mutableStateOf("") } OutlinedTextField( value =
queryState.value, onValueChange = { queryState.value = it } )
Search View • Remember and update search text • Perform
Search
Search View View Model Event
@Composable fun SearchView( onSearch: (String) -> Unit ) { }
OutlinedTextField( keyboardActions = KeyboardActions( onDone = { onSearch(queryState.value) } )
)
OutlinedTextField( keyboardActions = KeyboardActions( onDone = { onSearch(queryState.value) } )
)
Column { SearchView { imagesViewModel.searchImages(query) } }
class ImagesViewModel() fun searchImages(query: String) { } }
viewModelScope.launch { uiState.value = ImagesUiState.Loading val images = repository.searchImages(query =
query) uiState.value = ImagesUiState.Success(images = images) }
viewModelScope.launch { uiState.value = ImagesUiState.Loading val images = repo.searchImages(query =
query) uiState.value = ImagesUiState.Success(images = images) }
viewModelScope.launch { uiState.value = ImagesUiState.Loading val images = repo.searchImages(query =
query) uiState.value = ImagesUiState.Success(images = images) }
None
• Display location, tags • Like an Image
@Composable fun ImageDetailsScreen() { }
@Composable fun ImageDetailsScreen(imageId: Int?) { }
Image Details Screen View Model Side Effect
@Composable fun ImageDetailsScreen(imageId: Int?) { }
LaunchedEffect(key1 = imageId) { }
LaunchedEffect(key1 = imageId) { viewModel.getImage(imageId) }
val uiState = uiState.collectAsState().value
when (uiState) { ImageDetailsUiState.Loading ImageDetailsUiState.Success ImageDetailsUiState.Error }
• Display location, tags • Like an Image
Image Toolbar
Scaffold( topBar = { }, content = { }
)
Scaffold( topBar = { TopAppBar( title = { Text(text =
"Images") } } )
Scaffold( actions = { Icon( Icons.Default.FavoriteBorder ) } )
• Like Image -> • Unlike Image ->
class ImageDetailsViewModel { var isLiked = mutableStateOf(false) }
fun likeImage(imageId: Int) { }
fun likeImage(imageId: Int) { isLiked.value = true }
fun likeImage(imageId: Int) { isLiked.value = true viewModelScope.launch { val
result = repo.likeImage(imageId) result.onFailure { isLiked.value = false } } }
fun likeImage(imageId: Int) { isLiked.value = true viewModelScope.launch { val
result = repo.likeImage(imageId) result.onFailure { isLiked.value = false } } }
fun likeImage(imageId: Int) { isLiked.value = true viewModelScope.launch { val
result = repo.likeImage(imageId) result.onFailure { isLiked.value = false } } }
val isLikeState = remember { viewModel.isLiked }
Icon( if (isLikeState.value.not()) Icons.Default.FavoriteBorder else Icons.Filled.Favorite )
Icon( modifier = Modifier.clickable { viewModel.likeImage(imageId) } )
• Display location, tags • Like an Image
None
Nav Host Images List View Images Details View
Images List View Images Details View
val navController = rememberNavController()
val navController = rememberNavController() NavHost(navController) { }
NavHost(navController) { composable("images_list") { } composable(“images_details") { } }
NavHost(navController) { composable("images_list") { ImagesListScreen() } composable(“images_details") { ImageDetailsScreen()
} }
composable("images_list") { ImagesListScreen { } }
composable("images_list") { ImagesListScreen { navController.navigate(“images_details/{imageId}“) } }
composable(“images_details") { ImagesDetailsScreen( backStackEntry.arguments ?. getString(“imageId") ?. toInt(), ... )
}
None
Jetpack Compose • MutableStateOf • Remember • Side Effects •
@State • @ObservedObject • @Published SwiftUI
Jetpack Compose • MutableStateOf • Remember • Side Effects
SwiftUI Views, Concurrency, Managing State, Navigation
• Display Images • Search
Observed Object View Observe
Observed Object View Update
class ImagesRepository: ObservableObject { }
class ImagesRepository: ObservableObject { @Published var images: [ImageData] = []
}
Swift Concurrency • Async/Await • Tasks
func getImages() async { }
func getImages() async { images = apiService.getImages() }
func getImages() async { images = apiService.getImages() } Publishing changes
from background threads is not allowed; make sure to publish values from the main thread !
@MainActor func getImages() async { images = apiService.getImages() }
Observed Object View Observe
struct ContentView: View { }
struct ContentView: View { }
struct ContentView: View { var body: some View { }
}
struct ContentView: View { @StateObject var repository = ImagesRepository() }
struct ContentView: View { @StateObject var repository = ImagesRepository() }
Best Practice Use StateObject when creating objects.
struct ContentView: View { @StateObject var repository = ImagesRepository()
}.task { }
struct ContentView: View { @StateObject var repository = ImagesRepository()
}.task { await repository.getImages() }
struct ContentView: View { @State var showProgressBar = true
}
ZStack { }
ZStack { if (showProgressBar) { } }
ZStack { if (showProgressBar) { ProgressView() .progressViewStyle( CircularProgressViewStyle(tint: .blue) )
} }
struct ContentView: View { @State var showProgressBar = true
}.task { await repository.getImages() showProgressBar = false }
struct ContentView: View {
• Display Images • Search • Like an image
LazyVGrid(spacing: 10) { }
LazyVGrid(spacing: 10) { ForEach(repo.images, id: \.id) { item in }
}
LazyVGrid(spacing: 10) { ForEach(repo.images, id: \.id) { item in Image(item.url)
.resizable() .frame(minWidth: 0, maxWidth: .infinity) } }
• Display Images • Search • Like an image
@State var searchText = ""
LazyVGrid(spacing: 10) { }.searchable()
LazyVGrid(spacing: 10) { }.searchable(text: $searchText)
LazyVGrid(spacing: 10) { }.searchable(text: $searchText) Two way binding
@State var searchText = “" var searchResults: [ImageData] { if
searchText.isEmpty { return repository.images } else { return filterImages(searchText) }
• Display Images • Search • Like an image
struct ImageDetailsView: View { let imageData: ImageData }
struct ImageDetailsView: View { @State var isLiked: Bool = false
}
struct ImageDetailsView: View { @State var isLiked: Bool =
false var body: some View { }.onAppear { } }
struct ImageDetailsView: View { @State var isLiked: Bool =
false var body: some View { }.onAppear { isLiked = imageData.isLiked } }
struct ImageDetailsView: View {
Button(action: { isLiked.toggle() }) { Image(systemName: isLiked ? "heart.fill" :
"heart") }
struct ImageDetailsView: View { @State var isLiked: Bool =
false var body: some View { }.onChange(of: isLiked) { } }
@State var likeTask: Task<Void, Never>? = nil
.onChange(of: isLiked) { newValue in if (newValue) { likeTask ?.
cancel() likeTask = Task { await repository.likeImage(id: imageData.id) } } }
.onChange(of: isLiked) { newValue in if (newValue) { likeTask ?.
cancel() likeTask = Task { await repository.likeImage(id: imageData.id) } } }
.onChange(of: isLiked) { newValue in if (newValue) { likeTask ?.
cancel() likeTask = Task { await repository.likeImage(id: imageData.id) } } }
• Display Images • Search • Like an image
None
NavigationView { }
NavigationView { NavigationLink( destination: ImageDetailsView( repository: repository, imageData: item )
) }
None
toolbar { ToolbarItem { Button(action: { }) { Image(systemName:
"square.grid.2x2") } } }
Jetpack Compose • MutableStateOf • Remember • Side Effects •
@State • @ObservedObject • @Published SwiftUI
Resources • github.com/msya/ImagesComposeApp • github.com/msya/ImagesSwiftUIApp
Thank You! www.codingwithmohit.com @heyitsmohit