I coded a timer app that outputs text-to-speech audio, but the timer stops every time I switch to another app or lock the phone, and I would like it to continue timing even when I do that.
Here are the 2 composables that make up the current app:
**@Composable**
fun rememberTextToSpeech(context: Context): TextToSpeech {
var ttsInstance: TextToSpeech? = null
val tts = remember {
TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
ttsInstance?.language = Locale("spa")
}
}.also {
ttsInstance = it
}
}
DisposableEffect(Unit) {
onDispose {
tts.shutdown()
}
}
return tts
}
@OptIn(ExperimentalMaterial3Api::class)
**@Composable**
fun TimerScreen() {
val context = LocalContext.current
val tts = rememberTextToSpeech(context)
var inputText by remember { mutableStateOf("") }
var timeLeft by remember { mutableStateOf(0) } // seconds
var isRunning by remember { mutableStateOf(false) }
var isFinished by remember { mutableStateOf(false) } // ‹— NEW
var initialMinutes by remember { mutableStateOf(0) }
/* ───────────── COUNT‑DOWN ───────────── */
LaunchedEffect(isRunning, timeLeft) {
if (isRunning && timeLeft > 0) {
delay(1000) // tick
timeLeft -= 1
} else if (isRunning && timeLeft == 0) {
isRunning = false
isFinished = true // trigger the repeater
}
}
/* ───────────── REPEATER ───────────── */
LaunchedEffect(isFinished) {
while (isFinished) { // keeps looping until Reset toggles it off
val msg = "Your $initialMinutes minute timer is done"
tts.speak(msg, TextToSpeech.QUEUE_FLUSH, null, "timer_done")
delay(3000) // repeat interval (3s feels natural)
}
tts.stop() // stop any ongoing speech when reset
}
/* ───────────── UI ───────────── */
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF5F5DC))
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
"How many minutes would you like to time?",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
)
Spacer(Modifier.height(32.dp))
TextField(
value = inputText,
onValueChange = { if (it.all(Char::isDigit)) inputText = it },
label = { Text("Minutes") },
singleLine = true,
modifier = Modifier.width(120.dp)
)
Spacer(Modifier.height(64.dp))
val mm = timeLeft / 60
val ss = timeLeft % 60
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
String.format("%02d:%02d", mm, ss),
fontSize = 64.sp,
fontWeight = FontWeight.Bold
)
Spacer(Modifier.height(64.dp))
/* START */
Button(
onClick = {
val minutes = inputText.toIntOrNull() ?: 0
if (minutes > 0) {
initialMinutes = minutes
timeLeft = minutes * 60
isRunning = true
isFinished = false // safeguard
}
},
enabled = !isRunning && inputText.isNotBlank(),
colors = ButtonDefaults.buttonColors(containerColor = Color.Green)
) { Text("Start", color = Color.Black) }
/* RESET */
Button(
onClick = {
isRunning = false
isFinished = false // stops the repeater loop
timeLeft = 0
inputText = ""
},
colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) { Text("Reset") }
}
}
}
I've only tried the original implementation, which I did not expect to stop when switching the app or locking the phone.
Your current timer relies on LaunchedEffect, which runs within the Composable lifecycle.
When the app is minimized or the screen is locked, the Composable is no longer active, can cause the timer to pause.
Use a Foreground Service (depends on the OS version or custom ROM, please follow the best practices : https://developer.android.com/develop/background-work/services/fgs)
I think a foreground service is ideal for this use case because:
It keeps the timer running in the background.
It can trigger TTS when the timer finishes.
It displays a persistent notification to inform the user that the app is running (required for Android's foreground service policy).
It works even when the screen is locked or the app is in the background.
Hope this helps....