Slide 1

Slide 1 text

Quick Start to Jetpack Compose Jussi Pohjolainen

Slide 2

Slide 2 text

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!)

Slide 3

Slide 3 text

Essentials • Composables • State • Coroutines • LaunchedEffect • rememberCoroutineScope

Slide 4

Slide 4 text

Empty Activity will give jetpack compose project

Slide 5

Slide 5 text

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.

Slide 6

Slide 6 text

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!

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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!", ) }

Slide 10

Slide 10 text

@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

Slide 11

Slide 11 text

Managing State

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

mutableStateOf val state : MutableState = mutableStateOf("hello") state.value = "Hello" !// MutableState: interface MutableState : State { override var value: T }

Slide 15

Slide 15 text

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val mutableState : MutableState = 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.

Slide 16

Slide 16 text

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val mutableState : MutableState = remember { mutableStateOf(1) } Button(onClick = { mutableState.value!++ }) { Text(mutableState.value.toString()) } } } } Changing the state to Int You can also optimize mutableIntStateOf (no boxing)

Slide 17

Slide 17 text

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 }

Slide 18

Slide 18 text

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { !// val mutableState : MutableState = 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

Slide 19

Slide 19 text

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. Compose will inject the method at compile time

Slide 20

Slide 20 text

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val mutableState: MutableState> = 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

Slide 21

Slide 21 text

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { var mutableState: MutableState> = 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…

Slide 22

Slide 22 text

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { !// var mutableState: MutableState> !// = remember { mutableStateOf(mutableListOf("hei", "moi")) } val mutableList : SnapshotStateList = remember { mutableStateListOf("hei", "moi") } Button(onClick = { mutableList.add("Hello") }) { Text(mutableList.toList().toString()) } } } } Now it is observable list

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

@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

Slide 25

Slide 25 text

@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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

@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

Slide 28

Slide 28 text

Lifecycle

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

@Composable fun App() { val list: SnapshotStateList = 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

Slide 31

Slide 31 text

@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

Slide 32

Slide 32 text

@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!

Slide 33

Slide 33 text

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!

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

@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

Slide 36

Slide 36 text

Side-effects

Slide 37

Slide 37 text

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.

Slide 38

Slide 38 text

@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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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) } }

Slide 42

Slide 42 text

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" } }

Slide 43

Slide 43 text

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" }

Slide 44

Slide 44 text

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" }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Threading @Composable fun Test() { var uiText : String by remember { mutableStateOf("Loading!!...") } LaunchedEffect(Unit) { val deferredList : List> = List(2) { async { fetchFromNetwork() } } val results = deferredList.awaitAll() uiText = results.joinToString("\n") } Text(uiText) }

Slide 47

Slide 47 text

@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

Slide 48

Slide 48 text

@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

Slide 49

Slide 49 text

@Composable fun Test() { !// Remembering a value across recompositions var result by remember { mutableStateOf("") } var fetchJob by remember { mutableStateOf(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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

!// Define your data model data class Result(val users: List) 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

Slide 53

Slide 53 text

@Composable fun Test() { !// Remembering a value across recompositions var result by remember { mutableStateOf("") } var fetchJob by remember { mutableStateOf(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) } }

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

@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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

ViewModel

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

Advantages • Data Persistence across configuration changes • Improved testability • Easier maintenance

Slide 63

Slide 63 text

Dependency implementation("androidx.lifecycle:lifecycle- viewmodel-compose:2.7.0")

Slide 64

Slide 64 text

data class User(val id: Int = 0, val name: String) class UserViewModel : ViewModel() { private val _users : SnapshotStateList = mutableStateListOf() val users: List = _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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

@Composable fun AddButton() { val userViewModel : UserViewModel = viewModel() Button(onClick = { userViewModel.addUser(User(name = "jack")) }) { Text("Add") } } ViewModel is singleton

Slide 67

Slide 67 text

Retrofit

Slide 68

Slide 68 text

Retrofit Usage interface UserService { @GET("/users") suspend fun getUsers(): List @POST("/users") suspend fun addUser(@Body newUser: User): Response }

Slide 69

Slide 69 text

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) } }

Slide 70

Slide 70 text

class UserViewModel : ViewModel() { private val _users : SnapshotStateList = mutableStateListOf() val users: List = _users init { populateUsers() } private fun populateUsers() { viewModelScope.launch { try { val fetchedUsers = RetrofitInstance.userService.getUsers() _users.clear() _users.addAll(fetchedUsers) } catch (e: Exception) { e.printStackTrace() } } }

Slide 71

Slide 71 text

fun addUser(newUser: User) { viewModelScope.launch { val response : Response = 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) } } }

Slide 72

Slide 72 text

Navigation

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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”

Slide 76

Slide 76 text

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”

Slide 77

Slide 77 text

Passing Arguments

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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") } } }

Slide 80

Slide 80 text

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") } }

Slide 81

Slide 81 text

Passing Arguments back

Slide 82

Slide 82 text

@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") } }

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

@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? = savedStateHandle!?.getLiveData("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") } } }

Slide 85

Slide 85 text

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? = savedStateHandle!?.getLiveData("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

Slide 86

Slide 86 text

@Composable fun HomeScreen(navController: NavController) { val itemId = (1!..10).random() var name by rememberSaveable { mutableStateOf("") } val savedStateHandle = navController.currentBackStackEntry!?.savedStateHandle val liveData : LiveData? = savedStateHandle!?.getLiveData("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

Slide 87

Slide 87 text

Using MVVM

Slide 88

Slide 88 text

data class User(val id: Int = 0, val name: String) class UserViewModel : ViewModel() { private val _users : SnapshotStateList = mutableStateListOf() var counter by mutableIntStateOf(0) private set !// Optional: Make the setter private to control access val users: List = _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 } } }

Slide 89

Slide 89 text

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")) } } }

Slide 90

Slide 90 text

@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)) } } } }

Slide 91

Slide 91 text

@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)) } }

Slide 92

Slide 92 text

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") } }

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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.

Slide 95

Slide 95 text

Dependencies and permissions implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("com.google.android.gms:play-services-location:21.2.0") Minimun SDK 30

Slide 96

Slide 96 text

@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

Slide 97

Slide 97 text

class LocationViewModel(application: Application) : AndroidViewModel(application) { private val locationRepository = LocationRepository(application) private val _location = mutableStateOf(null) val location: State = _location fun startLocationUpdates() { locationRepository.startLocationUpdates { location -> _location.value = location } } } We will need application context to access GPS

Slide 98

Slide 98 text

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