1

I am developing an android app using Jetpack Compose and have a timer implemented using launchedEffect here is some dummy code for clearance

LaunchedEffect(key1 = timeLeft) {
    if(timeLeft > 0) {
        delay(100L)
        timeLeft -= 100L
    }
}

my problem is that when the app is in the background the LaunchedEffect stops running and the timer is "stuck" on the same value until I return to the app

  • 1
    You won't really be able to build something like this with `LaunchedEffect`, as it's tied to your UI, which gets destroyed once your app goes into the background. You'll need to look into Services, a Service is the right component to use to perform background work. – Egor Oct 28 '22 at 13:31
  • You can check out ViewModel option as well https://stackoverflow.com/a/73333458/5457853 – Thracian Oct 28 '22 at 16:03

2 Answers2

2

Launched Effect should not be used for that purpose, but your timer can survive minimization in a coroutine.

                    var time: Int by remember {
                        mutableStateOf(0)
                    }
            
                    LaunchedEffect(key1 = true, block = {
                        CoroutineScope(Dispatchers.IO).launch {
                            while (isTimerGoing) {
                                time = time + 1
                                delay(1000)
                            }
                        }
                    })

Below I'll describe other ways to do it.

You have several options to run a background task.

  • Alarms
  • Workmanager
  • Services
  • Coroutines
  1. First of all you might not need a background timer. You only need to remember start time and then show the timer when you are drawing ui.

  2. If you need to do something after a period of time, and you know exactly when, use an alarm.

  3. If you need this timer going all the time even with the app closed, consider using a foreground service.

  4. If it is ok to stop the timer when the app is cleared from memory you can use a viewModel.

The best match to your case will be a viewmodel solution.

Here is the sample made from Empty Compose Activity Template:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import ru.makproductions.timerapp.ui.theme.TimerAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TimerAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    val viewModel = viewModel(MainViewModel::class.java)
                    val state = viewModel.state.collectAsState()
                    LaunchedEffect(key1 = true, block = { viewModel.startTimer() })
                    Column(modifier = Modifier.fillMaxSize()) {
                        Text("Timer going: ${state.value.currentTime}")
                        Button(onClick = {
                            if (state.value.isTimerGoing) {
                                viewModel.stopTimer()
                            } else {
                                viewModel.startTimer()
                            }
                        }) {
                            if (state.value.isTimerGoing) {
                                Text(text = "Stop timer")
                            } else {
                                Text(text = "Start timer")
                            }
                        }
                    }
                }
            }
        }
    }
}

data class MainViewState(
    val currentTime: Int = 0,
    val isTimerGoing: Boolean = false
)
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow

class MainViewModel : ViewModel() {

    val state = MutableStateFlow<MainViewState>(MainViewState())

    fun startTimer() {
        state.tryEmit(state.value.copy(isTimerGoing = true))
        CoroutineScope(Dispatchers.IO).launch {
            while (state.value.isTimerGoing) {
                withContext(Dispatchers.Main) {
                    state.tryEmit(state.value.copy(currentTime = state.value.currentTime + 1))
                }
                delay(1000)
            }
        }
    }

    fun stopTimer() {
        state.tryEmit(state.value.copy(isTimerGoing = false))
    }
}
dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    implementation 'androidx.compose.material:material:1.1.1'

    //Add this line to get viewModel(...) in your composable
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")


    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}

Some docs:

Here is the official guide to background work - https://developer.android.com/guide/background

And here is documentation for the services - https://developer.android.com/guide/components/services

Inliner
  • 1,061
  • 7
  • 10
0

Try to pass the key which will not change to the LaunchedEffect.

Mike
  • 2,547
  • 3
  • 16
  • 30