DerivedState inside SideEffect does not cause recomposition - android

In the scenario where I need to handle side effect and I need to pass firstItemVisibleIndex is bigger than 0 to the lambda function in the composable. Imagine I have this derivedState
val isFirstItemVisible by remember {
derivedStateOf {
state.firstVisibleItemIndex == 0
}
}
if I pass this state inside DisposableEffect works well but in DisposableEffect I do not need to clear anything or do something onDispose function. Documentation says that If you will not do anything on onDispose function use SideEffect. When I use SideEffect, isFirstItemVisible value only changed one time. When I go down, it becomes false and when we are on top, we expect it to be true but this value does not get changed because recomposition does not occur. Since recomposition does not occur, block of code inside SideEffect does not run. Why is that happening ?

replace your code with
val isFirstItemVisible by remember (state) {
state.firstVisibleItemIndex == 0
}
thus, the calculation of the isFirstItemVisible will occur only if the state was changed

Related

How to make LaunchedEffect run once and never again?

I know that LaunchedEffect(key) is executed when composition starts, and is also cancelled and re-executed again when the key changes. However, I want it to be executed only once and never again, I am using the following workaround but I feel like I am missing something:
if (!launchedBefore) {
LaunchedEffect(null) {
//stuff
launchedBefore = true
}
}
The code above works just fine. But it feels like a stretched workaround while something much simpler can be done. Am I misunderstanding how LaunchedEffect fully works?
I tried using null and Unit as keys cuz they never changed, but the code is executed every time a composition takes place.
LaunchedEffect(Unit) should execute only once when the composition starts. The only time it would get re-executed during recomposition is if it gets removed from the view tree during one of the previous recompositions. An example would be if you have it within a condition whose value changes at some point (in an if block, when block or any other conditional statement).
I would assume that the problem with recomposing lies in the other part of the code that is not shown in your snippet. Check if the LaunchedEffect is nested in a conditional block that might cause it to get executed after a recomposition.
The problem is not in the LaunchEffect or its key parameter, but in the upper composable. The upper composable is getting recomposed. Probably you should display more details how the LaunchEffect is called and calling sites.
Recomposition looks up for nearest composable that could have been affected by the change.
I usually pass a longer-living variable for the key, something like ViewModel. It will only executed when the ViewModel is being initialized. Or using remembered saveable boolean may do the trick.
val bool = rememberSaveable { true }
LaunchedEffect(key1 = bool) {
// do something
}

Use of LaunchedEffect vs SideEffect in jetpack compose

