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

Introduction to Jetpack Compose

Introduction to Jetpack Compose

Composables
State
Coroutines
LaunchedEffect
rememberCoroutineScope

Jussi Pohjolainen

March 18, 2024
Tweet

More Decks by Jussi Pohjolainen

Other Decks in Technology

Transcript

  1. Overview • Jetpack Compose is a modern toolkit for building

    native Android UI • Jetpack Compose simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin API • Much easier, no more jumping between code and xml • Some UI elements a lot easier to implement (Lists!)
  2. Composable • Composables are functions in Jetpack Compose that define

    UI components. • Annotated with @Composable. • Can contain other composables, creating a hierarchy. • Automatically update UI with state changes. • Executed by the Compose runtime, not directly by the developer. • Can accept parameters for configuration and dynamic content. • Follow a declarative programming model. • Designed for Kotlin and integrate seamlessly with Android's UI toolkit.
  3. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { Greeting("Android") } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") } No XML, give composable function Annotates the function to be composable, part of the UI Text is another composable, notice now return!
  4. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { LectureDemoTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Greeting( name = "Android", modifier = Modifier.padding(innerPadding) ) } } } } } Your layout can start under status bar Common theme Text is another composable, notice now return! Provides slots for top/bottomBar
  5. Feature React Components Jetpack Compose Composables SwiftUI Views Paradigm Declarative

    Declarative Declarative UI Definition JSX Kotlin code with @Composable Swift code State Management useState, useContext, etc. remember, mutableStateOf, etc. @State, @Binding, etc. Reusability Components can be reused Composables can be reused Views can be reused Hierarchical Structure Component tree Composable tree View hierarchy Rendering Mechanism Virtual DOM, diffing algorithm Slot table, smart recomposition Declarative UI, diffing algorithm Lifecycle Management useEffect, useMemo, etc. DisposableEffect, LaunchedEffect onAppear, onDisappear Environment Adaptation Responsive design with CSS or libs Modifiers, MaterialTheme, etc. EnvironmentValues, @Environment Platform Integration Web (DOM) Android iOS, macOS, watchOS, tvOS Development Language JavaScript, TypeScript Kotlin Swift Community and Ecosystem Large, with extensive libraries Growing, Android-centric Growing, Apple platforms-focused UI Updates Explicit re-renders Automatic UI updates Automatic UI updates Data Flow Props down, events up Parameters, state hoisting Binding, state hoisting Debugging Tools React Developer Tools Layout Inspector, Compose Preview Xcode Previews, Instruments Learning Curve Moderate Moderate to high Moderate to high
  6. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { App() } } } @Composable fun App() { Greeting("Hello") Greeting("World") } @Composable fun Greeting(name: String) { Text( text = "Hello $name!", ) }
  7. @Composable fun App() { Column { Greeting("Hello") Greeting("World") } }

    @Composable fun Greeting(name: String) { Text( text = "Hello $name!", ) } Column will put the composables in one column
  8. Overview • State in an app is any value that

    can change over time • All Android apps display state to the user • Jetpack Compose helps you be explicit about where and how you store and use state in an Android app
  9. State and composition • Any time a state is updated

    a recomposition takes place • Composable functions can use the remember API to store an object in memory • A value computed by remember is stored in the Composition during initial composition, and the stored value is returned during recomposition • remember can be used to store both mutable and immutable objects.
  10. mutableStateOf val state : MutableState<String> = mutableStateOf("hello") state.value = "Hello"

    !// MutableState: interface MutableState<T> : State<T> { override var value: T }
  11. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { val mutableState : MutableState<String> = remember { mutableStateOf("Hello") } Button(onClick = { mutableState.value = "world" }) { Text(mutableState.value) } } } } Creates a MutableState that is remembered across recompositions. The remember function will ensure that the mutableStateOf("Hello") is only invoked once, and its result is preserved as long as the composable where this state is created remains in the Composition and does not get recomposed with different keys.
  12. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { val mutableState : MutableState<Int> = remember { mutableStateOf(1) } Button(onClick = { mutableState.value!++ }) { Text(mutableState.value.toString()) } } } } Changing the state to Int You can also optimize mutableIntStateOf (no boxing)
  13. import kotlin.reflect.KProperty class Delegate { private var value: Int =

    0 operator fun getValue(thisRef: Any?, property: KProperty!!<*>): Int { println("getValue called for '${property.name}', returning $value") return value } operator fun setValue(thisRef: Any?, property: KProperty!!<*>, newValue: Int) { println("setValue called for '${property.name}', changing value from $value to $newValue") value = newValue } } class Example { var x: Int by Delegate() !// Notice here the by keyword } fun main() { val example = Example() example.x = 4 !// This will invoke setValue of Delegate println(example.x) !// This will invoke getValue of Delegate }
  14. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { !// val mutableState : MutableState<Int> = remember { mutableStateOf(1) } var intState : Int by remember { mutableStateOf(1) } Button(onClick = { intState!++ }) { Text(intState.toString()) } } } } By using by keyword it will invoke the state variable for you! remember returns MutableIntState object, var intState: Int by mutableIntStateOf(0) The mutableIntState returns MutableIntState - object
  15. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { var (value, setValue) = remember { mutableStateOf(1) } Button(onClick = { setValue(!++value) }) { Text(value.toString()) } } } } Destruct MutableState<String>. Compose will inject the method at compile time
  16. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { val mutableState: MutableState<MutableList<String!>> = remember { mutableStateOf(mutableListOf("hei", "moi")) } Button(onClick = { mutableState.value.add("hello")}) { Text(mutableState.value.toString()) } } } } UI refresh does not happen, value itself is the same
  17. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { var mutableState: MutableState<MutableList<String!>> = remember { mutableStateOf(mutableListOf("hei", "moi")) } Button(onClick = { !// Create a new list that is a copy of the current one val newList = mutableState.value.toMutableList() !// Add "hello" to the new list newList.add("hello") !// Update mutableState with the new list mutableState.value = newList }) { Text(mutableState.value.toString()) !// Assuming mutableState is a list of strings } } } } Now it works, but it’s rather painful to do…
  18. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { !// var mutableState: MutableState<MutableList<String!>> !// = remember { mutableStateOf(mutableListOf("hei", "moi")) } val mutableList : SnapshotStateList<String> = remember { mutableStateListOf("hei", "moi") } Button(onClick = { mutableList.add("Hello") }) { Text(mutableList.toList().toString()) } } } } Now it is observable list
  19. State Hoisting • Main composable may have state • This

    state can be passed to child composables • You can also pass a function to child, child calls it and then parent will do something
  20. @Composable fun HelloScreen() { var name by remember { mutableStateOf("")

    } Column { Button(onClick = { name = "Jack" }) { Text("Change") } Greeting(name) } } @Composable fun Greeting(name : String) { Text(name) } This will change when main component state name changes
  21. @Composable fun HelloScreen() { var name by remember { mutableStateOf("")

    } Column { MyButton("Change") { newName -> name = newName } Text(name) } } @Composable fun MyButton(title : String, callback: (newName: String) -> Unit) { Button(onClick = { callback("Jack") }) { Text(title) } } Callback to parent Callback received, state change and “Jack” is shown
  22. State Preservation • From landscape to portrait state is not

    saved • The rememberSaveable API behaves similarly to remember because it retains state across recompositions, and also across activity or process recreation using the saved instance state mechanism. For example, this happens, when the screen is rotated. • If state contains custom objects, use @Parcelize
  23. @Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("")

    } Column { MyButton("Change") { newName -> name = newName } Text(name) } } @Composable fun MyButton(title : String, callback: (newName: String) -> Unit) { Button(onClick = { callback("Jack") }) { Text(title) } } Restores state also when orientation changes
  24. Lifecycle • Initial composition • Keep track of the composables

    that you have describe in UI • State change • Jetpack Compose schedules a recomposition • Re-executes the composables that may have changed in response to state changes • Lifecycle is simple • Enter • Recompose (0 – n) • Leave
  25. @Composable fun App() { val list: SnapshotStateList<String> = remember {

    mutableStateListOf("Jack", "Hannah") } Column { for (text in list) { Greeting(text) } Button(onClick = { list.add("Tina") }) { Text("Add Name") } } } State triggers recomposition When adding a name, it will not recompose the Greetings already in the list
  26. @Composable fun App() { val list = remember { mutableStateListOf("Jack",

    "Hannah") } Column { list.forEachIndexed { index, text -> Greeting(text, index) } Button(onClick = { list.add("Tina") }) { Text("Add Name") } } } @Composable fun Greeting(name: String, index: Int) { Text( text = "Hello $name!", ) SideEffect { Log.d("ComposeRecomposition", "Recomposing at $index") } } SideEffect is called after the composition is complete. Used to perform operations that can affect the state outside of the composable
  27. @Composable fun App() { val list = remember { mutableStateListOf("Jack",

    "Hannah", "Tina") } Column { list.forEachIndexed { index, text -> Greeting(text, index) } Button(onClick = { list.removeFirst() }) { Text("Add Name") } } } @Composable fun Greeting(name: String, index: Int) { Text( text = "Hello $name!", ) SideEffect { Log.d("ComposeRecomposition", "Recomposing at $index") } } If inserting to top or removing from top (or middle), every Greeting is composed again!
  28. data class Person(val id : Int, val name : String)

    @Composable fun App() { val list = remember { mutableStateListOf(Person(1, "Jack"), Person(2, "Tina"), Person(3, "Paul")) } Column { list.forEach { person -> key(person.id) { Greeting(person.name, person.id) } } Button(onClick = { list.removeFirst() }) { Text("Add Name") } } } @Composable fun Greeting(name: String, index: Int) { Text( text = "Hello $name!", ) SideEffect { Log.d("ComposeRecomposition", "Recomposing at $index") } } Key identifies the composable and now no unnessary refresh of composables!
  29. var id = 3 data class Person(val id : Int,

    val name : String) @Composable fun App() { val list = remember { mutableStateListOf(Person(1, "Jack"), Person(2, "Tina"), Person(3, "Paul")) } Column { list.forEach { person -> key(person.id) { Greeting(person.name, person.id) } } Button(onClick = { list.add(0, Person(!++id, "Amanda")) }) { Text("Add Name") } } } @Composable fun Greeting(name: String, index: Int) { Text( text = "Hello $name!", ) SideEffect { Log.d("ComposeRecomposition", "Recomposing at $index") } } Only amanda Greeting is recomposed
  30. @Composable fun App() { val list = remember { mutableStateListOf(

    Person(1, "Jack"), Person(2, "Tina"), Person(3, "Paul")) } LazyColumn { items(list, key = { person -> person.id }) { person -> Greeting(name = person.name, index = person.id) } item { Button(onClick = { list.add(0, Person(!++id, "Amanda")) }) { Text("Add Name") } } } } Some composables has prebuilt key functionality
  31. LaunchedEffect • Call suspend functions inside of composable • When

    LaunchedEffect enters the Composition, it launches a coroutine with the block of code passed as a parameter. • The coroutine will be cancelled if LaunchedEffect leaves the composition • If LaunchedEffect is recomposed with different keys, the existing coroutine will be cancelled and the new suspend function will be launched in a new coroutine.
  32. @Composable fun Clock() { !// State to hold the current

    time var time: LocalTime by remember { mutableStateOf(LocalTime.now()) } !// LaunchedEffect to update the time every second LaunchedEffect(Unit) { while (true) { time = LocalTime.now() !// Update the time Log.d("time", time.toString()) delay(1000) !// Wait for a second } } !// Display the formatted time Text(text = time.toString()) } Creates new coroutine in Dispatchers.Main, so it uses event loop mechanism
  33. setContent { var showClock by remember { mutableStateOf(true) } Column

    { Button(onClick = { showClock = !showClock }) { Text(if (showClock) "Hide Clock" else "Show Clock") } if (showClock) { Clock() } } } Will cancel the coroutine by default
  34. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { var inputText by remember { mutableStateOf("") } val number = inputText.toIntOrNull() Column { TextField( value = inputText, onValueChange = { inputText = it }, label = { Text("Enter countdown start") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) ) if (number !!= null) { Countdown(startNumber = number) } } } } } @Composable fun Countdown(startNumber: Int) { var currentNumber by remember { mutableIntStateOf(startNumber) } LaunchedEffect(startNumber) { for (i in startNumber downTo 0) { currentNumber = i delay(1000) } } Text(text = "Countdown: $currentNumber") } Dependency on the given argument. If it changes, current coroutine cancels and new one starts
  35. Threading @Composable fun Test() { LaunchedEffect(Unit) { Log.d("MainActivity", Thread.currentThread().name) val

    result = withContext(Dispatchers.IO) { Log.d("MainActivity", Thread.currentThread().name) "JSON" } println(result) } }
  36. Threading @Composable fun Test() { LaunchedEffect(Unit) { Log.d("MainActivity", Thread.currentThread().name) val

    result = fetchFromNetwork() println(result) } } suspend fun fetchFromNetwork() : String { return withContext(Dispatchers.IO) { Log.d("MainActivity", Thread.currentThread().name) delay(1000) "JSON" } }
  37. Threading @Composable fun Test() { LaunchedEffect(Unit) { Log.d("MainActivity", Thread.currentThread().name) val

    result = fetchFromNetwork() println(result) } } suspend fun fetchFromNetwork() = withContext(Dispatchers.IO) { Log.d("MainActivity", Thread.currentThread().name) delay(1000) "JSON" }
  38. Threading @Composable fun Test() { var uiText : String by

    remember { mutableStateOf("Loading!!...") } LaunchedEffect(Unit) { println(Thread.currentThread().name) val result = fetchFromNetwork() uiText = result } Text(uiText) } suspend fun fetchFromNetwork() = withContext(Dispatchers.IO) { println(Thread.currentThread().name) delay(1000) "JSON" }
  39. Threading @Composable fun Test() { var uiText : String by

    remember { mutableStateOf("Loading!!...") } LaunchedEffect(Unit) { val r1 : Deferred<String> = async { fetchFromNetwork() } val r2 : Deferred<String> = async { fetchFromNetwork() } val r1Andr2 : List<Deferred<String!>> = listOf(r1, r2) val results = r1Andr2.awaitAll() uiText = results.joinToString("\n") } Text(uiText) }
  40. Threading @Composable fun Test() { var uiText : String by

    remember { mutableStateOf("Loading!!...") } LaunchedEffect(Unit) { val deferredList : List<Deferred<String!>> = List(2) { async { fetchFromNetwork() } } val results = deferredList.awaitAll() uiText = results.joinToString("\n") } Text(uiText) }
  41. @Composable fun Test() { !// Remembering a value across recompositions

    var result by remember { mutableStateOf("") } !// Remembering a coroutine scope across recompositions val coroutineScope = rememberCoroutineScope() Column { Button(onClick = { coroutineScope.launch { val r = fetchFromNetwork() result = r } }) { Text(text = "Fetch") } !// Display the count Text(result) } } suspend fun fetchFromNetwork() = withContext(Dispatchers.IO) { println(Thread.currentThread().name) delay(1000) "JSON" } Will create several coroutines
  42. @Composable fun App() { !// Remembering a value across recompositions

    var count by remember { mutableStateOf(0) } !// Remembering a flag to track if coroutine is already running var coroutineRunning by remember { mutableStateOf(false) } !// Remembering a coroutine scope across recompositions val coroutineScope = rememberCoroutineScope() Column { Button(onClick = { if (!coroutineRunning) { coroutineRunning = true coroutineScope.launch { repeat(10) { !// Perform asynchronous operation delay(1000) !// Update the count count!++ } coroutineRunning = false } } }) { Text(text = "Click me") } !// Display the count Text(text = "Count: ${count}") } } Preventing other coroutines
  43. @Composable fun Test() { !// Remembering a value across recompositions

    var result by remember { mutableStateOf("") } var fetchJob by remember { mutableStateOf<Job?>(null) } !// Remembering a coroutine scope across recompositions val coroutineScope = rememberCoroutineScope() Column { Button(onClick = { fetchJob!?.cancel() fetchJob = coroutineScope.launch { val r = fetchFromNetwork() result = r } }) { Text(text = "Fetch") } !// Display the count Text(result) } } suspend fun fetchFromNetwork() = withContext(Dispatchers.IO) { println(Thread.currentThread().name) delay(1000) "JSON" } Manual cancellation
  44. LaunchedEffect vs rememberCoroutineScope • LaunchedEffect (automatic) • Composable enters composition

    • A specific key, (state or prop) changes • Cancels coroutine automatically when composable leaves the screen • Cancels coroutine if key changes and then it recomposes • “If this value changed or appeared in the UI, do something.” • rememberCoroutineScope (manual) • Launch coroutine on user interaction • Control over start and cancel, full manual control
  45. LaunchedEffect vs CoroutineScope Feature LaunchedEffect rememberCoroutineScope Scope Composable function Composable

    function Lifespan Tied to the composable's lifecycle Tied to the composable's lifecycle Launch Time Automatically on recomposition and if keys change Manually, whenever needed Use Case - Starting coroutines that need to be cancelled and restarted with the composable - Suitable for operations that should restart on key changes - Suitable if coroutine should start when composable starts - Starting coroutines without automatic restart on recomposition - More control over coroutine launch and cancellation Example Fetching data when composable enters the composition or keys change Handling user interactions that trigger coroutines, like button clicks Cancellation Automatically cancelled when the composable leaves the composition or keys change Manual cancellation or tied to the composable's lifecycle Control Over Coroutine Limited, as it restarts with key changes Full control, manual launch and cancellation
  46. !// Define your data model data class Result(val users: List<User>)

    data class User(val id: Int, val firstName: String) interface UserService { @GET("/users") suspend fun fetchUsers(): Result @GET("/users/{id}") suspend fun fetchUserById(@Path("id") id: Int): User companion object { fun getService(): UserService { val retrofit = Retrofit.Builder() .baseUrl("https:!//dummyjson.com/") .addConverterFactory(GsonConverterFactory.create()) .build() return retrofit.create(UserService!::class.java) } } } !/* implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") !*/ Retrofit Creates retrofit everytime, not optimal, we could reuse this
  47. @Composable fun Test() { !// Remembering a value across recompositions

    var result by remember { mutableStateOf("") } var fetchJob by remember { mutableStateOf<Job?>(null) } !// Remembering a coroutine scope across recompositions val coroutineScope = rememberCoroutineScope() Column { Button(onClick = { fetchJob!?.cancel() fetchJob = coroutineScope.launch { val user : User = UserService.getService().fetchUserById(5) result = user.firstName } }) { Text(text = "Fetch") } !// Display the count Text(result) } }
  48. interface UserService { @GET("/users") suspend fun fetchUsers(): Result @GET("/users/{id}") suspend

    fun fetchUserById(@Path("id") id: Int): User companion object { private val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() } val service: UserService by lazy { Retrofit.Builder() .baseUrl("https:!//dummyjson.com/") .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build() .create(UserService!::class.java) } } } More optimal solution - service object is not created immediately at app starts, only when accessed for the first time - It is singleton, so only one object shared
  49. interface UserService { @GET("/users") suspend fun fetchUsers(): Result @GET("/users/{id}") suspend

    fun fetchUserById(@Path("id") id: Int): User companion object { val service: UserService by lazy { Retrofit.Builder() .baseUrl("https:!//dummyjson.com/") .addConverterFactory(GsonConverterFactory.create()) .build() .create(UserService!::class.java) } } } If default OkHttpClient is ok
  50. @Composable fun Parent() { var selectedId by remember { mutableIntStateOf(1)

    } Column { Button(onClick = { selectedId!++ }) { Text("Next ID") } UserDetailsScreen(id = selectedId) } } @Composable fun UserDetailsScreen(id: Int) { var result by remember { mutableStateOf("") } LaunchedEffect(id) { result = UserService.service.fetchUserById(id).firstName } Text("User: $result") } Dependent on id changes
  51. LaunchedEffect vs RememberCoroutineScope • Want that coroutine triggers immediately after

    recomposition? • LaunchedEffect • Want the coroutine is launched when state variable changes • LaunchedEffect • Want that coroutine does not trigger immediately after recomposition? • rememberCoroutineScope • Want more control on the lifecycle of the coroutine? (start, stop) • rememberCoroutineScope
  52. ViewModel • Acts as a container for UI-related data •

    Handleds configuration changes such as screen rotations, keyboard adjustements • Is bridge between UI and Data • UI <-> viewmodel <-> Data • Keeps the UI code clean from data processing logic • Provides efficient communication with the Model • Fetch, cache, manage
  53. MVVM model • ViewModel is part of Model-View-ViewModel (MVVM) pattern

    • Clear separation between development of UI (View), business logic / back-end logic (the Model) through View Model • ViewModel is intermediary between the View and the Model
  54. MVVM Components • Model • Data and business logic layer

    • Database, network code, data repository etc • View • UI of the app • ViewModel • Provides the data needed by the View and acts as a channel to the Model, handling the logic of preparing the data for the UI • Exposes streams of data relevant to the view • Reacts to user interactions forwarded by the view
  55. data class User(val id: Int = 0, val name: String)

    class UserViewModel : ViewModel() { private val _users : SnapshotStateList<User> = mutableStateListOf<User>() val users: List<User> = _users init { populateUsers() } private fun populateUsers() { _users.add(User(id = 1, name = "John Doe")) _users.add(User(id = 2, name = "Jane Doe")) _users.add(User(id = 3, name = "Jim Beam")) } fun addUser(newUser: User) { val newId = (_users.maxByOrNull { it.id }!?.id !?: 0) + 1 _users.add(newUser.copy(id = newId)) } } Code that will check the max id, increment by 1 and then but a copy of user to the list
  56. Column { UserListScreen() AddButton() } @Composable fun UserListScreen() { val

    userViewModel : UserViewModel = viewModel() val users = userViewModel.users LazyColumn { items(users, key = { user -> user.id }) { user -> Text(text = "ID: ${user.id}, Name: ${user.name}", style = MaterialTheme.typography.bodyMedium) } } } Getting singleton instance
  57. @Composable fun AddButton() { val userViewModel : UserViewModel = viewModel()

    Button(onClick = { userViewModel.addUser(User(name = "jack")) }) { Text("Add") } } ViewModel is singleton
  58. Retrofit Usage interface UserService { @GET("/users") suspend fun getUsers(): List<User>

    @POST("/users") suspend fun addUser(@Body newUser: User): Response<User> }
  59. Retrofit Usage object RetrofitInstance { private val retrofit by lazy

    { Retrofit.Builder() .baseUrl("https:!//jsonplaceholder.typicode.com") !// Replace with your actual URL .addConverterFactory(GsonConverterFactory.create()) .build() } val userService: UserService by lazy { retrofit.create(UserService!::class.java) } }
  60. class UserViewModel : ViewModel() { private val _users : SnapshotStateList<User>

    = mutableStateListOf<User>() val users: List<User> = _users init { populateUsers() } private fun populateUsers() { viewModelScope.launch { try { val fetchedUsers = RetrofitInstance.userService.getUsers() _users.clear() _users.addAll(fetchedUsers) } catch (e: Exception) { e.printStackTrace() } } }
  61. fun addUser(newUser: User) { viewModelScope.launch { val response : Response<User>

    = RetrofitInstance.userService.addUser(newUser) val user : User? = response.body() if(user !!= null) { val newId = (_users.maxByOrNull { it.id }!?.id !?: 0) + 1 val uniqueUser = user.copy(id = newId) _users.add(uniqueUser) } } }
  62. Overview • The Navigation component provides support for Jetpack Compose

    applications • It is an external dependency • implementation("androidx.navigation:navigation-compose:2.8.9") • You will need • NavHost • NavController • You will need to define the destination composable
  63. Navigate: App() val navController = rememberNavController() NavHost(navController = navController, startDestination

    = "home") { composable("home") { HomeScreen(navController) } composable("details") { DetailsScreen(navController) } } rememberNavController() is a Compose function that remembers the NavHost situation across recompositions: current destination, back stack, navigation arguments
  64. Navigate: HomeScreen() @Composable fun HomeScreen(navController: NavController) { Box( contentAlignment =

    Alignment.Center, !// This centers the Button within the Box. modifier = Modifier.fillMaxSize() !// The Box will fill the entire screen. ) { !// Button automatically sizes itself to fit its content, and now it's also centered. Button(onClick = { navController.navigate("details") }) { Text(text = "Go to Details") } } } Navigate to “details”
  65. Navigate: DetailsScreen() @Composable fun DetailsScreen(navController: NavController) { Box( contentAlignment =

    Alignment.Center, !// This centers the Button within the Box. modifier = Modifier.fillMaxSize() !// The Box will fill the entire screen. ) { Text("detail") } } Navigate to “details”
  66. Navigate: App() val navController = rememberNavController() NavHost(navController = navController, startDestination

    = "home") { composable("home") { HomeScreen(navController) } composable("details/{itemId}") { backStackEntry -> DetailsScreen(navController, backStackEntry.arguments!?.getString("itemId")) } } Current destination in the navigation stack
  67. Navigate: HomeScreen() @Composable fun HomeScreen(navController: NavController) { val itemId =

    (1!..10).random() Box( contentAlignment = Alignment.Center, !// This centers the Button within the Box. modifier = Modifier.fillMaxSize() !// The Box will fill the entire screen. ) { !// Button automatically sizes itself to fit its content, and now it's also centered. Button(onClick = { navController.navigate("details/$itemId") }) { Text(text = "Go to Details") } } }
  68. Navigate: DetailScreen() @Composable fun DetailsScreen(navController: NavController, itemId: String?) { Box(

    contentAlignment = Alignment.Center, !// This centers the Button within the Box. modifier = Modifier.fillMaxSize() !// The Box will fill the entire screen. ) { !// Button automatically sizes itself to fit its content, and now it's also centered. Text("detail: $itemId") } }
  69. @Composable fun DetailsScreen(navController: NavController, itemId: String?) { BackHandler { !//

    Logic to handle the back button press, similar to what would be used for a button click. val result = "${(1!..10).random()}" navController.previousBackStackEntry!?.savedStateHandle!?.set("keyForResult", result) navController.popBackStack() } Box( contentAlignment = Alignment.Center, !// This centers the Button within the Box. modifier = Modifier.fillMaxSize() !// The Box will fill the entire screen. ) { !// Button automatically sizes itself to fit its content, and now it's also centered. Text("detail: $itemId") } }
  70. Results back navController .previousBackStackEntry? .savedStateHandle? .set("keyForResult", result) Refers to the

    screen below the current one on the navigation stack. (Home) Key-value store that is used for passing data Adding key-value pair
  71. @Composable fun HomeScreen(navController: NavController) { val itemId = (1!..10).random() !//

    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7") !// or latest !// Whenever the backstack entry's result change we recomposition and update the UI val savedStateHandle = navController.currentBackStackEntry!?.savedStateHandle val liveData : LiveData<String>? = savedStateHandle!?.getLiveData<String>("keyForResult") val result : String by liveData!?.asFlow()!?.collectAsState(initial = "") !?: remember { mutableStateOf("") } Box( contentAlignment = Alignment.Center, !// This centers the Button within the Box. modifier = Modifier.fillMaxSize() !// The Box will fill the entire screen. ) { !// Button automatically sizes itself to fit its content, and now it's also centered. Button(onClick = { navController.navigate("details/$itemId") }) { Text(text = "Go to Details $result") } } }
  72. What? • Get the key-value store • val savedStateHandle =

    navController.currentBackStackEntry!?.savedStateHandle • Jetpack Compose has no onResume()-like lifecycle callback, so we observe changes in SavedStateHandle reactively (e.g., using LiveData): • val liveData : LiveData<String>? = savedStateHandle!?.getLiveData<String>("keyForResult") • LiveData was designed for the old Android model (Activities, XML, Fragments). • Jetpack Compose prefers Flow, which integrates natively. • val result : String by liveData!?.asFlow()!?.collectAsState(initial = "") !?: remember { mutableStateOf("") } • If liveData is null (e.g. savedStateHandle is null), a fallback is used
  73. @Composable fun HomeScreen(navController: NavController) { val itemId = (1!..10).random() var

    name by rememberSaveable { mutableStateOf("") } val savedStateHandle = navController.currentBackStackEntry!?.savedStateHandle val liveData : LiveData<String>? = savedStateHandle!?.getLiveData<String>("keyForResult") val result : String by liveData!?.asFlow()!?.collectAsState(initial = "") !?: remember { mutableStateOf("") } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = { navController.navigate("details/$name") }) { Text(text = "Go to Details with name of $name") } TextField( value = name, onValueChange = { name = it }, label = { Text("Enter name") }, singleLine = true ) } } } name would dissappear if just using remember
  74. data class User(val id: Int = 0, val name: String)

    class UserViewModel : ViewModel() { private val _users : SnapshotStateList<User> = mutableStateListOf<User>() var counter by mutableIntStateOf(0) private set !// Optional: Make the setter private to control access val users: List<User> = _users init { populateUsers() } fun increment() { counter!++ } private fun populateUsers() { _users.add(User(id = 1, name = "John Doe")) _users.add(User(id = 2, name = "Jane Doe")) _users.add(User(id = 3, name = "Jim Beam")) } fun fetchById(id: Int): User? { !// Access the current list of users and find the user with the matching ID return _users.find { user -> user.id !== id } } }
  75. App @Composable fun App() { val navController = rememberNavController() NavHost(navController

    = navController, startDestination = "home") { composable("home") { HomeScreen(navController) } composable("details/{personId}") { backStackEntry -> DetailsScreen(navController, backStackEntry.arguments!?.getString("personId")) } } }
  76. @Composable fun HomeScreen(navController: NavController) { val viewModel: UserViewModel = viewModel()

    val people = viewModel.users val counter = viewModel.counter Column { Text("Counter = $counter", modifier = Modifier .fillMaxWidth() .padding(16.dp)) Divider(modifier = Modifier.padding(horizontal = 16.dp)) LazyColumn { items(items = people, key = { person -> person.id }) { person -> Text( text = person.name, modifier = Modifier .fillMaxWidth() .clickable { navController.navigate("details/${person.id}") } .padding(16.dp) ) Divider(modifier = Modifier.padding(horizontal = 16.dp)) } } } }
  77. @Composable fun DetailsScreen(navController: NavController, personId: String?) { val viewModel: UserViewModel

    = viewModel() val personIdInt = personId!?.toIntOrNull() val user = personIdInt!?.let { viewModel.fetchById(it) } BackHandler { viewModel.increment() navController.popBackStack() } if (user !!= null) { Column(modifier = Modifier.padding(16.dp)) { Text("Name: ${user.name}") Text("Id: ${user.id}") } } else { Text("User not found", modifier = Modifier.padding(16.dp)) } }
  78. DetailScreen @Composable fun DetailsScreen(navController: NavController, viewModel: MyViewModel, itemId: String?) {

    BackHandler { !// Logic to handle the back button press, similar to what would be used for a button click. viewModel.setResult((1!..10).random()) navController.popBackStack() } Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize() ) { Text("detail: $itemId") } }
  79. Location API • Primarily used to gather the user's current

    location, track movements, and manage geofencing • Location-based functionalities like maps, location tracking, or proximity alerts
  80. Key Components • FusedLocationProviderClient: • The main entry point for

    interacting with the fused location provider. • LocationRequest: • Defines the quality of service for location updates, such as the interval, priority, and accuracy. • LocationCallback: • Receives notifications from FusedLocationProviderClient about location changes or status updates.
  81. @Composable fun App() { val viewModel: LocationViewModel = viewModel() val

    location = viewModel.location val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions(), onResult = { permissions -> "// Check if all requested permissions have been granted val allPermissionsGranted = permissions.entries.all { it.value } if (allPermissionsGranted) { "// Start location updates through the ViewModel if permissions are granted viewModel.startLocationUpdates() } } ) LaunchedEffect(Unit) { permissionLauncher.launch(arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION )) } Text(text = location.value"?.let { loc -> "Lat: ${loc.latitude}, Lon: ${loc.longitude}" } "?: "Location not available") } Asks from user permissions to use location
  82. class LocationViewModel(application: Application) : AndroidViewModel(application) { private val locationRepository =

    LocationRepository(application) private val _location = mutableStateOf<Location?>(null) val location: State<Location?> = _location fun startLocationUpdates() { locationRepository.startLocationUpdates { location -> _location.value = location } } } We will need application context to access GPS
  83. class LocationRepository(private val context: Context) { fun startLocationUpdates(callback: (Location?) ->

    Unit) { "// Check if location permissions are granted if (ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) "== PackageManager.PERMISSION_GRANTED "&& ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_COARSE_LOCATION ) "== PackageManager.PERMISSION_GRANTED ) { "// Permissions are granted, proceed with requesting location updates val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) val locationRequest = LocationRequest.Builder(10000L) .setPriority(Priority.PRIORITY_HIGH_ACCURACY) .setIntervalMillis(5000L) .build() val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { callback(locationResult.lastLocation) } } fusedLocationProviderClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) } else { Log.d("Location", "Not granted!") } } } Totally separate class for listening for location changes, informs these via callback