Home > front end >  Jetpack Compose - Make LaunchedEffect keep running while app is running in the background
Jetpack Compose - Make LaunchedEffect keep running while app is running in the background

Time:10-29

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

CodePudding user response:

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

CodePudding user response:

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

  • Related