Detecting when your app returns to the foreground can be useful in many situations — such as triggering analytics events, refreshing data, or improving security.
In this article, I’ll show you an elegant way to do it using ActivityLifecycleCallbacks
along with SharedFlow
— fully decoupled and observable from the ViewModel.
🎯 Goal
Implement a system to observe from the ViewModel when the app enters the foreground, without coupling the logic to any Activity or Fragment.
🧠 General approach
- We create an activity lifecycle observer that tracks how many activities are currently started.
- We emit an event when the app moves from background to foreground.
- We observe that event from the ViewModel.
🧩 Step 1: Global lifecycle observer
We create an AppLifecycleObserver
object that implements Application.ActivityLifecycleCallbacks
. This object detects when the app has come back to the foreground (when at least one Activity starts after the app was previously in the background).
import android.app.Activity
import android.app.Application
import android.os.Bundle
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
object AppCameToForegroundEvent
object AppLifecycleObserver : Application.ActivityLifecycleCallbacks {
private val _events = MutableSharedFlow<Any>(replay = 1, extraBufferCapacity = 1)
val events = _events.asSharedFlow()
private var wasInBackground = false
private var activityCount = 0
override fun onActivityStarted(activity: Activity) {
activityCount++
if (activityCount == 1 && wasInBackground) {
_events.tryEmit(AppCameToForegroundEvent)
wasInBackground = false
}
}
override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0) {
wasInBackground = true
}
}
// Required functions
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
// Only for testing purposes
internal fun resetState() {
wasInBackground = false
activityCount = 0
_events.resetReplayCache()
}
}
🧪 Step 2: Registering the observer in the Application
This observer must be registered when the app starts, preferably in your Application
class:
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class BaseApplication : Application() {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(AppLifecycleObserver)
}
}
🔄 Step 3: Observing from the ViewModel
Thanks to SharedFlow being hot and lifecycle-independent, the ViewModel can observe events regardless of when it’s created.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MyViewModel @Inject constructor() : ViewModel() {
init {
observeCameFromBackground()
}
private fun observeCameFromBackground() {
viewModelScope.launch {
AppLifecycleObserver.events
.filterIsInstance<AppCameToForegroundEvent>()
.collect {
// Here your business logic
}
}
}
}
Tests:
import android.app.Activity
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Test
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
@OptIn(ExperimentalCoroutinesApi::class)
class AppLifecycleObserverTest {
@Before
fun setUp() {
AppLifecycleObserver.resetState()
}
@Test
fun `GIVEN app starts for first time WHEN activity is started THEN no event is emitted`() = runTest {
// Given - fresh observer state (app starting for first time)
val observer = AppLifecycleObserver
val mockActivity = mockActivity()
// When - first activity starts (app launch)
observer.onActivityStarted(mockActivity)
// Then - no event should be emitted (timeout should occur)
val receivedEvent = withTimeoutOrNull(100) {
observer.events
.filterIsInstance<AppCameToForegroundEvent>()
.first()
}
assertNull("No event should be emitted on first app start", receivedEvent)
}
@Test
fun `GIVEN app goes to background WHEN app comes to foreground THEN event is emitted`() = runTest {
// Given - simulate app lifecycle: start -> background -> foreground
val observer = AppLifecycleObserver
val mockActivity = mockActivity()
// App starts for first time
observer.onActivityStarted(mockActivity)
// App goes to background (all activities stopped)
observer.onActivityStopped(mockActivity)
// Start collecting events before coming to foreground
val events = mutableListOf<AppCameToForegroundEvent>()
val job = launch {
observer.events
.filterIsInstance<AppCameToForegroundEvent>()
.collect { events.add(it) }
}
// When - app comes to foreground (activity started after being in background)
observer.onActivityStarted(mockActivity)
// Give some time for event to be collected
kotlinx.coroutines.delay(50)
job.cancel()
// Then - event should be emitted
assertEquals("Event should be emitted when coming from background", 1, events.size)
assertEquals(AppCameToForegroundEvent, events.first())
}
@Test
fun `GIVEN app goes to background multiple times WHEN app comes to foreground each time THEN event is emitted each time`() = runTest {
// Given
val observer = AppLifecycleObserver
val mockActivity = mockActivity()
val events = mutableListOf<AppCameToForegroundEvent>()
val job = launch {
observer.events
.filterIsInstance<AppCameToForegroundEvent>()
.collect { events.add(it) }
}
// Give time for subscription to start
kotlinx.coroutines.delay(10)
// App starts for first time (no event expected)
observer.onActivityStarted(mockActivity)
kotlinx.coroutines.delay(10)
// When - multiple background/foreground cycles
observer.onActivityStopped(mockActivity) // First background
kotlinx.coroutines.delay(10)
observer.onActivityStarted(mockActivity) // First foreground
kotlinx.coroutines.delay(10)
observer.onActivityStopped(mockActivity) // Second background
kotlinx.coroutines.delay(10)
observer.onActivityStarted(mockActivity) // Second foreground
kotlinx.coroutines.delay(10)
job.cancel()
// Then - two events should be emitted (one for each foreground transition)
assertEquals("Two events should be emitted for two background/foreground cycles", 2, events.size)
}
private fun mockActivity() = object : Activity() {}
}
✅ Advantages of this approach
- Decoupled: ViewModels remain independent of UI components.
- Centralized: Lifecycle observation is handled once, at the application level.
- Reactive: SharedFlow enables multiple listeners to react to the same event.
- Testable: Both the emission and observation logic are easy to mock and verify.
🧭 Conclusion
Detecting when your app returns to the foreground is very useful for refreshing information, protecting sensitive content, or triggering analytics events. This pattern provides a robust, reactive, and scalable solution.
Do you have any suggestions or improvements on this approach? Feel free to contact me or leave a comment on the article!