user13756297
user13756297

Reputation: 85

Reset mutableStateOf if the initial value changes - Jetpack Compose

I am new to Jetpack Compose and I just started trying it out by creating a timer application but I ran into a problem.

In my application each task has a different duration and after one of them is finished (the time has elapsed) then the next one should start. My problem is that my app works correctly only for the first task. After the first task is finished and the second one should be displayed the task title and description change but the timer's countdown value remains 0, it doesn't update from the previous state.

The onFinished calls one of the viewmodel's method that will fetch the next task that should be displayed and loads it into a livedata. I observe the changes on this livedata the following way:

val task = viewModel.currentTask.observeAsState().value

This task has a duration field that I pass to this Timer composable but when this task will be updated (because the livedata has a new value) the Timer composable doesn't recognize these changes. It doesn't restart the countdown, it stays 0.

I am not sure if I am understanding and using correctly the MutableState concept so can someone please help me?

@Composable
fun Timer(duration: Long, onFinished: () -> Unit) {
    var currentTimerValue by remember { mutableStateOf(duration) }
    LaunchedEffect(key1 = currentTimerValue) {
        if (currentTimerValue > 0) {
            delay(1000L)
            currentTimerValue--
        } else {
            onFinished.invoke()
        }
    }

    Text(text = currentTimerValue.toString(), fontSize = 24.sp, color = Color.White)
}

Upvotes: 8

Views: 3976

Answers (2)

user2287994
user2287994

Reputation:

The accepted approach by @Thracian will only work if duration has a different value. If you try to reset your timer back to the same value, it won't work. Here I propose a re-work of the timer (this is taken from my perosnal project):

    @Composable
fun CountdownTimer(
    data: CountdownTimerRelevantData,
    onEventFromViewModel: Flow<PuzzleScreenEventViewModelToScreen>,
    modifier: Modifier = Modifier
) {
    var remainingSeconds by remember { mutableIntStateOf(0) }
    var startCountdown by remember { mutableStateOf(false) }
    ObserveAsEvents(onEventFromViewModel) { event ->
        if (event is ShowNextPuzzleEvent) {
            startCountdown = true
        }
    }
    LaunchedEffect(startCountdown) {
        startCountdown = false
        remainingSeconds = data.initialTime
        while(remainingSeconds > 0) {
            delay(1.seconds)
            remainingSeconds--
            onEventToViewModel(CheckVictoryConditionEvent)
        }
    }
    Text(
        text = remainingSeconds.toTimeFormattedMinutesString()
                + ":"
                + remainingSeconds.toTimeFormattedSecondsString(),
        modifier = modifier
    )
}

data class CountdownTimerRelevantData(
    val initialTime: Int
)

You can use shared flow or event for the event flow from the viewmodel. Simply emit the event from your viewmodel.

Upvotes: 0

Thracian
Thracian

Reputation: 66674

The issue is here

var currentTimerValue by remember { mutableStateOf(duration) }

After Timer composable enters composition the block inside remember is initilized and it doesn't change unless you reset it with new key.

You can update it as

var currentTimerValue by remember(duration) { mutableStateOf(duration) }

which will reset to duration every time duration param of Timer function changes

Upvotes: 14

Related Questions