How to Use Jetpack Compose Inside Android Service

Jetpack Compose, the new UI framework for Android development, works well in the standard environment of Activities and Fragments. However, if you attempt to show Compose-based UI from a Service, you’ll encounter problems. I faced this challenge in my current freelance project, so I’ll describe the solution here to spare you the time and headache.

Android Service to Show an Overlay

Let’s say your app declares SYSTEM_ALERT_WINDOW permission and the user grants it to the app through settings. This means that you can show overlays over other applications, even when your app is in the background.

To implement the overlay feature, you create this Service:

class ComposeOverlayService: Service() {

    lateinit var windowManager: WindowManager

    private var overlayView: View? = null

    override fun onCreate() {
        super.onCreate()
        windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
    }

    override fun onBind(intent: Intent?): IBinder? {
        throw RuntimeException("bound mode not supported")
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        if (intent.hasExtra(INTENT_EXTRA_COMMAND_SHOW_OVERLAY)) {
            showOverlay()
        }
        if (intent.hasExtra(INTENT_EXTRA_COMMAND_HIDE_OVERLAY)) {
            hideOverlay()
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        hideOverlay()
    }

    private fun showOverlay() {

        if (overlayView != null) {
            return
        }

        overlayView = ComposeView(this).apply {
            setContent {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                ) {
                    Image(
                        modifier = Modifier
                            .align(Alignment.TopCenter)
                            .width(200.dp)
                            .offset(y = 100.dp),
                        painter = painterResource(id = R.drawable.ic_tyc_logo),
                        contentDescription = null
                    )
                }
            }
        }

        windowManager.addView(overlayView, getLayoutParams())
    }

    private fun hideOverlay() {
        if (overlayView == null) {
            return
        }
        windowManager.removeView(overlayView)
        overlayView = null
    }

    private fun getLayoutParams(): WindowManager.LayoutParams {
        return WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
            PixelFormat.TRANSLUCENT
        )
    }

    companion object {

        private const val INTENT_EXTRA_COMMAND_SHOW_OVERLAY = "INTENT_EXTRA_COMMAND_SHOW_OVERLAY"
        private const val INTENT_EXTRA_COMMAND_HIDE_OVERLAY = "INTENT_EXTRA_COMMAND_HIDE_OVERLAY"

        internal fun showOverlay(context: Context) {
            val intent = Intent(context, ComposeOverlayService::class.java)
            intent.putExtra(INTENT_EXTRA_COMMAND_SHOW_OVERLAY, true)
            context.startService(intent)
        }

        internal fun hideOverlay(context: Context) {
            val intent = Intent(context, ComposeOverlayService::class.java)
            intent.putExtra(INTENT_EXTRA_COMMAND_HIDE_OVERLAY, true)
            context.startService(intent)
        }

    }
}

This Service is supposed to show and hide an image overlay on the screen when invoked in this manner:

ComposeOverlayService.showOverlay(context) // show overlay
ComposeOverlayService.hideOverlay(context) // hide overlay

If overlayView would be comprised of just standard Android Views, without Compose, this approach would work right away. However, since ComposeView is involved, the situation gets more complicated.

Cryptic Jetpack Compose Error

When you attempt to use the above Service to show an overlay, you’ll get this error:

FATAL EXCEPTION: main
    Process: com.techyourchance.android.debug, PID: 15344
    java.lang.IllegalStateException: ViewTreeLifecycleOwner not found from androidx.compose.ui.platform.ComposeView{d69a80f V.E...... ......I. 0,0-0,0}
        at androidx.compose.ui.platform.WindowRecomposer_androidKt.createLifecycleAwareWindowRecomposer(WindowRecomposer.android.kt:352)

The framework complains that ComposeView doesn’t have ViewTreeLifecycleOwner. Well, great, but what should we do about that?!

Since this error doesn’t affect ComposeView‘s hosted in Activities and Fragments, we can deduce that, somehow, these components provide this critical dependency. So, we need to figure out how to “create” it and then “inject” into our ComposeView inside the Service.

Turning the Service Into LifecycleOwner and SavedStateRegistryOwner

