Search code examples
androidandroid-jetpack-composeandroid-jetpackside-effects

Side-effects in JetPack Compose


While I understand or think I understand, what side-effects are in jet-pack compose.

It's any work done in composable which escapes the scope of the composable function

I understand doing stuff like I/O operations or mutating a variable outside of function scope, giving reference and not clearing it (memory leak), or mutating a local variable that is not composable state - these all are side-effects, as they can lead to unexpected behavior and leaks because of recomposition, which is not deterministic and can run many times. And to take care of side-effects, we have effect-handlers.

Considering everything above, I need a bit of clarity in a few scenarios

  • Side-effect - in case of mutating object, does it holds true for any object other than compose object? As in compose states (mutableStateOf etc..) doesn't lead to side-effects? Why?
  • Why callbacks/state/event hoisting to your activity/fragment/viewmodel,does not lead to side-effect?

For example

@Composable
fun MyComposable(
viewModel:MyViewModel,
launchSomeActivity:()->Unit
){
   var state by remember { mutableStateOf("") }
   state = "some string" // not a side-effect?

   viewModel.someStringObject = "a" // it's a side-effect?

   launchSomeActivity() // it's a side-effect?

   when(val screenState = viewModel.screenState.collectAsState().value){
      is ScreenState.Success -> launchSomeActivity() // not a side-effect. why?
      is ScreenState.Error -> state="some String" // not a side-effect. why?
   }
}

I also recall reading somewhere, trigger side-effects from callbacks, such as onClick as that always executes on UI thread, or say calling some lambda from ViewModel.

Would like to understand the above scenario as well, as in how it prevents side-effects, calling a lambda or a callback?


Solution

  • The SideEffect doesn't imply that anything else isn't a side-effect. It is an escape-hatch for things should be considered and effect of composition but are not, or cannot be, expressed in the node tree.

    For example, Dialog uses SideEffect to communicated dialog properties, layout-direction and the dismiss callback from the Dialog parameters to the Android dialog created as a result of composition. This presents the illusion that Dialog is natively part of Compose even though it uses the view system to create the dialog.

    In general, a composition function should only read state, not modify state. If it does modify state it should be only to state created during composition and used by the composable functions called directly by the composable function that created the state; and only before any of the children read the state, and not after.

    Side-effect - in case of mutating object, does it holds true for any object other than compose object? As in compose states (mutableStateOf etc..) doesn't lead to side-effects? Why?

    Composable functions should only read the state in these objects; it should not be modifying this state. For observable state, such as mutableStateOf, Compose observes changes to this state and will schedule a recomposition of the composable functions that read the state. You can use mutableStateOf to perform side-effects; you shouldn't.

    Why callbacks/state/event hoisting to your activity/fragment/viewmodel,does not lead to side-effect?

    Not if they are used correctly, no; not from Compose's perspective. Composition functions are a transform function from the data they read to user interface tree. As the view model changes, the transform is incrementally re-evaluated to produce the UI implied by the new state of the view model. State hoisting allows higher-level parts of the application that have more context over how the data should be stored and/or validated, to control the state of lower-level, more generic parts of the application. The components that have hoisted state should never write to the state. If they need to state to be changed they should call an event handler with the new desired state or with a description of what change to make (e.g. DeleteCustomer #1234). It should be up to the provided event handlers to actually make the change to the hoisted state.

    state = "some string" // not a side-effect?

    No, it is not. It initializes state as state does not escape the composition is considered part of the composition. This is not recommended practice but it is not a side-effect.

    launchSomeActivity()

    This is a side-effect and should be performed in LaunchEffect which will launch the co-routine in a scope that will automatically be cancelled when the composition function is no longer called in the composition.

    is ScreenState.Success -> launchSomeActivity() // not a side-effect. why?

    This is a side effect for the same reason with the same answer.

    is ScreenState.Error -> state="some String" // not a side-effect. why?

    This is not a side-effect as mentioned above (i.e. a side-effect implies it has an effect to something outside of composition, this doesn't), but, if state has already been read then it is considered a backwards write and heavily discouraged as it could cause composition to be repeated, perhaps indefinitely; as anything that has already read it might be rescheduled to be executed again.