Read more" />

How to detect if your app is in the foreground from the ViewModel in Android

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!

Call today

+34 634 548 126

You've found the person you were looking for. Let's talk.

Let's get you started