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.
Related
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
}
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.
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.
I have a top-level composable representing some app screen, which uses 2 view-models (for clarity). ViewModel_B starts some network activity once created:
Once network request finishes, it updates state_B: MutableStateFlow inside ViewModel_B which is observed from our composable. And that state is, for example, Loading | Error | Success enum, and let's say Success contains some payload:
When it's Success, I want my ViewModel_A to start another activity, which would in turn affect another state_A: MutableStateFlow, which we also observe and show UI based on it:
#Composable
fun MainScreen(
viewModel_A = viewModel { factory_A },
viewModel_B = viewModel { factory_B }.apply { networkRequest_B() }
) {
val state_B by viewModel_B.state_B.collectAsState()
when (val sB = state_B) {
Loading -> LoadingScreen() // Composable
Error -> ErrorScreen() // Composable
is Success -> {
val state_A by viewModel_A.state_A.collectAsState()
viewModel_A.doAction(sB.payload) // changes state_A internally
when (val sA = state_A) {
// other composables based on state_A values
}
}
}
}
When I run this code, the whole MainScreen() method gets called again and again in an infinite loop. Hence, two questions:
When should I start observing state_A - as close as possible to the place, where it is relevant? I mean, there is no sense to expect any values from it if state_B was Loading | Error, since there is no screen for state_A. Should I put viewModel_A.state_A.collectAsState() where it is now or I should hoist it top?
I believe, infinite loop is due to improper usage of Jetpack Compose here. It recomposes and runs viewModel_A.doAction() every recomposition, which will affect inner composables and everything would repeat. I believe, I should use rememberCoroutineScope() here OR LaunchedEffect {}, but I'm not sure how to do this properly.
What I see from LauchedEffect docs is that it's recomposed when key1 changes. I actually want to depend on sB.payload when running doAction(), but docs discourages me from passing sB.payload for key1.
For rememberCoroutineScope() - not sure, how to use it in my case, and also - where should I declare it - top or inside is Success -> {} block?
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.