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?
Related
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 am still new to compose and I am curious how people treat this kind of thing.
Let's imagine that we have a screen that has two variants, one variant with some views, another variant with other views. That variant should be dictated by a persisted flag, which I have stored using DataStore (the new SharedPrefs). The only issues is that unlike SharedPrefs, DataStore is asynchronous and is made to work with coroutines. So here's what happens, the screen gets rendered in default state (variant A) for just a split second, atfer about 100-200ms the viewModel successfully reads the value from DataStore on a coroutine and posts it on a mutableStateOf(), which as a result triggers recomposition with the variant B of the screen that is saved in the prefs. This transition is visible and the entire behavior looks glitchy. How do you fix this? I don't want the screen to compose the default state before the viewModel has time to read the stored value, I want the screen to await those 100-200ms without doing anything and composing the views only after the reading from DataStore.
The code looks like:
#Composable
fun MyScreen(){
val viewModel = hiltViewModel<ScreenViewModel>()
val state = viewModel.uiState
if(state == MyScreenState.A){
[...] // some view here
} else {
[...] // other view here
}
}
#HiltViewModel
class ScreenViewModel #Inject constructor(
private val dataStoreService: DataStoreService
) : BaseViewModel() {
var uiState by mutableStateOf(MyScreenState.A)
init {
viewModelScope.launch {
dataStoreService.flag().collect { flag ->
uiState = if(flag) MyScreenState.A else MyScreenState.B
}
}
}
}
For simplicity, MyScreenState is just a simple enum in this case. One of the things I thought about is defaulting the uiState to null instead of variant A and in my screen check if the state is null and if so returning a Unit (basically rendering nothing). If the state is not null, render the screen accordingly. But the truth is that I don't feel like making that uiState nullable, I avoid working with nulls at all cost because they make the code just a little less readable and needs extra handling. What's your solution on this? Thanks.
Instead of creating nullable state create another state that represents nothing being happening. I generally use Idle state to set as initial or a UI state when nothing should happen. I also use this approach for fire once events after event is invoked and processed.
var uiState by mutableStateOf(MyScreenState.Idle)
it will be a loading or a blank screen depending on your UI
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.
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.
Checking out codelab's basic tutorial there is a snippet to increase counter on button when clicked
#Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
Column(modifier = Modifier.weight(1f)) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
colors = ButtonConstants.defaultButtonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text("I've been clicked $count times")
}
}
It is clear that remember { mutableStateOf(0) } stores the state/value. My question is what remember does under the hood. Using var count = remember { 0 } or mutableStateOf(0) without remember does not increase the value.
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
var count = remember { 0 }
Column(modifier = Modifier.fillMaxHeight()) {
Column(modifier = Modifier.weight(1f)) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
Counter(
count = count,
updateCount = { newCount ->
count = newCount
}
)
}
}
Snippet above does not update the value printed on Text, does remember only work with MutableState?
remember - allows you to remember state from previous recompose invocation and just this.
So if you for instance randomize color at initial run. The randomized color will going to be calculated once and reused whenever re-compose is necessary.
so ...
remember = store value just in case recompose will be called.
Now the second thing is knowing when re-compose should be actually triggered.
and there mutable states comes to help.
mutablestate = store the value AND in case i update value trigger recompose for all elements using this data.
To learn how composition and recomposition works you can check out Under the hood of Jetpack Compose article by Leland Richardson, which describes inner works very well, also youtube video here. And most of this answer uses article as reference and quoted most from it.
The implementation of the Composer contains a data structure that is closely related to a Gap Buffer. This data structure is commonly used in text editors.
A gap buffer represents a collection with a current index or cursor. It is implemented in memory with a flat array. That flat array is larger than the collection of data that it represents, with the unused space referred to as the gap.
Basically adding space near your Composable function slot table to be able to update UI dynamically with high costs since get, move, insert, and delete — are constant time operations, except for moving the gap. Moving the gap is O(n) but this does not happen often which you need to change all UI structure, on average, UIs don’t change structure very much.
#Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
}
When the compiler sees the Composable annotation, it inserts additional parameters and calls into the body of the function.
First, the compiler adds a call to the composer.start method and passes it a compile time generated key integer.
fun Counter($composer: Composer) {
$composer.start(123)
var count by remember($composer) { mutableStateOf(0) }
Button(
$composer,
text="Count: $count",
onPress={ count += 1 },
)
$composer.end()
}
When this composer executes it does the following:
Composer.start gets called and stores a group object
remember inserts a group object
the value that mutableStateOf returns, the state instance, is stored.
Button stores a group, followed by each of its parameters.
And then finally we arrive at composer.end.
The data structure now holds all of the objects from the composition, the entire tree in execution order, effectively a depth first traversal of the tree.
So remember needed to store a mutableState() to get value from previous composition and mutableState() is required to trigger one.
And MutableState interface uses #Stable annotation
#Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
Stable is used to communicate some guarantees to the compose compiler about how a certain type or function will behave.
When applied to a class or an interface, Stable indicates that the following must be true:
The result of equals will always return the same result for the same two instances.
When a public property of the type changes, composition will be notified.
All public property types are stable.
When applied to a function or a property, the Stable]annotation indicates that the function will return the same result if the same
parameters are passed in. This is only meaningful if the parameters
and results are themselves Stable, Immutable, or primitive.
The invariants that this annotation implies are used for optimizations by the compose compiler, and have undefined behavior if
the above assumptions are not met. As a result, one should not use this annotation unless they are certain that these conditions are satisfied.
Another source with a Video that describes how Compose works.
Codelab example mentions about remember and mutableState as
Reacting to state changes is at the very heart of Compose. Compose
apps transform data into UI by calling Composable functions. If your
data changes, you recall these functions with the new data, creating
an updated UI. Compose offers tools for observing changes in your
app's data, which will automatically recall your functions—this is
called recomposing. Compose also looks at what data is needed by an
individual composable so that it only needs to recompose components
whose data has changed and can skip composing those that are not
affected.
Under the hood, Compose uses a custom Kotlin compiler plugin so when
the underlying data changes, the composable functions can be
re-invoked to update the UI hierarchy.
To add internal state to a composable, use the mutableStateOf
function, which gives a composable mutable memory. To not have a
different state for every recomposition, remember the mutable state
using remember. And, if there are multiple instances of the composable
at different places on the screen, each copy will get its own version
of the state. You can think of internal state as a private variable in
a class.
remember{X} and remember{mutableStateOf(X)} are useful in different scenarios.
First one is required when your object doesn't need to be instantiated at each recomposition, and there is another trigger that triggers composition.
An example for this is remember{Paint()}, any object that doesn't need to be instantiated more than once or memory heavy to instantiate. If a Composable that possesses this object is recomposed, properties of your object don't change thanks to remember, if you don't use remember your object is instantiated on each recomposition and all the properties previously set are reset.
If you need a trigger(mutableStateOf) and need to have the latest value(remember) like in question choose remember{mutableStateOf()}
Variables are cleared on every compositon.
Using remember will get the previous value.
I think its equivalent to declare a mutableState in ViewModel.