Source: .
State Hoisting
It is the way of making a component stateless by relocating its state to a higher level in the component hierarchy.
Let’s remake the above EditProfileScreen.kt
@Composable fun EditProfileScreen() { var fullName by remember { mutableStateOf("") } // state hoisting EditProfileContent(fullName, onFullNameChange = { fullName = it }) } @Composable fun EditProfileContent(fullName: String, onFullNameChange: (String) -> Unit) { Column( Modifier .background(Color.White) .padding(30.dp) .fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally ) { OutlinedTextField( value = fullName, onValueChange = onFullNameChange, label = { Text("Full Name") } ) Spacer(modifier = Modifier.padding(20.dp)) Text(text = fullName) } }
The general pattern for state hoisting is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
State & Event of EditProfileScreen
The pattern where the state goes down, and events go up is called a unidirectional data flow.
A hoisted state offers several key advantages:
- SingleSource : Centralizing the state reduces errors, providing a single, reliable sourc - Encapsulated: State changes are restricted to specific composables, ensuring internal management. - Shareable: The hoisted state can be accessed by multiple composables, facilitating seamless data sharing. - Interceptable: Callers can intercept or modify events before they affect the state, allowing for customized handling. - Decoupled: State for stateless composables can reside anywhere, promoting separation of concerns.
Just Keep In Mind
Composable shouldn’t break the unidirectional data flow.
Composable functions can be executed in any order or parallel.
Recomposition is optimistic and optimizes by skipping unnecessary functions.
Composables can run frequently, even every animation frame.
Okay Then… ⚠️
What if we need a service that needs to be initialized or created on the composables screen? Will it recompose multiple times?
Yes , there are scenarios where we must interact with the external world or perform certain actions unrelated to UI rendering. These states triggering in every recomposition can lead to unexpected application behaviour. 🫠
“We have a problem, We have introduced another problem to fix it” — Jetpack Compose Engineers👺
To prevent these unexpected issues in composable functions when recomposition occurs, we use effect-handlers
Effect-Handlers 🔧
Effect-Handlers in Jetpack Compose are mechanisms used to manage side effects in the UI. They facilitate the execution of tasks that are not directly related to rendering the user interface, such as network requests, database operations, or animations.
*Dealing with Effect-handlers were a challenge for developers until this article.
Effect handlers enhance performance, maintainability, and debugging by separating non-UI tasks from UI rendering logic.
There are two types of effect handlers:
Suspended Effect Handler : for suspending functions. — LaunchEffect — rememberCoroutineScope
Non-suspended Effect Handler: for non-suspending functions — DisposableEffect — SideEffect
LaunchEffect
It is the commonly used side effect in Jetpack Compose. It triggers when a composition first starts and can execute suspend functions.
It accepts two parameters: a key and a coroutineScope block.
We can use any state in the key parameter.
Inside the coroutineScope block, we can use suspended or non-suspended functions.
LaunchEffect runs only once in the composable function.
To run the LaunchEffect block again, you can provide a state that changes over time in the key parameter.
Suppose, we need to init DataManager in HomeScreen composable.
@Composable fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) { // This key can be changed as necessary like using counter LaunchedEffect(Unit) { // Here Unit will Trigger Only 1 Time viewModel.sentMessageSeenStatus() } //.... }
rememberCoroutineScope()
rememberCoroutineScope() creates a coroutine scope tied to the current composable, ensuring proper lifecycle management for launched coroutines.
Suppose we need to call some suspended function from composables.
@Composable fun ResetPasswordScreen(viewModel: AuthViewModel = hiltViewModel()) { // Remember the coroutine scope tied to this composable's lifecycle val coroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { coroutineScope.launch { viewModel.startReSendTokenTimer() } } //.... }
This ensures that any coroutines launched within this scope are automatically canceled when the LoginScreen composable is removed from the composition.
Disposable-effect
DisposableEffect() operates similarly to LaunchedEffect(). Both trigger their side effects upon entering the composition and rerun them when the specified keys change.
The difference is that DisposableEffect allows us to specify cleanup logic that will be executed when the composable is removed from the composition.
@Composable fun TrackingScreen() { val context = LocalContext.current val scope = rememberCoroutineScope() var data by remember { mutableStateOf<SensorMetaData?>(null) } DisposableEffect(Unit) { val sensorMetaManager = SensorMetaManager(context) sensorMetaManager.init() val job = scope.launch { sensorMetaManager.data .receiveAsFlow() .onEach { data = it } .collect() } onDispose { sensorMetaManager.unRegisterSensors() job.cancel() } } }
This is useful for scenarios where you need to perform cleanup or cancel ongoing operations when the composable is no longer needed.
Side-effects
It is used to perform side effects that don’t depend on the composable’s inputs. It’s useful for executing actions like logging, analytics, or interactions with external systems.
The SideEffect function allows you to perform side effects during composition. It is executed each time the composable is recomposed. For example:
@Composable fun LoginScreen() { SideEffect { Log.d("LoginScreen", "Starting login screen...") } }
Note: Avoid executing non-composable code within a composable function; Always use side effects for such tasks.
State Management with Kotlin Flow
Flows in Kotlin allow for the sequential retrieval of multiple values from coroutine-based asynchronous tasks. They are particularly useful for scenarios like receiving a stream of data over a network connection.
val devicesFlow: Flow<String> = flowOf("Android", "IOS", "Web")
Figure: devices String Flow in components
Producers : Responsible for providing the data that makes up the flow.
Intermediaries : Operators are applied to manipulate the flow stream between the producer and consumer.
Consumers : Collect and process the values emitted by the producer.
Types of Flows
Cold Flows: Producer code executes only when a consumer begins collecting values. It’s like a Water Tap that only releases water when someone turns it on.
Hot Flows : Immediately emit values upon creation, regardless of consumer status.
Ways of Creating Flow
Using the flow builder function
You can create a flow using the flow builder function. Inside the flow block, you can emit values using the emit function.
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow fun simpleFlow(): Flow<Int> = flow { for (i in 1..5) { emit(i) } }
2. Converting collections to Flow
You can convert collections (such as lists or arrays) to a Flow using the asFlow() extension function.
import kotlinx.coroutines.flow.asFlow fun listToFlow(): Flow<Int> { val list = listOf(1, 2, 3, 4, 5) return list.asFlow() }
3. Using flowOf function
You can use the flowOf function to create a Flow with predefined values.
import kotlinx.coroutines.flow.flowOf fun predefinedFlow(): Flow<Int> = flowOf(1, 2, 3, 4, 5)
StateFlow and SharedFlow:
StateFlow and SharedFlow are both implementations of hot flows that can be used for managing state and sharing data between different parts of the application.
StateFlow represents a flow of state values that emits the latest value immediately and then only emits subsequent values upon change.
SharedFlow allows multiple collectors and can buffer and replay values for new subscribers.
Converting a flow from Cold to Hot
A cold flow can be made hot by calling the shareIn() function on the flow. This call requires a coroutine scope in which to execute the flow, a replay value, and a start policy setting indicating the conditions under which the flow is to start and stop. The available start policy options are as follows:
SharingStarted.WhileSubscribed() — The flow is kept alive as long as it has active subscribers.
SharingStarted.Eagerly() — The flow begins immediately and remains active even in the absence of active subscribers.
SharingStarted.Lazily() — The flow begins only after the first consumer subscribes and remains active even in the absence of active subscribers.
fun getColdDevicesFlow(): Flow<String> = flow { val devices = listOf("Android", "iOS", "Web", "Smart TV") for (device in devices) { emit(device) delay(2000) // Emit every 2 second } }.shareIn(viewModelScope, SharingStarted .WhileSubscribed(replayExpirationMillis = 0))
MutableStateFlow
It is a type of state flow in Kotlin coroutines, often used in Jetpack Compose for managing mutable states. Unlike regular state flows, MutableStateFlow allows you to change its value programmatically using its value property. This makes it ideal for representing mutable states in your app. It emits values sequentially over time and observers can track changes to its value.
class ProfileViewModel : ViewModel() { private val _deviceFlow = MutableStateFlow("Android") val deviceFlow: StateFlow<String> = _deviceFlow.asStateFlow() fun updateText(newText: String) { _deviceFlow.value = newText } } @Composable fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) { val deviceFlow = viewModel.deviceFlow.collectAsState() // Use deviceFlow.value in your UI }
Flows (Unlike LiveData ) don’t naturally understand Android’s lifecycle because they’re from Kotlin, not Android.
However, we can handle this by collecting flow values responsibly within lifecycle scopes using coroutines or other methods. It helps applications by facilitating asynchronous programming, state management, and composition of UI components reactively and efficiently.
We will look into the usages of Flow in the upcoming series. From requesting API to storing in the room & rendering in LazyColumn .
Hope you will see it in FYP! 🗿
Keep Learning, Keep Composing…
Kt. Academy Open Workshops