Hi guys I am learning side-effect in my project. I want to know when should I use LaunchedEffect and SideEffect in which scenario. I am adding some piece of code using both effect. Please lemme know if I am doing wrong here.
1st using LaunchedEffect, please guide me if we need effect on this function or not.
#Composable
fun BluetoothRequestContinue(multiplePermissionsState: MultiplePermissionsState) {
var launchPermission by remember { mutableStateOf(false) }
if (launchPermission) {
LaunchedEffect(Unit) {
multiplePermissionsState.launchMultiplePermissionRequest()
}
}
AbcMaterialButton(
text = stringResource(R.string.continue_text),
spacerHeight = 10.dp
) {
launchPermission = true
}
}
2nd using SideEffect to open setting using intent
#Composable
fun OpenPermissionSetting(router: Router = get()) {
val activity = LocalContext.current as Activity
var launchSetting by remember { mutableStateOf(false) }
if (launchSetting) {
SideEffect {
activity.startActivity(router.permission.getPermissionSettingsIntent(activity))
}
}
AbcMaterialButton(
text = stringResource(R.string.open_settings),
spacerHeight = 10.dp
) {
launchSetting = true
}
}
Please let me know if we need Effect or not. Also guide me if we need different effect as well. Thanks
There difference between
if (launchSetting) {
SideEffect {
// Do something
}
}
and
if (launchPermission) {
LaunchedEffect(Unit) {
multiplePermissionsState.launchMultiplePermissionRequest()
}
}
both enters recomposition when conditions are true but LaunchedEffect is only invoked once because its key is Unit. SideEffect is invoked on each recomposition as long as condition is true.
SideEffect function can be used for operations that should be invoked only when a successful recomposition happens
Recomposition starts whenever Compose thinks that the parameters of a
composable might have changed. Recomposition is optimistic, which
means Compose expects to finish recomposition before the parameters
change again. If a parameter does change before recomposition
finishes, Compose might cancel the recomposition and restart it with
the new parameter.
When recomposition is canceled, Compose discards the UI tree from the
recomposition. If you have any side-effects that depend on the UI
being displayed, the side-effect will be applied even if composition
is canceled. This can lead to inconsistent app state.
Ensure that all composable functions and lambdas are idempotent and
side-effect free to handle optimistic recomposition.
Sample from official docs
To share Compose state with objects not managed by compose, use the
SideEffect composable, as it's invoked on every successful
recomposition.
#Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
LaunchedEffect is for calling a function on composition or on recomposition if keys are changed.
If you write your LaunchedEffect as
LaunchedEffect(key1= launchPermission) {
if(launchPermission) {
// Do something
}
}
code block inside if will not be called in composition if key is not true but whenever it changes from false to true code block will be invoked. This is useful for one-shot operations that are not fired by user interaction directly or when an operation requires a CoroutineScope invoked after user interaction, animations or calling suspend functions such as lazyListState.animateScrollToItem()
Definition of concept of side-effect from Wikipedia
In computer science, an operation, function or expression is said to
have a side effect if it modifies some state variable value(s) outside
its local environment, which is to say if it has any observable effect
other than its primary effect of returning a value to the invoker of
the operation. Example side effects include modifying a non-local
variable, modifying a static local variable, modifying a mutable
argument passed by reference, performing I/O or calling other
functions with side-effects. In the presence of side effects, a
program's behaviour may depend on history; that is, the order of
evaluation matters. Understanding and debugging a function with side
effects requires knowledge about the context and its possible
histories.

How does backwards write in Jetpack Compose work?

We have the official docs showcasing an example of backwards write in Compose:
#Composable
fun BadComposable() {
var count by remember { mutableStateOf(0) }
// Causes recomposition on click
Button(onClick = { count++ }, Modifier.wrapContentSize()) {
Text("Recompose")
}
Text("$count")
count++ // Backwards write, writing to state after it has been read
}
It's clear to me that updating State object inside a composable like that causes a backwards write.
The stated effect of this backwards write is that we end up with an infinite cycle of recompositions.
However, what's interesting is that the count++ increment action at the bottom of the composable doesn't trigger an infinite cycle of recompositions. Basically, on the first composition of BadComposable, everything seems fine as there are no extra recompositions even though we've updated count at the end of the body of BadComposable.
If, however, you then press on the Button, the count++ increment action inside the onClick callback triggers an infinite cycle of recompositions.
Why do we have to press on the Button to trigger an infinite cycle of recompositions?
I was expecting that it's enough to read a value and update it in the body of the composable to get such an effect.
Jorge Castillo (thank you for your help) has hinted that it may be because the first update of the count state object happens before the initial recomposition scope of BadComposable is registered.
Source for this idea.

Activity Launcher(File picker) is loading multiple times in single event - Jetpack compose

