How to make LaunchedEffect run once and never again? - android

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
}

Related

NetworkOnMainThreadException when used as a LaunchedEffect key

When I use
LaunchedEffect(Dispatchers.IO)
I get,
NetworkOnMainThreadException
How should I use this function to run on background thread?
this is my code:
LaunchedEffect(Dispatchers.IO) {
val input = URL("https://rezaapp.downloadseriesmovie.ir/maintxt.php").readText()
println(input)
}
I'm using it inside my jetpack compose project
LaunchedEffect is one of the many Side Effects in Jetpack Compose, but instead of just explaining, it would be better for us to just have very simple compose use-case. Though I'm expecting that you already know what is re-composition and how a MutableState triggers it.
What we'll have:
a screen with a button in the middle
a MutableState increment-able integer value
a Log statement inside LaunchedEffect
What we'll do
click the button and increment the MutableState integer value
print the incremented value
What we'll expect
Logcat will display the value coming from LaunchedEffect, even before clicking the button
Our simple Composable
#Composable
fun ComposeSample() {
var intNum by remember {
mutableStateOf(0)
}
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(onClick = {
intNum++
}) {
Text(
text = "Increment the integer"
)
}
}
LaunchedEffect(Unit) {
Log.e("IntNumber", "Current value: $intNum")
}
}
At this point, pay attention to the key I supplied with the LaunchedEffect.
When the screen is rendered for the first time, LaunchedEffect will trigger and we'll see a logcat print.
E: Current value: 0
But when I click the button it doesn't show the incremented value. Because LaunchedEffect needs a key that will change if you intend to trigger it every re-composition.
Now I changed the key I supplied to LaunchedEffect using the MutableState intNum variable,
LaunchedEffect(intNum) {
Log.e("IntNumber", "Current value: $intNum")
}
every click the logcat prints, because every time the intNum changes, the LaunchedEffect is triggered and triggers the Logcat statement.
E: Current value: 0
E: Current value: 1
E: Current value: 2
E: Current value: 3
When a key of a LaunchedEffect has changed and a composition happens, it will trigger anything inside its block.
I'm not sure what you are trying to achieve with your posted code, I don't even know how did it happen, but I suppose there aren't any use-case (to the best of my knowledge) where you will use a specific coroutine Dispatcher as a LaunchedEffect key.
Let me give it to you straight and clear,
lifecyclescope, or any coroutine scope in compose ui is on main
thread by default.
Especially for the compose coroutine scope you can not change their
dispatcher,
I strongly suggest that you call ViewModel method and there you launch the coroutine in side viewmodelSope, also keep in mind that the compose coroutine scopes are for light suspending operation, do not perform any heavy lift in those scopes.

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.

Avoid UI Blocking while running an operation that requires to be run on the main thread

