1

I want to change variable value or call function according to lifecycle in jetpack compose. I tried according to this answer. Also read from the doc. I did this without any problem.

I have @Composable function

@Composable
fun BluetoothConnectionContentStateful(
    context: Context = LocalContext.current,
    viewModel: PairViewModel = getViewModel()
) {
    var selectedIndexOfAvailableItem by remember { mutableStateOf(DEVICE_NOT_SELECTED_INDEX) }
    val rememberPairScreenState = rememberConnectionScreenState(context, viewModel)
    val bluetoothEnableState by remember { derivedStateOf { viewModel.isBluetoothEnabled } }
    LaunchedEffect(key1 = bluetoothEnableState) {
        viewModel.handleBluetoothScanState(rememberPairScreenState.bluetoothAdapter)
    }

    LaunchedEffect(key1 = selectedIndexOfAvailableItem) {
        viewModel.handleTimeWarning(selectedIndexOfAvailableItem)
    }

    ComposableLifecycle { source, event ->
        if (event == Lifecycle.Event.ON_PAUSE) {
             viewmodel.stopScan()
        } else if (event == Lifecycle.Event.ON_RESUME) {
          viewModel.handleBluetoothScanState(rememberPairScreenState.bluetoothAdapter)
        }
    }
}

ComposableLifecycle

@Composable
fun ComposableLifecycle(
    lifeCycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onEvent: (LifecycleOwner, Lifecycle.Event) -> Unit
) {
    DisposableEffect(lifeCycleOwner) {
        val observer = LifecycleEventObserver { source, event ->
            onEvent(source, event)
        }
        lifeCycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifeCycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

PairViewModel

class PairViewModel : BaseViewModel() {

    companion object {
        private const val WARNING_PERIOD_IN_MILLES: Long = 60000 // For 1 minute scan
        const val DEVICE_NOT_SELECTED_INDEX = -1
    }

    var isBluetoothEnabled by mutableStateOf(false)
        private set

    private var job = Job()
        get() {
            if (field.isCancelled) field = Job()
            return field
        }

    fun startScan(adapter: BluetoothAdapter) = viewModelScope.launch {
        // start scan
    }

    fun stopScan(adapter: BluetoothAdapter) {
       // stop scan
    }


    fun handleBluetoothScanState(bluetoothAdapter: BluetoothAdapter) {
        if (isBluetoothEnabled) {
            startScan(bluetoothAdapter)
        } else {
            cancelTimeWarning()
            stopScan(bluetoothAdapter)
        }
    }


    fun handleTimeWarning(selectedIndexOfAvailableItem: Int) {
        if (selectedIndexOfAvailableItem == DEVICE_NOT_SELECTED_INDEX) {
            startTimeWarning()
        } else {
            cancelTimeWarning()
        }
    }

    fun startTimeWarning() {
        viewModelScope.launch(job) {
            delay(WARNING_PERIOD_IN_MILLES)
            // make event here
        }
    }

    internal fun cancelTimeWarning() {
        if (job.isActive) {
            job.cancel()
        }
        // make event here
    }
}

Note bluetoothEnableState is Bluetooth ON and OFF i.e. true and false respectively.

I am listening bluetoothEnableState using LaunchedEffect when bluetooth state change ON or OFF. This is working fine. Now the problem occur,

1. when activity start the viewModel.handleBluetoothScanState(rememberPairScreenState.bluetoothAdapter) is called two times on onResume and LaunchedEffect.

2. when user disable bluetooth it state change to false and he goes to onPause. After coming back onResume handleBluetoothScanState will call and also change the bluetooth state to true then again call the LaunchedEffect.

I will explain more detail of 2nd condition

On Resume             Close App   Open App
Bluetooth disable ->  On Pause -> OnResume -> Bluetooth enable
      |                              |            |
     \ /                            \ /          \ /
Launcheffect                      function     Launcheffect 

So I want to prevent to call double time function so any idea how can we solve this? Thanks

z.g.y
  • 5,512
  • 4
  • 10
  • 36
Kotlin Learner
  • 3,995
  • 6
  • 47
  • 127
  • Isn't that the expected outcome?, `onResume` will be called when you minimized the app and go back, and since based on your diagram, `LaunchedEffect's` boolean `key` changes from false to true, it will also trigger. – z.g.y Nov 25 '22 at 08:58
  • yes but can we automise the logic? – Kotlin Learner Nov 25 '22 at 09:13
  • why do you need to call `viewModel.handleBluetoothScanState` in `onResume`? when `LaunchedEffect` already reacts to bluetooth ON(`true`) and OFF(`false`) ? – z.g.y Nov 25 '22 at 09:24
  • 1
    I don’t have much experience with Compose yet, but my understanding is Composables are intended for creating UI, not doing all the behavior of your app. LaunchedEffects should be rarely used, but you seem to be trying to use them all over, frequently. It seems the things you are doing in the LaunchedEffects above you should be managing completely outside any Composable. – Tenfour04 Nov 25 '22 at 09:29
  • 1
    In this case you are involving the composable in telling the ViewModel that its own property has changed. Seems to me that should be handled entirely within the ViewModel instead of creating code spaghetti. – Tenfour04 Nov 25 '22 at 09:32
  • @Tenfour04 you are right that I am doing so much code wrong here. I used `LaunchEffect` because I am changing the code outside. – Kotlin Learner Nov 25 '22 at 09:36
  • @Tenfour04 I did all business logic in view-model except this `LaunchEffect` logic. – Kotlin Learner Nov 25 '22 at 09:37

1 Answers1

1

I don't understand your use-case and I can't compile your code, but since you only want to startScan when blueTooth is ON and stopScan when blueTooth is OFF, I think you don't need to handle the same call/logic in your onResume

// I don't think you need it here,
else if (event == Lifecycle.Event.ON_RESUME) {
      viewModel.handleBluetoothScanState(rememberPairScreenState.bluetoothAdapter)
}

since LaunchedEffect already reacts to bluetooth state changes ON(true)/OFF(false) and its already calling the same function in your ViewModel.

z.g.y
  • 5,512
  • 4
  • 10
  • 36