Home > Mobile >  How to properly use DisposableEffect in Jetpack Compose
How to properly use DisposableEffect in Jetpack Compose

Time:10-18

I want to listen for sensor data in a @Composable and cancel listening for sensor data when returning to the previous interface.

So I wrote the following code:

PresetsList {
  navigator.navigate(
    PresetsEditorDestination(
      orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE,
      presetsModel = it
    )
  )
}

PresetsList contains multiple jumpable PresetsEditor interfaces

@OptIn(ExperimentalLifecycleComposeApi::class)
@Destination
@Composable
fun PresetsEditor(
  orientation: Int,
  presetsModel: PresetsModel,
  viewModel: EditorViewModel = hiltViewModel()
) {
  val context = LocalContext.current
  val systemUiController = LocalSystemUiController.current
  val dialogState by viewModel.dialogState.collectAsState()
  val steeringValue by viewModel.sensorFlow.collectAsStateWithLifecycle()

  systemUiController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

  DisposableEffect(Unit) {
    
    // Force this @Composable to be landscape and hide the statusBar.

    val activity = context.findActivity() ?: return@DisposableEffect onDispose {}
    val originalOrientation = activity.requestedOrientation
    activity.requestedOrientation = orientation
    systemUiController.isSystemBarsVisible = false

    onDispose {
      activity.requestedOrientation = originalOrientation
      systemUiController.isSystemBarsVisible = true
      viewModel.stopListeningSensor() // stop listening sensor.
    }
  }

  println("steeringValue: $steeringValue")

  EditorContent(
    presetsModel, 
    dialogState, 
    viewModel::onClickLabel, 
    viewModel::onDismissRequest
  )
}

viewModel.kt

@HiltViewModel
class EditorViewModel @Inject constructor(
  private val sensor: AbstractSensor
) : ViewModel(){

  private val _sensorFlow = MutableStateFlow(0f)
  val sensorFlow = _sensorFlow.asStateFlow()

  init {
    sensor.startListening()
    sensor.setOnSensorValuesChangedListener {
      _sensorFlow.value = it
    }
  }
}

But I encountered a problem. When I jump to this @Composable from other interfaces, the code in my onDispose will run the stopListeningSensor method once, which will cause my sensor data to end shortly after I start listening. So I'm a little lost on how to use DisposableEffect to solve this problem.

CodePudding user response:

You're reading the value of steeringValue inside your Composable, which is a value bound to change, as you reveal. What's the invisible issue here? Obviously the change in the value will trigger a recomposition leading to onDispose being called, which is the expected behaviour. If all you want is to run a codeblock when the Composable goes out-of-composition, then it would be easier to place that codeblock at the place where you make the navigation call. It might be a little risky to place it within the Composable without proper knowledge and handling of scopes and side-effects. One way I see to add it within the Composable while taking care of all of that, is to use the BackHandler Composable. It receives a lambda that would only be executed when the user presses the back button.

BackHandler { ... }

Move the `onDispose logic to one of these blocks, and it should work.

There's no way to know when a Composable goes out-of-composition, so you're only bet is to place the logic in a custom-handled event, like the back press, or when the app is pushed to the background, using onStop, or to be the most accurate, place the call at the place where you call the navigation method, which is the architecture most normal developers would go with. You probably won't be able to perform the latter since you're using an out-of-earth library for navigation, so... Yeah, good luck to you sir/lady.

CodePudding user response:

I didn't describe my problem clearly, although I wrote comments in the code. My problem is jumping from a normal @Composable (in portrait) to a @Composable in landscape and after the jump starts listening for sensor data.

The reason why onDispose is triggered once when entering @Composable is actually because configuration is changed, and the life cycle of Activity is executed once ON_STOP (because of the change of screen orientation)

The solution is also simple: remove the init block in my viewModel as it doesn't need such a long life cycle (eg when the app is in the background, no need to listen for sensor data), and start listening for sensor data in the DisposableEffect block.

val activity = LocalContext.current as MainActivity

DisposableEffect(Unit) {

  // Force this @Composable to be landscape and hide the statusBar.
  val originalOrientation = activity.requestedOrientation
  activity.requestedOrientation = orientation
  systemUiController.isSystemBarsVisible = false

  viewModel.startListeningSensor()

  onDispose {
    activity.requestedOrientation = originalOrientation
    systemUiController.isSystemBarsVisible = true
    viewModel.stopListeningSensor() // stop listening sensor.
  }
}

Finally, I'd like to say to Richard Onslow Roper that the navigation library I'm using is fine, it's just that I stupidly didn't think that the lifecycle caused by screen changes affects the DisposableEffect (it does exactly what Richard Onslow Roper said in the first comment, My @Composable is destroyed) and doesn't clearly describe what's going on in my code.

  • Related