I use an API that has a method, let's say run() that must be executed on the main thread, or else it'll throw an exception. Now, I've tried coroutines, and it doesn't work in a standard launch{...} block, which is understandable. Now, since it is a little long running task, I wish to show a UI to the user, indicating the same, i.e., a process is taking place. Now, I do not require assistance on the animation logic, but I cannot understand how is the animation supposed to keep up alongside all the heavy IO stuff that may be going on on the main thread.
Also, I've been experiencing some very odd behaviour in this Composable. Kingly have a look,
#Composable
fun CustomC() {
var trigger by remember {
mutableStateOf(false)
}
val color by animateColorAsState(targetValue = if (trigger) Color.Gray else Color.Cyan)
Surface(Modifier.fillMaxSize()) {
Text(
modifier = Modifier.background(color),
text = "Running"
)
}
// I tried this but this seems to produce a crash, indicating that the run method is not running on the main thread, but how? Removing this LaunchedEffect removes the error.
// LaunchedEffect(Unit){
// delay(2000)
// trigger = true
// }
run() // Must be executed on the Main Thread
}
This app crashes if I put that LaunchedEffect block over there, but it is not even interacting with run() in any way, per my knowledge. And another strange behaviour is as follows:
#Composable
fun CustomC() {
var trigger by remember {
mutableStateOf(false)
}
val color by animateColorAsState(targetValue = if (trigger) Color.Gray else Color.Cyan)
Surface(Modifier.fillMaxSize()) {
Text(
modifier = Modifier.background(color),
text = "Running"
)
}
trigger = true
run() // Must be executed on the Main Thread
}
Now, you would expect the Composable to be turning Cyan before the run method is called, right? IT DOESN'T!! IT JUST DOESN'TTTT
It just starts executing run() and then finally AFTER the method is done executing, the Composable turns cyan. This clearly implies that the recompositions are blocked while the method is running, so all I need is a way to get around that.
EDIT: An important piece of information that I missed earlier, when I call the run() method inside of a LaunchedEffect, the method seems to work fine, i.e., the app doesn't crash, but the UI is still blocked.
Also, if I call the method inside a launch block WITHIN a LaunchedEffect, the same thing happens as above, where the method runs fine but the UI is clogged. What is the role of launch here then?
FINAL EDIT: A very rare thing I saw in this scenario was when the crash appeared, it did not throw any sort of an exception. It just raised an error like so:
Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x2f6000412f6018 in tid 29693
I got this error from my search history, and it pointed me to some things related to programming in the NDK, which I do not. Also, now no matter what I do, the error won't come up. Seems like a glitch in Android or Studio.
Since the cause of the crash seems to be the method was not given access to I/O by the system, the problem was resolved using a coroutine with I/O access.
Now, in my ViewModel, I wrapped the run method in a coroutine like this
suspend fun runWithIO(){ // custom method
withContext(Dispatchers.IO){
run() // The original method, provided by the API
}
}
This satisfies all the constraints, whatsoever - Runs on a thread with I/O access, does not block the Main Thread, uses best practices like coroutines without any side-effects.
So, the explanation was posted by Johann in the first comment on the original post which has just been adapted to my specific use-case here.
So, if an API that you might be using states that the methods are supposed to be run only on the main thread, you should probably play around for a while with different coroutine contexts, since the API may only require the main thread to perform I/O operations, which can also be performed in a simple coroutine like this. If the method does indeed require the main thread, there's also Dispatchers.Main to assist you with that, you can run only the required part of a function on the Main Thread, but please note, this call WILL BE blocking.

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.

Using mutableStateOf instead of observeAsState

I'm working with Jetpack Compose in an Android app and had the problem that my uiState (LiveData) was set to its initial value on every recomposition, since I've initialized it like
val authUiState: AuthUIState by authenticationViewModel.uiState.observeAsState(AuthUIState.Loading)
It was set to Loading on every recomposition before it was set to the correct value.
When I tried to Remember the value, I learned that we can't use observeAsState within the remember block and finally changed it to
val authUiState = remember{ mutableStateOf(authenticationViewModel.uiState.value) }.value
This works, but I'm not quite sure, if this is the common and good way to solve this.
What do you think? Should I do it differently? Do you need more information?
See if the uiState inside your viewmodel is something like a LiveData Object, (which is kinda what it seems like from the code), the recommended way is to store it in the viewmodel itself as mutable state.
var uiState by mutableStateOf (initialValue)
private set //Do not allow external modifications to maintain consistency of state
fun onUiStateChange(newValue: Any){
uiState = newValue
}
You just need to initialise it as a MutableState, in the rest of the code, to update, delete or whatever you want to do with it, just treat it as a regular variable. Compose will trigger recomposition every time the value is updated.
The following code snippet below will almost certainly not work, because here, the state is whatever you wrap inside mutableStateOf(), which is just a simple value which will be fetched once from the viewmodel and then remembered throughout recompositions, so no code change will be triggered here
val authUiState by remember{ mutableStateOf(authenticationViewModel.uiState.value) }
Storing state in the viewmodel as mutableState, is as far as my knowledge extends, the best practice in compose. You will see the same in the 'State Codelab' from Android developers
Good luck

Categories

Resources