To resolve the aforementioned error, you’ll need to turn your Service into LifecycleOwner and pass it into the ComposeView. After you do that, you’ll see another error. To solve that next problem, you’ll also need to turn the Service into SavedStateRegistryOwner and pass that into the ComposeView as well.

This is the full implementation:

class ComposeOverlayService: Service(),
    LifecycleOwner,
    SavedStateRegistryOwner {

    lateinit var windowManager: WindowManager

    private val _lifecycleRegistry = LifecycleRegistry(this)
    private val _savedStateRegistryController: SavedStateRegistryController = SavedStateRegistryController.create(this)

    override val savedStateRegistry: SavedStateRegistry = _savedStateRegistryController.savedStateRegistry
    override val lifecycle: Lifecycle = _lifecycleRegistry

    private var overlayView: View? = null

    override fun onCreate() {
        super.onCreate()
        windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager

        _savedStateRegistryController.performAttach()
        _savedStateRegistryController.performRestore(null)
        _lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    }

    override fun onBind(intent: Intent?): IBinder? {
        throw RuntimeException("bound mode not supported")
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        if (intent.hasExtra(INTENT_EXTRA_COMMAND_SHOW_OVERLAY)) {
            showOverlay()
        }
        if (intent.hasExtra(INTENT_EXTRA_COMMAND_HIDE_OVERLAY)) {
            hideOverlay()
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        hideOverlay()
        _lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    }

    private fun showOverlay() {
        MyLogger.i("showOverlay()")

        if (overlayView != null) {
            MyLogger.i("overlay already shown - aborting")
            return
        }

        _lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
        _lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)

        overlayView = ComposeView(this).apply {
            setViewTreeLifecycleOwner(this@ComposeOverlayService)
            setViewTreeSavedStateRegistryOwner(this@ComposeOverlayService)
            setContent {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                ) {
                    Image(
                        modifier = Modifier
                            .align(Alignment.TopCenter)
                            .width(200.dp)
                            .offset(y = 100.dp),
                        painter = painterResource(id = R.drawable.ic_tyc_logo),
                        contentDescription = null
                    )
                }
            }
        }

        windowManager.addView(overlayView, getLayoutParams())
    }

    private fun hideOverlay() {
        MyLogger.i("hideOverlay()")
        if (overlayView == null) {
            MyLogger.i("overlay not shown - aborting")
            return
        }
        windowManager.removeView(overlayView)
        overlayView = null

        _lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        _lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
    }

    private fun getLayoutParams(): WindowManager.LayoutParams {
        return WindowManager.LayoutParams(
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.MATCH_PARENT,
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
            PixelFormat.TRANSLUCENT
        )
    }

    companion object {

        private const val INTENT_EXTRA_COMMAND_SHOW_OVERLAY = "INTENT_EXTRA_COMMAND_SHOW_OVERLAY"
        private const val INTENT_EXTRA_COMMAND_HIDE_OVERLAY = "INTENT_EXTRA_COMMAND_HIDE_OVERLAY"

        internal fun showOverlay(context: Context) {
            val intent = Intent(context, ComposeOverlayService::class.java)
            intent.putExtra(INTENT_EXTRA_COMMAND_SHOW_OVERLAY, true)
            context.startService(intent)
        }

        internal fun hideOverlay(context: Context) {
            val intent = Intent(context, ComposeOverlayService::class.java)
            intent.putExtra(INTENT_EXTRA_COMMAND_HIDE_OVERLAY, true)
            context.startService(intent)
        }

    }
}

Please note how I pass two references to the Service into ComposeView after construction. Furthermore, note that I manually manage the states of _lifecycleRegistry and _savedStateRegistryController. If you’ll mismanage the states of these objects, you might encounter weird bugs (e.g. animations not playing in your overlay).

Summary

That’s it, now you can use Jetpack Compose in your Services without growing gray hair. You can find a full implementation of this feature, including the integration with in-app button to show or hide the overlay, in my open-sourced TechYourChance application.

Thanks for reading and please leave your comments and questions below.

Check out my premium

Android Development Courses

Leave a Comment