The Lesson we Skipped in Jetpack Compose ☄

The Lesson You Skipped in Jetpack Compose ☄

Core concepts of Composables and its helping hands — Episode III

We have used Composable in the last episodes of the Jetpack Compose Series. Now, Let’s see what happens under the hood.

When we annotate @Composable we tell the Compose compiler that this function is meant to convert data into UI elements. This rendering process happens in three key phases:

  1. Composition: Determines what UI elements to display. Compose execute composable functions to create a blueprint of UI.
  2. Layout: Determines where to position UI elements. This phase includes measurement and placement steps, where elements are sized and positioned within a 2D coordinate system.
  3. Drawing: Renders the UI elements onto a Canvas/ Device Screen.
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:

  1. Suspended Effect Handler: for suspending functions.
     — LaunchEffect
     — rememberCoroutineScope
  2. 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

  1. 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

The Lesson we Skipped in Jetpack Compose ☄ was originally published in Kt. Academy on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue ReadingThe Lesson we Skipped in Jetpack Compose ☄

Read and learn: Technical reviewers wanted & 2 Kotlin Coroutines Articles & Android art.

Read and learn: Technical reviewers wanted & 2 Kotlin Coroutines Articles & Android art. & Persistent memory & No-code & Coroutines Workshop 👌

Hi there Code-lovers! 🌞

New book series coming soon! 📗📘📗
Marcin Moskała started working on 3 books called Kotlin for developers.
Wanna contribute? 🤔
More information: 👇

🔎 Technical reviewers wanted 🔍
👉
Kotlin for Developers — technical reviewers wanted!

👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻👨‍💻👩‍💻

Below you can find a brief list of today’s newsletter content:

Kotlin Coroutines articles written by Marcin Moskała:
👉 Job and children awaiting in Kotlin Coroutines
👉 Kotlin Coroutines dispatchers

Articles are the parts from the Kotlin Coroutine book. You can find the whole book here. 📖

Android’s Apps article written by Kashif Mehmood:
👉 CI/CD Pipeline for Flavoured Android Apps using Fastlane and Github Actions

Kashif submitted a story to our blog. Thank you! 💪
Rember that, we are always open to new authors and valuable content.
More info on our Medium 📝

Persistent memory article series prepared by Sarthak Makhija 🧠
👉 Persistent memory — Introduction
👉 In-memory dictionary
👉 Introducing persistent memory

Sarthak offered us a completely new topic. We are so glad we could help him with publishing on our website.
Would you like to share with us something interesting you have written?
Write to kasia@kt.academy 📧
More articles coming soon! 📝

Be or not to be? Code or No-code? written by Marcin Moskała 💭
👉 Code or No-code?

❗ Kotlin Coroutines open workshop ❗ Registration is still open!

Ready, steady, go! 🙃


Read and learn: Technical reviewers wanted & 2 Kotlin Coroutines Articles & Android art. was originally published in Kt. Academy on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue ReadingRead and learn: Technical reviewers wanted & 2 Kotlin Coroutines Articles & Android art.

Most Common Android Problems  — Android Pitfalls

Most Common Android Problems — Android Pitfalls 🐭 🧀

Photo by Lauren Mancke on Unsplash

What does happens when a screen is rotated? Before we think about this question let’s try to understand how does Android deal with that.

When a user rotates their phone the configuration of the Activity changes, so android OS destroys the Activity and recreates it again.

Behind the scenes; when the device orientation changes, first the Activity will disappear for a millisecond when the onPause, onStop, and onDestroy methods are called. After a few milliseconds, the activity will be restarted and the onCreate, onStart, and onResume methods are called.


Most Common Android Problems  — Android Pitfalls 🐭 🧀 was originally published in Kt. Academy on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue ReadingMost Common Android Problems  — Android Pitfalls

End of content

No more pages to load