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

The ever increasing convergence of native iOS a...

John O'Reilly
September 08, 2022

The ever increasing convergence of native iOS and Android development - Droidcon NYC 2022

This talk will illustrate with examples where we find ourselves today in the world of iOS and Android mobile development with the ever increasing convergence of the languages and frameworks we're using, covering for example how similar the following are.

- Swift/Kotlin
- SwiftUI/Compose
- Swift 5.5/Kotlin concurrency frameworks

John O'Reilly

September 08, 2022
Tweet

More Decks by John O'Reilly

Other Decks in Technology

Transcript

  1. The ever increasing convergence of native iOS and Android development

    John O’Reilly, Droidcon NYC 2022 @joreilly
  2. iOS and Android development convergence • Swift / Kotlin •

    SwiftUI / Compose • Use of MVVM, unidirectional data fl ow etc • Swift 5.5 / Kotlin approach to concurrency • Device/UX similarities @joreilly
  3. @joreilly Key iOS and Android Milestones - 2007: iPhone OS

    1.0 -2008 - iPhone OS 2.0 + App Store - Android 1.0 - 2014: Swift support on iOS - 2017: Kotlin support on Android -2019 - Jetpack Compose announced - SwiftUI announced/released -2021 - Jetpack Compose released - Swift 5.5 concurrency updates -2022 - Android 13, iOS 16
  4. for name in names { print(name) } names.forEach { name

    in print(name) } for name in names { if name == "Robert" { break } print(name) } for (name in names) { println(name) } names.forEach { name -> println(name) } for (name in names) { if (name == "Robert") { break } println(name) } @joreilly https://swiftbysundell.com/basics/loops/
  5. let names = ["John", "Emma", "Robert", "Julia"] 
 for name

    in names { print(name) } names.forEach { name in print(name) } for name in names { if name == "Robert" { break } print(name) } val names = arrayOf("John", "Emma", “Robert", “Julia") for (name in names) { println(name) } names.forEach { name -> println(name) } for (name in names) { if (name == "Robert") { break } println(name) } @joreilly
  6. for index in 0..<names.count { print(index, names[index]) } for index

    in names.indices { print(index, names[index]) } for (index, name) in names.enumerated() { print(index, name) } for (index in 0 until names.size) { println("$index ${names[index]}") } for (index in names.indices) { println("$index ${names[index]}") } for ((index, value) in names.withIndex()) { println("$index $value") } @joreilly Swift Kotlin
  7. let namesByCategory = [ "friends": ["John", "Emma"], "family": ["Robert", "Julia"]

    ] for (category, names) in namesByCategory { print(category, names) } for category in namesByCategory.keys { print(category) } for names in namesByCategory.values { print(names) } val namesByCategory = mapOf( "friends" to listOf("John", "Emma"), "family" to listOf("Robert", "Julia") ) for ((category, names) in namesByCategory) { println("$category $names") } for (category in namesByCategory.keys) { println(category) } for (names in namesByCategory.values) { println(names) } @joreilly Swift Kotlin
  8. names.sort() var sortedNames = names.sorted() names.reverse() var reversedNames = names.reversed()

    
 
 names.sort { (x, y) -> Bool in return x > y } names.sort() var sortedNames = names.sorted() names.reverse() var reversedNames = names.reversed() 
 
 names.sortWith(Comparator{ a, b -> b.compareTo(a) }) @joreilly Swift Kotlin https://sarunw.com/posts/di ff erent-ways-to-sort-array-of-strings-in-swift/
  9. @joreilly Declarative UI • What vs How • We declare

    what our UI should look like in particular state • Encourages use MVVM/Unidirectional Data Flow • Used in SwiftUI, Compose, React, Flutter • SwiftUI released 2019 • Compose released 2021
  10. @joreilly struct CharactersListRowView: View { let character: CharacterDetail var body:

    some View { HStack { AsyncImage(url: URL(string: character.image)) .frame(width: 50, height: 50) VStack(alignment: .leading) { Text(character.name) .font(.title3) Text("\(character.episode.count) episode(s)") .font(.footnote) } } } } @Composable fun CharactersListRowView(character: CharacterDetail) { Row { AsyncImage(model = character.image, contentDescription = character.name, contentScale = ContentScale.Fit, modifier = Modifier.size(60.dp).clip(CircleShape) ) Column { Text(character.name, style = MaterialTheme.typography.h6) Text("${character.episode.size} episode(s)”, style = MaterialTheme.typography.body2) } } } SwiftUI Compose
  11. @joreilly struct CharactersListRowView: View { let character: CharacterDetail var body:

    some View { HStack { AsyncImage(url: URL(string: character.image)) .frame(width: 50, height: 50) VStack(alignment: .leading) { Text(character.name) .font(.title3) Text("\(character.episode.count) episode(s)") .font(.footnote) } } } } @Composable fun CharactersListRowView(character: CharacterDetail) { Row { AsyncImage(model = character.image, contentDescription = character.name, contentScale = ContentScale.Fit, modifier = Modifier.size(60.dp).clip(CircleShape) ) Column { Text(character.name, style = MaterialTheme.typography.h6) Text("${character.episode.size} episode(s)”, style = MaterialTheme.typography.body2) } } } SwiftUI Compose
  12. Canvas(modifier = Modifier.fillMaxWidth()) { val blockSize = size.width / displayWidth

    for (x in 0 until displayWidth) { for (y in 0 until displayHeight) { val index = x + displayWidth * y if (screenData[index] == 1) { val xx = blockSize * x.toFloat() val yy = blockSize * y.toFloat() drawRect(Color.Black, topLeft = Offset(xx, yy), size = Size(blockSize, blockSize)) } } } } Canvas { context, size in let blockSize = size.width / CGFloat(displayWidth) for x in 0 ..< displayWidth { for y in 0 ..< displayHeight { let index = x + displayWidth * y if (screenData.get(index: Int32(index)) == 1) { let xx = blockSize * CGFloat(x) let yy = blockSize * CGFloat(y) let rect = CGRect(x: xx, y: yy, width: blockSize, height: blockSize) context.fill(Path(rect), with: .color(.black)) } } } } SwiftUI Compose https://github.com/joreilly/chip-8
  13. @joreilly struct PlayerView: View { var player: Player var body:

    some View { HStack { let url = URL(string: player.photoUrl) AsyncImage(url: url) { image in image.resizable() } placeholder: { ProgressView() } .frame(width: 64, height: 64) VStack(alignment: .leading) { Text(player.name) Text(player.team) } Spacer() Text(String(player.points)) } } } @Composable fun PlayerView(player: Player) { Row { AsyncImage(model = player.photoUrl, contentDescription = player.name, contentScale = ContentScale.Fit, modifier = Modifier.size(60.dp) ) Spacer(modifier = Modifier.size(12.dp)) Column( modifier = Modifier .weight(1f) .padding(start = 8.dp) ) { Text(player.name) Text(player.team) } Text(player.points) } } SwiftUI Compose
  14. @joreilly struct PlayerView: View { var player: Player var body:

    some View { HStack { let url = URL(string: player.photoUrl) AsyncImage(url: url) { image in image.resizable() } placeholder: { ProgressView() } .frame(width: 64, height: 64) VStack(alignment: .leading) { Text(player.name) Text(player.team) } Spacer() Text(String(player.points)) } } } @Composable fun PlayerView(player: Player) { Row { AsyncImage(model = player.photoUrl, contentDescription = player.name, contentScale = ContentScale.Fit, modifier = Modifier.size(60.dp) ) Spacer(modifier = Modifier.size(12.dp)) Column( modifier = Modifier .weight(1f) .padding(start = 8.dp) ) { Text(player.name) Text(player.team) } Text(player.points) } } SwiftUI Compose
  15. @joreilly Navigation - Compose NavHost(navController, startDestination = Screen.PersonList.title) { composable(route

    = Screen.PersonList.title) { PersonListScreen { navController.navigate(Screen.PersonDetails.title + "/${it.name}") } } composable(route = Screen.PersonDetails.title + "/{person}") { backStackEntry -> val personName = backStackEntry.arguments?.get("person") as String PersonDetailsScreen(personName, popBack = { navController.popBackStack() }) } } https://github.com/joreilly/PeopleInSpace
  16. @joreilly Navigation - SwiftUI NavigationStack { List(viewModel.people, id: \.self) {

    person in NavigationLink(value: person) { PersonView(viewModel: viewModel, person: person) } } .navigationDestination(for: Assignment.self) { person in PersonDetailsView(viewModel: viewModel, person: person) } .navigationBarTitle(Text("People In Space")) }
  17. @joreilly Charts Chart(playerHistory) { BarMark( x: .value("Season", $0.seasonName), y: .value("Points",

    $0.totalPoints) ) } .chartXAxisLabel("Season", alignment: Alignment.center) .chartYAxisLabel("Total Points") XYChart( xAxisModel = CategoryAxisModel(categoryXAxisModel), yAxisModel = LinearAxisModel(yAxisModel.autoScaleRange()), xAxisTitle = "Season", yAxisTitle = “Total Points" ) { VerticalBarChart(series = barChartEntries) } SwiftUI Compose (KoalaPlot) https://github.com/joreilly/FantasyPremierLeague/
  18. @joreilly AnyLayout/moveableContentOf let layout = vertical ? AnyLayout(VStack()) : AnyLayout(HStack())

    layout { ... } val movableContent = remember(content as Any) { movableContentOf(content) } if (vertical) { Column { movableContent() } } else { Row { movableContent() } } SwiftUI Compose
  19. @joreilly @joreilly SwiftUI Compose var bio by remember { mutableStateOf("")

    } TextField(value = bio, onValueChange = { bio = it }, maxLines = 5, placeholder = { Text("Enter your bio") } ) @State private var bio = "" TextField("Enter your bio", text: $bio, axis: .vertical) .lineLimit(.. 5)
  20. @joreilly SwiftUI Compose ProvideTextStyle(TextStyle(fontWeight = FontWeight.Bold)) { Column { Text(departure.timetableId)

    Text(departure.displayName) } } VStack { Text(departure.timetableId) Text(departure.displayName) } .fontWeight(.bold) https://github.com/joreilly/GalwayBus
  21. @joreilly Swipe to Refresh List(viewModel.leagueStandings) { leagueResult in LeagueReesultView(leagueResult: leagueResult)

    } .refreshable { await viewModel.getLeageStandings() } SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing), onRefresh = { viewModel.getLeagueStandings() }, ) { LazyColumn { items(items = leagueStandings) { leagueResult -> LeagueResultView(leagueResult = leagueResult) } } } SwiftUI Compose https://github.com/joreilly/FantasyPremierLeague/
  22. Structured Concurrency • Support for writing asynchronous and parallel code

    in a structured way • Swift: Tasks, async functions, AsyncSequence • Kotlin: Coroutines, suspend functions, Flow • Swift Tasks/Kotlin Coroutines can be suspended and resumed later • UI lifecycle awareness/cancellation etc @joreilly
  23. @joreilly func fetchWeatherHistory() async -> [Double] { // code to

    retrieve weather history } func calculateAverageTemp(for records: [Double]) async -> Double { return <calculate average> } func upload(result: Double) async -> String { "OK" } func processWeather() async { let records = await fetchWeatherHistory() let average = await calculateAverageTemp(for: records) let response = await upload(result: average) print("Server response: \(response)") } https://www.hackingwithswift.com/quick-start/concurrency/
  24. @joreilly func fetchWeatherHistory() async -> [Double] { // code to

    retrieve weather history } func calculateAverageTemp(for records: [Double]) async -> Double { return <calculate average> } func upload(result: Double) async -> String { "OK" } func processWeather() async { let records = await fetchWeatherHistory() let average = await calculateAverageTemp(for: records) let response = await upload(result: average) print("Server response: \(response)") } suspend fun fetchWeatherHistory(): Array<Double> { // code to retrieve weather history } suspend fun calculateAverageTemp(records: Array<Double>) : Double { return <calculate average> } suspend fun upload(result: Double): String { return "OK" } suspend fun processWeather() { val records = fetchWeatherHistory() val average = calculateAverageTemp(records) val response = upload(average) println(“Server response: ${response}") } Swift Kotlin
  25. @joreilly func fetchWeatherHistory() async -> [Double] { // code to

    retrieve weather history } func calculateAverageTemp(for records: [Double]) async -> Double { return <calculate average> } func upload(result: Double) async -> String { "OK" } func processWeather() async { let records = await fetchWeatherHistory() let average = await calculateAverageTemp(for: records) let response = await upload(result: average) print("Server response: \(response)") } suspend fun fetchWeatherHistory(): Array<Double> { // code to retrieve weather history } suspend fun calculateAverageTemp(records: Array<Double>) : Double { return <calculate average> } suspend fun upload(result: Double): String { return "OK" } suspend fun processWeather() { val records = fetchWeatherHistory() val average = calculateAverageTemp(records) val response = upload(average) println(“Server response: ${response}") } Swift Kotlin
  26. @joreilly static func main() async { await processWeather() } Task

    { await processWeather() } .task { await processWeather() } suspend fun main () { processWeather() } scope.launch { processWeather() } LaunchedEffect(key) { processWeather() } Calling Swift async/Kotlin suspend functions Swift/SwiftUI Kotlin/Compose
  27. @joreilly static func main() async { await processWeather() } Task

    { await processWeather() } .task { await processWeather() } Calling Swift async/Kotlin suspend functions Swift/SwiftUI Kotlin/Compose suspend fun main () { processWeather() } scope.launch { processWeather() } LaunchedEffect(key) { processWeather() }
  28. @joreilly static func main() async { await processWeather() } Task

    { await processWeather() } .task { await processWeather() } Calling Swift async/Kotlin suspend functions Swift/SwiftUI Kotlin/Compose suspend fun main () { processWeather() } scope.launch { processWeather() } LaunchedEffect(key) { processWeather() }
  29. func bootstrapSequence() async { async let moviesTask = loadMovies() async

    let userTask = currentUser() let (_, currentUser) = await (moviesTask, userTask) async let favoritesTask = updateFavorites(user: user) async let profilesTask = updateUserProfile(user: user) async let ticketsTask = updateUserTickets(user: user) let (favorites, profile, tickets) = await (favoritesTask, profileTask, ticketsTask) // use the loaded data as needed } @joreilly https://www.donnywals.com/running-tasks-concurrently-with-swift-concurrencys-async-let/
  30. func bootstrapSequence() async { async let moviesTask = loadMovies() async

    let userTask = currentUser() let (_, currentUser) = await (moviesTask, userTask) async let favoritesTask = updateFavorites(user: user) async let profilesTask = updateUserProfile(user: user) async let ticketsTask = updateUserTickets(user: user) let (favorites, profile, tickets) = await (favoritesTask, profileTask, ticketsTask) // use the loaded data as needed } suspend fun bootstrapSequence() = coroutineScope { val moviesDef = async { loadMovies() } val userDef = async { currentUser() } val (_, currentUser) = awaitAll(moviesDef, userDef) val favoritesDef = async { updateFavorites(user = user) } val profilesDef = async { updateUserProfile(user = user) } val ticketsDef = async { updateUserTickets(user = user) } val (favorites, profile, tickets) = awaitAll(favoritesDef, profilesDef, ticketsDef) // use the loaded data as needed } Swift Kotlin @joreilly
  31. func bootstrapSequence() async { async let moviesTask = loadMovies() async

    let userTask = currentUser() let (_, currentUser) = await (moviesTask, userTask) async let favoritesTask = updateFavorites(user: user) async let profilesTask = updateUserProfile(user: user) async let ticketsTask = updateUserTickets(user: user) let (favorites, profile, tickets) = await (favoritesTask, profileTask, ticketsTask) // use the loaded data as needed } suspend fun bootstrapSequence() = coroutineScope { val moviesDef = async { loadMovies() } val userDef = async { currentUser() } val (_, currentUser) = awaitAll(moviesDef, userDef) val favoritesDef = async { updateFavorites(user = user) } val profilesDef = async { updateUserProfile(user = user) } val ticketsDef = async { updateUserTickets(user = user) } val (favorites, profile, tickets) = awaitAll(favoritesDef, profilesDef, ticketsDef) // use the loaded data as needed } Swift Kotlin @joreilly
  32. func bootstrapSequence() async { async let moviesTask = loadMovies() async

    let userTask = currentUser() let (_, currentUser) = await (moviesTask, userTask) async let favoritesTask = updateFavorites(user: user) async let profilesTask = updateUserProfile(user: user) async let ticketsTask = updateUserTickets(user: user) let (favorites, profile, tickets) = await (favoritesTask, profileTask, ticketsTask) // use the loaded data as needed } suspend fun bootstrapSequence() = coroutineScope { val moviesDef = async { loadMovies() } val userDef = async { currentUser() } val (_, currentUser) = awaitAll(moviesDef, userDef) val favoritesDef = async { updateFavorites(user = user) } val profilesDef = async { updateUserProfile(user = user) } val ticketsDef = async { updateUserTickets(user = user) } val (favorites, profile, tickets) = awaitAll(favoritesDef, profilesDef, ticketsDef) // use the loaded data as needed } Swift Kotlin @joreilly
  33. func bootstrapSequence() async { async let moviesTask = loadMovies() async

    let userTask = currentUser() let (_, currentUser) = await (moviesTask, userTask) async let favoritesTask = updateFavorites(user: user) async let profilesTask = updateUserProfile(user: user) async let ticketsTask = updateUserTickets(user: user) let (favorites, profile, tickets) = await (favoritesTask, profileTask, ticketsTask) // use the loaded data as needed } suspend fun bootstrapSequence() = coroutineScope { val moviesDef = async { loadMovies() } val userDef = async { currentUser() } val (_, currentUser) = awaitAll(moviesDef, userDef) val favoritesDef = async { updateFavorites(user = user) } val profilesDef = async { updateUserProfile(user = user) } val ticketsDef = async { updateUserTickets(user = user) } val (favorites, profile, tickets) = awaitAll(favoritesDef, profilesDef, ticketsDef) // use the loaded data as needed } Swift Kotlin @joreilly
  34. @joreilly Swift Kotlin for await quote in quotes { print(quote)

    } let ucQuotes = quotes.map(\.localizedUppercase) for await quote in ucQuotes { print(quote) } let anonQuotes = quotes .filter { $0.contains("Anonymous") } for await quote in anonQuotes { print(line) } quotes.collect { quote -> println(quote) } val ucQuotes = quotes.map { it.uppercase() } ucQuotes.collect { quote -> println(quote) } val anonQuotes = quotes .filter { it.contains("Anonymous")} anonQuotes.collect { quote -> println(quote) } https://www.hackingwithswift.com/quick-start/concurrency/how-to-manipulate-an-asyncsequence-using-map- fi lter-and-more
  35. @joreilly Swift Kotlin for await quote in quotes { print(quote)

    } let ucQuotes = quotes.map(\.localizedUppercase) for await quote in ucQuotes { print(quote) } let anonQuotes = quotes .filter { $0.contains("Anonymous") } for await quote in anonQuotes { print(line) } quotes.collect { quote -> println(quote) } val ucQuotes = quotes.map { it.uppercase() } ucQuotes.collect { quote -> println(quote) } val anonQuotes = quotes .filter { it.contains("Anonymous")} anonQuotes.collect { quote -> println(quote) }
  36. @joreilly Swift Kotlin for await quote in quotes { print(quote)

    } let ucQuotes = quotes.map(\.localizedUppercase) for await quote in ucQuotes { print(quote) } let anonQuotes = quotes .filter { $0.contains("Anonymous") } for await quote in anonQuotes { print(line) } quotes.collect { quote -> println(quote) } val ucQuotes = quotes.map { it.uppercase() } ucQuotes.collect { quote -> println(quote) } val anonQuotes = quotes .filter { it.contains("Anonymous")} anonQuotes.collect { quote -> println(quote) }
  37. @joreilly let nums1 = [1, 2, 3].async let nums2 =

    [4, 5, 6].async for await num = merge(nums1, nums2) { print(num) } let nums = [1, 2, 3].async let strs = ["a", "b", "c"].async for await (a, b) = zip(nums, strs) { print("\(a) \(b)”) } val nums1 = arrayOf(1, 2, 3).asFlow() val nums2 = arrayOf(4, 5, 6).asFlow() merge(nums1, nums2).collect { num -> println(num) } val nums = arrayOf(1, 2, 3).asFlow() val strs = arrayOf("a", "b", "c").asFlow() nums.zip(strs) { a, b -> "$a $b" }.collect { println(it) } Swift Kotlin
  38. @joreilly let nums1 = [1, 2, 3].async let nums2 =

    [4, 5, 6].async for await num = merge(nums1, nums2) { print(num) } let nums = [1, 2, 3].async let strs = ["a", "b", "c"].async for await (a, b) = zip(nums, strs) { print("\(a) \(b)”) } val nums1 = arrayOf(1, 2, 3).asFlow() val nums2 = arrayOf(4, 5, 6).asFlow() merge(nums1, nums2).collect { num -> println(num) } val nums = arrayOf(1, 2, 3).asFlow() val strs = arrayOf("a", "b", "c").asFlow() nums.zip(strs) { a, b -> "$a $b" }.collect { println(it) } Swift Kotlin
  39. Kotlin/Native • technology for compiling Kotlin code to native binaries

    • includes an LLVM-based backend for the Kotlin compiler • compiler creates framework for Swift and Objective-C projects • can access native APIs directly from Kotlin/Native • shared code can also be built as Swift Package @joreilly
  40. @joreilly View Model Compose Repository KMM Shared Code Kotlin Swift

    View Model SwiftUI Data Model Server REST API
  41. @joreilly fun pollNetwork(network: String): Flow<List<Station>> = flow { while (true)

    { val stations = fetchBikeShareInfo(network) emit(stations) delay(POLL_INTERVAL) } } Kotlin Multiplatform shared code (CityBikesRespository)
  42. class BikeShareViewModel: ObservableObject { @Published var stations = [Station]() private

    let repo: CityBikesRepository init(repo: CityBikesRepository) { self.repo = repo } func pollNetworkUpdates(network: String) async { let stream = asyncStream(for: repo.pollNetwork(network: network)) for await data in stream { self.stations = data } } } @joreilly iOS View Model class BikeShareViewModel( private val repo: CityBikesRepository ) : ViewModel() { var stations by mutableStateOf<List<Station>>(emptyList()) suspend fun pollNetworkUpdates(network: String) { repo.pollNetworkUpdates(network).collect { this.stations = it } } } Android View Model
  43. class BikeShareViewModel: ObservableObject { @Published var stations = [Station]() private

    let repo: CityBikesRepository init(repo: CityBikesRepository) { self.repo = repo } func pollNetworkUpdates(network: String) async { let stream = asyncStream(for: repo.pollNetwork(network: network)) for await data in stream { self.stations = data } } } @joreilly iOS View Model class BikeShareViewModel( private val repo: CityBikesRepository ) : ViewModel() { var stations by mutableStateOf<List<Station>>(emptyList()) suspend fun pollNetworkUpdates(network: String) { repo.pollNetworkUpdates(network).collect { this.stations = it } } } Android View Model
  44. @joreilly struct StationsScreen: View { @ObservedObject var viewModel : BikeShareViewModel

    let network: String var body: some View { List(viewModel.stations) { station in StationView(station: station) } .task { await viewModel.pollNetworkUpdates(network: network) } } } SwiftUI @Composable fun StationsScreen(viewModel: BikeShareViewModel, network: String) { LaunchedEffect(network) { viewModel.pollNetworkUpdates(network) } LazyColumn { items(viewModel.stations) { station -> StationView(station) } } } Compose
  45. @joreilly struct StationsScreen: View { @ObservedObject var viewModel : BikeShareViewModel

    let network: String var body: some View { List(viewModel.stations) { station in StationView(station: station) } .task { await viewModel.pollNetworkUpdates(network: network) } } } SwiftUI @Composable fun StationsScreen(viewModel: BikeShareViewModel, network: String) { LaunchedEffect(network) { viewModel.pollNetworkUpdates(network) } LazyColumn { items(viewModel.stations) { station -> StationView(station) } } } Compose
  46. @joreilly struct StationView : View { var station: Station var

    body: some View { HStack { Image("ic_bike").resizable() .renderingMode(.template) .foregroundColor(station.freeBikes() < 5 ? .orange : .green) .frame(width: 32.0, height: 32.0) Spacer().frame(width: 16) VStack(alignment: .leading) { Text(station.name).font(.headline) HStack { Text("Free:").font(.subheadline).frame(width: 80, alignment: .leading) Text("\(station.freeBikes())").font(.subheadline) } HStack { Text("Slots:").font(.subheadline).frame(width: 80, alignment: .leading) Text("\(station.slots())").font(.subheadline) } } } } }
  47. @joreilly @Composable fun StationView(station: Station) { Row(modifier = Modifier.padding(start =

    16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) { Image(painterResource(R.drawable.ic_bike), colorFilter = ColorFilter.tint(if (station.freeBikes() < 5) lowAvailabilityColor else highAvailabilityColor), modifier = Modifier.size(32.dp), contentDescription = station.freeBikes().toString()) Spacer(modifier = Modifier.size(16.dp)) Column { Text(station.name, style = MaterialTheme.typography.h6) val textStyle = MaterialTheme.typography.body2 Row { Text("Free:", style = textStyle, textAlign = TextAlign.Justify, modifier = Modifier.width(48.dp)) Text("${station.freeBikes()}, style = textStyle) } Row { Text("Slots:", style = textStyle, textAlign = TextAlign.Justify, modifier = Modifier.width(48.dp), ) Text("${station.slots()}", style = textStyle) } } } }
  48. @joreilly struct StationView : View { var station: Station var

    body: some View { HStack { Image("ic_bike") .frame(width: 32.0, height: 32.0) Spacer().frame(width: 16) VStack(alignment: .leading) { Text(station.name) HStack { Text("Free:") Text("\(station.freeBikes())") } HStack { Text("Slots:") Text("\(station.slots())") } } } } } SwiftUI @Composable fun StationView(station: Station) { Row(modifier = Modifier.padding(16.dp)) { Image(painterResource(R.drawable.ic_bike) modifier = Modifier.size(32.dp)) Spacer(modifier = Modifier.size(16.dp)) Column { Text(station.name) Row { Text("Free:") Text("${station.freeBikes()}") } Row { Text("Slots:") Text("${station.slots()}") } } } } Compose
  49. @joreilly struct StationView : View { var station: Station var

    body: some View { HStack { Image("ic_bike") .frame(width: 32.0, height: 32.0) Spacer().frame(width: 16) VStack(alignment: .leading) { Text(station.name) HStack { Text("Free:") Text("\(station.freeBikes())") } HStack { Text("Slots:") Text("\(station.slots())") } } } } } SwiftUI @Composable fun StationView(station: Station) { Row(modifier = Modifier.padding(16.dp)) { Image(painterResource(R.drawable.ic_bike) modifier = Modifier.size(32.dp)) Spacer(modifier = Modifier.size(16.dp)) Column { Text(station.name) Row { Text("Free:") Text("${station.freeBikes()}") } Row { Text("Slots:") Text("${station.slots()}") } } } } Compose
  50. @joreilly Colour Tinting let tintColor = station.freeBikes() < 5 ?

    lowAvailabilityColor : highAvailabilityColor Image("ic_bike").resizable() .renderingMode(.template) .foregroundColor(tintColor) .frame(width: 32.0, height: 32.0) val tintColor = if (station.freeBikes() < 5) lowAvailabilityColor else highAvailabilityColor Image(painterResource(R.drawable.ic_bike), colorFilter = ColorFilter.tint(tintColor), modifier = Modifier.size(32.dp), contentDescription = station.freeBikes().toString()) SwiftUI Compose
  51. @joreilly let playerStream = asyncStream(for: repository.playerList) .map { $0.sorted {

    $0.points > $1.points } } let queryStream = $query .debounce(for: 0.5, scheduler: DispatchQueue.main) .values for try await (players, query) in combineLatest(playerStream, queryStream) { self.playerList = players . fi lter { query.isEmpty || $0.name.localizedCaseInsensitiveContains(query) } } Swift AsyncSequence and Kotlin Flow “combined" https://johnoreilly.dev/posts/swift-async-algorithms-combine/
  52. @joreilly Recap • We are all mobile developers! • Swift/Kotlin

    language similarities • Common (declarative) approach to UI development • Common approach to concurrency • KMM for sharing some parts of the “boring” non-UI code!