I am using a file picker inside a HorizontalPager in jetpack compose. When the corresponding screen is loaded while tapping the button, the launcher is triggered 2 times.
Code snippet
var openFileManager by remember {
mutableStateOf(false)
}
if (openFileManager) {
launcher.launch("*/*")
}
Button(text = "Upload",
onClick = {
openFileManager = true
})
Edited: First of all Ian's point is valid why not just launch it in the onClick directly? I also assumed that maybe you want to do something more with your true false value. If you want nothing but launch then all these are useless.
The screen can draw multiple times when you click and make openFileManager true so using only condition won't prevent it from calling multiple times.
You can wrap your code with LaunchedEffect with openFileManager as a key. The LaunchedEffect block will run only when your openFileManager change.
if (openFileManager) {
LaunchedEffect(openFileManager) {
launcher.launch("*/*")
}
}
You should NEVER store such important state inside a #Composable. Such important business logic is meant to be stored in a more robust holder like the ViewModel.
ViewModel{
var launch by mutableStateOf (false)
private set
fun updateLaunchValue(newValue: Boolean){
launch = newValue
}
}
Pass these to the Composable from the main activity
MyComposable(
launchValue = viewModel.launch
updateLaunchValue = viewModel::updateLaunchValue
)
Create the parameters in the Composable as necessary
#Comoosable
fun Uploader(launchValue: Boolean, onUpdateLaunchValue: (Boolean) -> Unit){
LaunchedEffect (launchValue){
if (launchValue)
launcher.launch(...)
}
Button { // This is onClick
onUpdateLaunchValue(true) // makes the value true in the vm, updating state
}
}
If you think it is overcomplicated, you're in the wrong paradigm. This is the recommended AND CORRECT way of handling state in Compose, or any declarative paradigm, really afaik. This keeps the code clean, while completely separating UI and data layers, allowing controlled interaction between UI and state to achieve just the perfect behaviour for the app.

Why is LaunchedEffect(true) suspicious?

I'm working on implementing MVI using compose. In order for me to follow the proper event loop, I need to propagate clicks events through my view model and then observe side effects. I have looked at a few implementations and they all use LaunchedEffect(true) to observe side effects and take actions.
I have a similar setup for example:
#Composable
fun HelloComposeScreen(
viewModel: MyViewModel = hiltViewModel(),
onClickedNext: () -> Unit
) {
LaunchedEffect(true) {
viewModel.sideEffect.collectLatest { sideEffect ->
when (sideEffect) {
DashboardSideEffect.CreateParty -> onClickedNext()
}
}
}
Button(
onClick = { viewModel.onEvent(UserEvent.ClickedButton)},
) {
Text("Click Me")
}
}
This results in me using LaunchedEffect(true) for any screen that has navigation or one time events but the official documentation has this warning
Warning: LaunchedEffect(true) is as suspicious as a while(true). Even though there are valid use cases for it, always pause and make sure that's what you need.
My questions are:
When exactly does the LaunchedEffect get canceled? The documentation says that it matches the lifecycle of the call site. Is that the composition in this case?
Considering that the official documentation has a warning there? Should I not be using this LaunchedEffect(true) setup for observing side effects through my project? What would be an alternative?
The LaunchedEffect is canceled along with its coroutine in two variants:
The passed key argument(s) is changed - in this case the current LaunchedEffect will be cancelled and a new one will be created.
LaunchedEffect is removed from the life tree, for example, in case you put it (or its parent at any level) in an if block and the condition becomes false.
If you do not need to pass any key that should restart LaunchedEffect, you can pass Unit. Any other constant, like true in your case, is considered suspect because it cannot be changed at runtime and yet may look like complex logic to any coder.
The LacunchedEffect is a Composable function and it runs coroutines in a coroutineScope.
The coroutineScope will be canceled and restarted in two cases:
When the passed keys to LaunchedEffect gets changed. Changing a passed key from value x to y cancels the current coroutineScope, and then launching the block of code inside LaunchedEffect again with a new passed keys.
When the LaucnhedEffect exits the composition. That means in a later composition if the LaucnhedEffect does not recompose. For example, because it's inside an if statement that gets evaluated as false or if one of the parent composables in the composition tree exits the composition.
Example:
#Composable
fun MyComposable(authorId: Int, showReadMore: Boolean) {
// ... logic....
// When showReadMore is false, the latest LaunchedEffect composable exits the composition (the coroutineScope will be cancelled)
if (showReadMore) {
// Changing the value of authorId when showReadMore is true, cancels the coroutineScope and launch the block again.
LaunchedEffect(authorId) {
// Get more info of the author using suspend function
// Since we use LaunchedEffect to run suspend function(s) inside
}
}
}
For the second question: Passing any value like false, true, 1, 2, and Unit gives the same result. Passing Unit makes the code more sense and easier to read because it indicates void which means that we don't care about restarting the coroutineScope in the first case (when keys changes) because the keys are void.

Categories

Resources