๐Ÿ“ฆ Service ร— Overlay ร— Compose ร— MVVM: ์–ด๋–ป๊ฒŒ ๋ถ™์˜€์„๊นŒ?

์ด์ง„์˜ยท2025๋…„ 7์›” 14์ผ
post-thumbnail

๐Ÿ‘€ Service, ์–ด๋””๊นŒ์ง€ ์จ๋ณด์…จ๋‚˜์š”?

์—ฌ๋Ÿฌ๋ถ„์€ Service๋ฅผ ํ†ตํ•ด ์–ด๋””๊นŒ์ง€ ์‚ฌ์šฉํ•ด๋ณด์…จ๋‚˜์š”? ๐Ÿค”

์ €๋Š” ๋ณดํ†ต Service๋ฅผ ์ด์šฉํ•ด์„œ
๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ๐Ÿ“ฅ, ์—…๋กœ๋“œ ๐Ÿ“ค, ์Œ์•… ์žฌ์ƒ ๐ŸŽต ๊ฐ™์€
UI ์—†์ด ์กฐ์šฉํžˆ ๋™์ž‘ํ•˜๋Š” ์ž‘์—…๋“ค์— ํ™œ์šฉํ•ด์™”์Šต๋‹ˆ๋‹ค.


๐Ÿ”” ForegroundService๋„ ์จ๋ดค๋‹ค๋ฉด?

๋ฌผ๋ก  ForegroundService๋ฅผ ํ™œ์šฉํ•ด์„œ
์•Œ๋ฆผ(Notification)์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ฃผ๊ฑฐ๋‚˜,
๋ฒ„ํŠผ ์ œ์–ด ๋“ฑ ๊ฐ„๋‹จํ•œ UI ์ธํ„ฐ๋ž™์…˜์„ ๋‹ค๋ค„๋ณธ ๊ฒฝํ—˜๋„ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿงญ

์˜ˆ๋ฅผ ๋“ค์–ด,

  • ์‹ค์‹œ๊ฐ„ ์ง„ํ–‰ ์ƒํ™ฉ์„ ์•Œ๋ฆผ์— ํ‘œ์‹œํ•˜๊ฑฐ๋‚˜
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์žฌ์ƒ ์ปจํŠธ๋กค์„ ์ œ๊ณตํ•˜๊ฑฐ๋‚˜
    ํ•  ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ–ˆ์—ˆ์ฃ .

๐Ÿคนโ€โ™€๏ธ UI, ViewModel, Hilt๊นŒ์ง€โ€ฆ Service๊ฐ€ ๋ฐ”๋น ์กŒ์–ด์š”

๊ทธ๋Ÿฐ๋ฐ ์ด๋ฒˆ์—” ์ข€ ๋‹ฌ๋ž์Šต๋‹ˆ๋‹ค.
์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Service๋ฅผ ํ†ตํ•ด Overlay๋ฅผ ๋„์šฐ๊ณ ,
๊ทธ ์œ„์—์„œ ์‚ฌ์šฉ์ž์™€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋Š” UI๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ–ˆ๊ฑฐ๋“ ์š”. ๐Ÿ˜ถโ€๐ŸŒซ๏ธ

๊ฒŒ๋‹ค๊ฐ€ ๊ทธ UI๋Š” ๋‹จ์ˆœํ•œ View๊ฐ€ ์•„๋‹ˆ๋ผ

  • Jetpack Compose๋กœ ๊ตฌ์„ฑ๋˜๊ณ 
  • ViewModel, Hilt๊นŒ์ง€ ์—ฐ๋™๋˜๋Š”
    ๊ฝค ๋ณต์žกํ•œ ๊ตฌ์กฐ์˜€์Šต๋‹ˆ๋‹ค. ๐Ÿ”ง

๐Ÿ“Œ ๊ทธ๋ž˜์„œ ์ด ๊ธ€์—์„œ๋Š”...

์ œ๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์ž๋ฃŒ๊ฐ€ ๋ถ€์กฑํ•ด ๊ฝค ๋งŽ์€ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ฒช์—ˆ๊ณ ,
์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์‹œ๋„ํ•ด๋ณด๋ฉฐ ํ•˜๋‚˜์”ฉ ์ •๋‹ต์„ ์ฐพ์•„๊ฐ”์Šต๋‹ˆ๋‹ค. ๐Ÿ”

๊ทธ ๊ณผ์ •์„ ์ •๋ฆฌํ•ด๋‘๋ฉด
๋น„์Šทํ•œ ๊ตฌ์กฐ๋ฅผ ๊ณ ๋ฏผํ•˜๋Š” ๋ถ„๋“ค๊ป˜ ๋„์›€์ด ๋  ๊ฒƒ ๊ฐ™์•„,
์•„๋ž˜์™€ ๊ฐ™์€ ๋‚ด์šฉ์„ ์ค‘์‹ฌ์œผ๋กœ ํ’€์–ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ‘‡

  • ๐Ÿ› ๏ธ Service์—์„œ Overlay๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋„์šฐ๋Š” ๋ฐฉ๋ฒ•
  • ๐ŸชŸ WindowManager + ComposeView ์—ฐ๊ฒฐ ์‹ค์ „ ์ฝ”๋“œ
  • ๐Ÿง  Overlay ์•ˆ์—์„œ๋„ ViewModel์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ๐Ÿงฉ MVVM + Hilt๊นŒ์ง€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์—ฐ๊ณ„ํ•˜๋Š” ๊ตฌ์กฐ ์„ค๊ณ„ ํŒ

๐ŸชŸ ComposeView๋ฅผ Window ์œ„์— ๋„์šฐ๋ ค๋ฉด?

Android์˜ ์ผ๋ฐ˜์ ์ธ UI ๊ตฌ์„ฑ์€ Activity๋‚˜ Fragment๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์ง€๋งŒ,
Service์—์„œ Overlay๋ฅผ ๋„์šฐ๊ธฐ ์œ„ํ•ด์„  WindowManager๋ฅผ ์ง์ ‘ ๋‹ค๋ค„์•ผ ํ•ฉ๋‹ˆ๋‹ค.
์—ฌ๊ธฐ์— Jetpack Compose๋ฅผ ์–น๊ธฐ ์œ„ํ•ด์„  ComposeView๋ฅผ ํ™œ์šฉํ•˜๊ฒŒ ๋˜์ฃ .

๊ทธ๋Ÿผ ๋ณธ๊ฒฉ์ ์œผ๋กœ Service ์œ„์— Compose ๊ธฐ๋ฐ˜ UI๋ฅผ ๋„์šฐ๋Š” ๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ณผ๊ฒŒ์š”. ๐Ÿงฑ


๐Ÿšง Service + ComposeView ๊ตฌ์กฐ

@AndroidEntryPoint
class ComposeOverlayService : Service() {

    @Inject
    lateinit var composeWindow: ComposeWindow

    override fun onCreate() {
        super.onCreate()
        composeWindow.create()
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        composeWindow.reconfigureContent()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        composeWindow.destroy()
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

๐Ÿงพ ComposeOverlayService ๊ฐ„๋‹จ ๊ตฌ์กฐ ์ •๋ฆฌ

ComposeOverlayService๋Š” Jetpack Compose๋กœ ๋งŒ๋“  UI๋ฅผ
Overlay ํ˜•ํƒœ๋กœ Window์— ๋„์šฐ๊ธฐ ์œ„ํ•œ Android Service
์ž…๋‹ˆ๋‹ค.

์ด ํด๋ž˜์Šค์—๋Š” @AndroidEntryPoint๋ฅผ ๋ถ™์—ฌ
Hilt๋ฅผ ํ†ตํ•ด ์˜์กด์„ฑ์„ ์ฃผ์ž…๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์ง„์ž…์ ์ž„์„ ๋ช…์‹œํ•˜๊ณ ,
ComposeWindow๋ผ๋Š” ๋ณ„๋„ ํด๋ž˜์Šค๋ฅผ ์ฃผ์ž…๋ฐ›์•„
Overlay UI ์ƒ์„ฑ ๋ฐ WindowManager ๊ด€๋ จ ์ฒ˜๋ฆฌ๋ฅผ ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.


Service๋Š” ์ „์ฒด ๊ตฌ์กฐ์—์„œ ์ƒ๋ช…์ฃผ๊ธฐ๋งŒ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

Overlay๊ฐ€ ํ•ญ์ƒ ๋–  ์žˆ์–ด์•ผ ํ•˜๋Š” ํŠน์„ฑ์ƒ,
onStartCommand()์—์„œ๋Š” START_STICKY๋ฅผ ๋ฐ˜ํ™˜ํ•ด
์„œ๋น„์Šค๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋ผ๋„ ์ž๋™์œผ๋กœ ๋ณต๊ตฌ๋˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.


์ฆ‰, ์ด ๊ตฌ์กฐ๋Š”
๐Ÿ‘‰ UI ๋กœ์ง์€ ComposeWindow์— ์œ„์ž„ํ•˜๊ณ 
๐Ÿ‘‰ Service๋Š” ์ปจํŠธ๋กค๋Ÿฌ ์—ญํ• ๋งŒ ์ˆ˜ํ–‰ํ•˜๋Š”
๊น”๋”ํ•œ ์ฑ…์ž„ ๋ถ„๋ฆฌ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.


๐Ÿงฑ ComposeWindow ์ฝ”๋“œ ๊ตฌ์กฐ

์•„๋ž˜๋Š” ์‹ค์ œ Overlay UI๋ฅผ Window์— ๋„์šฐ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•œ
ComposeWindow ์ถ”์ƒ ํด๋ž˜์Šค์™€ ๊ทธ ๊ตฌํ˜„์ฒด ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.


๐Ÿ“„ ComposeWindow.kt

abstract class ComposeWindow(
    private val context: Context,
    private val lifecycleOwner: MyLifecycleOwner,
) {
    protected val windowManager: WindowManager =
        context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    protected val params by lazy { createLayoutParams() }
    protected val composeView by lazy { createComposeView() }
    protected val handler = Handler(Looper.getMainLooper())

    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    fun create() {
        lifecycleOwner.run {
            onCreate()
            configureComposeView()
            onStart()
            showOverlay()
            onResume()
        }
    }

    fun destroy() {
        if (composeView.isAttachedToWindow) {
            windowManager.removeView(composeView)
        }
        scope.cancel()
        lifecycleOwner.run {
            onPause()
            onStop()
            onDestroy()
        }
    }

    fun reconfigureContent() {
        handler.post {
            configureComposeView()
        }
    }

    protected fun setVisibility(visible: Boolean) {
        if (composeView.isVisible == visible) return
        composeView.isVisible = visible
    }

    private fun createComposeView(): ComposeView = ComposeView(context).apply {
        pivotX = 0f
        pivotY = 0f
        setViewTreeLifecycleOwner(lifecycleOwner)
        setViewTreeViewModelStoreOwner(lifecycleOwner)
        setViewTreeSavedStateRegistryOwner(lifecycleOwner)
        setViewTreeOnBackPressedDispatcherOwner(lifecycleOwner)
    }

    protected abstract fun configureComposeView()
    protected abstract fun showOverlay()
    protected abstract fun createLayoutParams(): WindowManager.LayoutParams

    protected fun defaultOverlayFlags() = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
        WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
}

๐Ÿงฉ ComposeWindowImpl.kt

class ComposeWindowImpl @Inject constructor(
    val context: Context,
    lifecycleOwner: MyLifecycleOwner,
) : ComposeWindow(context, lifecycleOwner) {

    override fun configureComposeView() {
        composeView.setContent {
            SampleTheme {
                SampleScreen(
                    composeView,
                    windowManager,
                    params,
                    onShowWindow = ::setVisibility,
                )
            }
        }
    }

    override fun showOverlay() {
        if (!composeView.isAttachedToWindow) {
            windowManager.addView(composeView, params)
        }
    }

    override fun createLayoutParams() = WindowManager.LayoutParams(
        200,
        100,
        WindowManager.LayoutParams.TYPE_CAMERA_OVERLAY,
        defaultOverlayFlags(),
        PixelFormat.TRANSLUCENT,
    ).apply {
        gravity = Gravity.TOP or Gravity.LEFT
        x = 0
        y = 0
    }
}

๐Ÿ”ง WindowModule.kt (DI ์ œ๊ณต)

@Module
@InstallIn(SingletonComponent::class)
object WindowModule {

    @Provides
    @Singleton
    fun provideComposeWindow(
        @ApplicationContext context: Context,
        lifecycleOwner: MyLifecycleOwner,
    ): ComposeWindow {
        return ComposeWindowImpl(
            context,
            lifecycleOwner,
        )
    }
}

๐Ÿ“˜ ๊ตฌ์กฐ ์„ค๋ช…

์œ„ ์ฝ”๋“œ๋Š” Service์—์„œ ๋„์šฐ๋Š” Overlay UI๋ฅผ Compose ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ
ํ•ต์‹ฌ์ ์ธ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.


โœ… ComposeWindow (์ถ”์ƒ ํด๋ž˜์Šค)

  • Overlay UI ๊ตฌ์„ฑ์„ ์œ„ํ•œ ๊ธฐ๋ณธ ํ‹€์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒ๋ช…์ฃผ๊ธฐ ํ๋ฆ„(create(), destroy())์„ ์ค‘์‹ฌ์œผ๋กœ
    ComposeView ์ƒ์„ฑ, WindowManager ๋“ฑ๋ก/์ œ๊ฑฐ, ์žฌ๊ตฌ์„ฑ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ๋‚ด๋ถ€์—์„œ ์ง์ ‘ ComposeView๋ฅผ ์ƒ์„ฑํ•˜๋ฉฐ, ์ด๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์„ค์ •์„ ์ˆ˜๋™์œผ๋กœ ํ•ด์ค๋‹ˆ๋‹ค:
setViewTreeLifecycleOwner(lifecycleOwner)
setViewTreeViewModelStoreOwner(lifecycleOwner)
setViewTreeSavedStateRegistryOwner(lifecycleOwner)
setViewTreeOnBackPressedDispatcherOwner(lifecycleOwner)

โœ… ComposeWindowImpl (๊ตฌํ˜„ ํด๋ž˜์Šค)

  • ComposeWindow์˜ ์‹ค์ œ ๊ตฌํ˜„์ฒด๋กœ, Overlay UI๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ๊ตฌ์„ฑํ•˜๊ณ  ํ‘œ์‹œํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
  • SampleScreen์„ ComposeView์— ์—ฐ๊ฒฐํ•˜๊ณ , Window์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•˜๋Š” ๊ณผ์ •์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • Overlay์˜ ํฌ๊ธฐ, ์œ„์น˜, ํƒ€์ž… ๋“ฑ์˜ LayoutParams๋„ ์ด ํด๋ž˜์Šค์—์„œ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

โœ… ViewTree ์„ค์ • ํ•จ์ˆ˜ ์š”์•ฝ

Jetpack Compose๋ฅผ Activity๋‚˜ Fragment๊ฐ€ ์•„๋‹Œ ํ™˜๊ฒฝ (์˜ˆ: Service, WindowManager Overlay ๋“ฑ)์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด
๋‹ค์Œ ๋„ค ๊ฐ€์ง€ ์„ค์ •์„ ๋ฐ˜๋“œ์‹œ ์ˆ˜๋™์œผ๋กœ ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด ์„ค์ •๋“ค์€ Android View Hierarchy์—์„œ Compose, ViewModel, Navigation, ์ƒํƒœ ์ €์žฅ ๋“ฑ์˜ ๊ธฐ๋Šฅ์ด
์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋„๋ก ๋ณด์žฅํ•ด์ค๋‹ˆ๋‹ค.


โœ… ๊ณตํ†ต๋œ ๋ชฉ์ 

ComposeView๊ฐ€ Fragment๋‚˜ Activity๊ฐ€ ์•„๋‹Œ ํ™˜๊ฒฝ(์˜ˆ: Service์˜ Overlay)์—์„œ ์‚ฌ์šฉ๋  ๊ฒฝ์šฐ,
Compose ๊ด€๋ จ ๊ธฐ๋Šฅ๋“ค์ด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์Œ ๋„ค ๊ฐ€์ง€๋ฅผ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • LifecycleOwner
  • ViewModelStoreOwner
  • SavedStateRegistryOwner
  • OnBackPressedDispatcherOwner

๐Ÿ” ์ค‘์š”ํ•จ์ˆ˜๋ณ„ ์ž์„ธํ•œ ์„ค๋ช…

Jetpack Compose๋ฅผ Activity/Fragment ์™ธ๋ถ€ ํ™˜๊ฒฝ(์˜ˆ: Service, Overlay)์—์„œ ์‚ฌ์šฉํ•  ๋•Œ
๋ฐ˜๋“œ์‹œ ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•˜๋Š” ViewTree ๊ด€๋ จ ํ•จ์ˆ˜๋“ค์„ ์•„๋ž˜์—์„œ ์ž์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

๐Ÿงฉ setViewTreeLifecycleOwner(lifecycleOwner: LifecycleOwner)

๐Ÿ“Œ ์—ญํ• 

  • View์™€ ๊ทธ ํ•˜์œ„ ํŠธ๋ฆฌ๋“ค์ด ์ˆ˜๋ช… ์ฃผ๊ธฐ(Lifecycle) ๋ฅผ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ
  • Compose ๋‚ด๋ถ€์—์„œ Lifecycle.current ๋“ฑ์„ ํ†ตํ•ด ํ˜„์žฌ Lifecycle์— ์ ‘๊ทผ ๊ฐ€๋Šฅ

๐Ÿ› ๏ธ ์ค‘์š” ์ด์œ 

  • LaunchedEffect, DisposableEffect, rememberUpdatedState ๋“ฑ์€ Lifecycle์— ์˜์กด
  • ์˜ˆ: Overlay๊ฐ€ ์ข…๋ฃŒ๋˜๋ฉด Coroutine๋„ ์ž๋™ ์ •๋ฆฌ๋ผ์•ผ ํ•จ โ†’ Lifecycle ์ •๋ณด ํ•„์š”

๐Ÿงฉ setViewTreeViewModelStoreOwner(storeOwner: ViewModelStoreOwner)

๐Ÿ“Œ ์—ญํ• 

  • View ํŠธ๋ฆฌ ํ•˜์œ„์—์„œ ViewModel์„ ์ €์žฅ/๊ณต์œ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” Store ์ œ๊ณต

๐Ÿ› ๏ธ ์ค‘์š” ์ด์œ 

  • viewModel() ๋˜๋Š” hiltViewModel() ์‚ฌ์šฉ ์‹œ ํ•„์š”ํ•œ ์„ค์ •
  • Service๋‚˜ Overlay์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ViewModelStore๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์ˆ˜๋™ ์„ค์ • ํ•„์ˆ˜

๐Ÿงฉ setViewTreeSavedStateRegistryOwner(owner: SavedStateRegistryOwner)

๐Ÿ“Œ ์—ญํ• 

  • View ํŠธ๋ฆฌ์—์„œ ์ƒํƒœ ์ €์žฅ/๋ณต์› ๊ธฐ๋Šฅ ์ œ๊ณต

๐Ÿ› ๏ธ ์ค‘์š” ์ด์œ 

  • rememberSaveable()์ด ๋‚ด๋ถ€์ ์œผ๋กœ ์ด Registry๋ฅผ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ์ €์žฅ/๋ณต์›
  • Activity๋‚˜ Fragment๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ์ง์ ‘ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ƒํƒœ ๋ณต์›์ด ๋™์ž‘ํ•˜์ง€ ์•Š์Œ

๐Ÿงฉ setViewTreeOnBackPressedDispatcherOwner(owner: OnBackPressedDispatcherOwner)

๐Ÿ“Œ ์—ญํ• 

  • ๋’ค๋กœ ๊ฐ€๊ธฐ(back) ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ Dispatcher๋ฅผ View์— ์—ฐ๊ฒฐ

๐Ÿ› ๏ธ ์ค‘์š” ์ด์œ 

  • BackHandler { ... } ๋ฅผ Compose์—์„œ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ด ์„ค์ •์ด ํ•„์ˆ˜
  • ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๋’ค๋กœ๊ฐ€๊ธฐ ์ฝœ๋ฐฑ์ด ๋ฌด์‹œ๋จ

โœ… ์š”์•ฝ ์ •๋ฆฌ

Jetpack Compose๋ฅผ Activity๋‚˜ Fragment ์™ธ๋ถ€ ํ™˜๊ฒฝ(์˜ˆ: Service, Overlay ๋“ฑ)์—์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š”
๋‹ค์Œ ๋„ค ๊ฐ€์ง€ ์„ค์ •์„ ๋ฐ˜๋“œ์‹œ ์ˆ˜๋™์œผ๋กœ ์ ์šฉํ•ด์•ผ Compose ๊ธฐ๋Šฅ์ด ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค:

  1. setViewTreeLifecycleOwner(view, lifecycleOwner)
    โ†’ LaunchedEffect, DisposableEffect ๋“ฑ ์ƒ๋ช…์ฃผ๊ธฐ ๊ธฐ๋ฐ˜ ์ปดํฌ์ €๋ธ” ์ž‘๋™์„ ์œ„ํ•ด ํ•„์š”

  2. setViewTreeViewModelStoreOwner(view, storeOwner)
    โ†’ viewModel(), hiltViewModel() ์‚ฌ์šฉ ์‹œ ViewModel ํƒ์ƒ‰ ๊ธฐ๋ฐ˜ ์ œ๊ณต

  3. setViewTreeSavedStateRegistryOwner(view, owner)
    โ†’ rememberSaveable() ๋“ฑ ์ƒํƒœ ์ €์žฅ ๊ธฐ๋Šฅ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ

  4. setViewTreeOnBackPressedDispatcherOwner(view, owner)
    โ†’ BackHandler ์‚ฌ์šฉ์„ ์œ„ํ•œ ๋’ค๋กœ ๊ฐ€๊ธฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ง€์›


๐Ÿ’ก ์ด ๋„ค ๊ฐ€์ง€๋ฅผ ๋น ์ง์—†์ด ์„ค์ •ํ•ด์•ผ Compose์˜ ์ฃผ์š” ๊ธฐ๋Šฅ๋“ค์ด Activity ์—†์ด๋„ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•œ๋‹ค.
ํŠนํžˆ Service ๊ธฐ๋ฐ˜ Overlay UI ๊ตฌ์„ฑ ์‹œ์—๋Š” ํ•„์ˆ˜ ์ฒดํฌ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.


โœ… SampleScreen.kt

@Composable
fun SampleScreen(
    composeView: ComposeView,
    windowManager: WindowManager,
    layoutParams: LayoutParams,
    onShowWindow: (Boolean) -> Unit,
) {
    val entryPoint = getEntryPoint(LocalContext.current)
    val sampleViewModel: SampleViewModel = entryPoint.sampleViewModel()

    SampleContent()
}
  • SampleScreen์€ Overlay์— ๋„์›Œ์งˆ Compose UI์˜ ์ง„์ž…์ ์ž…๋‹ˆ๋‹ค.

  • โš ๏ธ ์ผ๋ฐ˜์ ์ธ Activity ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— hiltViewModel()์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
    ๐Ÿ‘‰ ๋Œ€์‹ , EntryPoint๋ฅผ ํ†ตํ•ด Hilt์—์„œ ViewModel์„ ์ˆ˜๋™์œผ๋กœ ์ฃผ์ž…๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    ์ด๋Š” Service, Dialog, WindowManager์™€ ๊ฐ™์ด ComposeView๋งŒ ์กด์žฌํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ ViewModel์„ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ํ•„์ˆ˜ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

  • ์ด ViewModel์„ ํ™œ์šฉํ•˜์—ฌ UI ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์‹ค์ œ UI๋Š” SampleContent()์—์„œ ๊ทธ๋ฆฌ๋ฉฐ, ์ด ํ•จ์ˆ˜๋Š” ViewModel ๋˜๋Š” ์™ธ๋ถ€ ์ƒํƒœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

  • composeView, windowManager, layoutParams, onShowWindow๋Š”
    Overlay ์ œ์–ด๋ฅผ ์œ„ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ, ์ถ”ํ›„ ๊ธฐ๋Šฅ ํ™•์žฅ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.


โœ… ViewModelModule.kt / ViewModelEntryPoint.kt

@Module
@InstallIn(SingletonComponent::class)
object ViewModelModule {

    @Provides
    @Singleton
    fun provideSampleViewModel(
        sampleRepository: SampleRepository,
    ): SampleViewModel {
        return SampleViewModel(
            sampleRepository,
        )
    }
}

@InstallIn(SingletonComponent::class)
@EntryPoint
interface ViewModelEntryPoint {
    fun sampleViewModel(): SampleViewModel
}

fun getEntryPoint(context: Context): ViewModelEntryPoint {
    return EntryPointAccessors.fromApplication(
        context,
        ViewModelEntryPoint::class.java,
    )
}
  • Activity, Fragment ์—†์ด ViewModel์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ
    (์˜ˆ: Service, WindowManager, Overlay)์—์„œ๋Š”
    hiltViewModel()์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—,
    EntryPoint๋ฅผ ํ†ตํ•ด ViewModel์„ ์ˆ˜๋™์œผ๋กœ ์ฃผ์ž…๋ฐ›๋Š” ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • ViewModelModule์€ Hilt๊ฐ€ SampleViewModel์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก
    ๋ช…์‹œ์ ์œผ๋กœ @Provides ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•œ ๋ชจ๋“ˆ์ž…๋‹ˆ๋‹ค.
    ์ด๋ฅผ ํ†ตํ•ด @Inject ์ƒ์„ฑ์ž๋ฅผ ๊ฐ€์ง„ ViewModel๋„ Hilt๊ฐ€ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • ViewModelEntryPoint๋Š” ์™ธ๋ถ€์—์„œ Hilt ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์—ด์–ด์ฃผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.
    @EntryPoint์™€ @InstallIn(SingletonComponent::class)๋กœ ๋“ฑ๋ก๋œ ์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด
    ViewModel์„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • getEntryPoint(context) ํ•จ์ˆ˜๋Š” EntryPointAccessors๋ฅผ ์ด์šฉํ•ด
    ApplicationContext๋กœ๋ถ€ํ„ฐ Hilt EntryPoint ์ธ์Šคํ„ด์Šค๋ฅผ ํš๋“ํ•ฉ๋‹ˆ๋‹ค.

  • ๐Ÿ“Œ ์ด ๊ตฌ์กฐ ๋•๋ถ„์— hiltViewModel() ์—†์ด๋„
    ComposeView + Service ๊ธฐ๋ฐ˜ ํ™˜๊ฒฝ์—์„œ๋„ ViewModel์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฃผ์ž…๋ฐ›์•„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โœ… SampleViewModel.kt

class SampleViewModel @Inject constructor(
    private val sampleRepository: SampleRepository,
) : ViewModel() {

}
  • ์ƒ์„ฑ์ž์— @Inject๋ฅผ ๋ถ™์—ฌ sampleRepository๋ฅผ ์ฃผ์ž…๋ฐ›์œผ๋ฉฐ,
    Hilt๊ฐ€ ์ด ViewModel์˜ ์˜์กด์„ฑ์„ ์ž๋™์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์ผ๋ฐ˜์ ์œผ๋กœ๋Š” @HiltViewModel์„ ์„ ์–ธํ•˜๊ณ  hiltViewModel()๋กœ ์ฃผ์ž…๋ฐ›์ง€๋งŒ,
    ์ด ํ”„๋กœ์ ํŠธ๋Š” Activity๋‚˜ Fragment๊ฐ€ ์•„๋‹Œ ํ™˜๊ฒฝ(์˜ˆ: Service, Overlay) ์ด๋ฏ€๋กœ
    ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

  • โ— ๋”ฐ๋ผ์„œ ์ด ViewModel์—๋Š” @HiltViewModel์„ ๋ถ™์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
    @HiltViewModel์€ ๋‚ด๋ถ€์ ์œผ๋กœ ViewModelProvider.Factory์™€ Android์˜ LifecycleScope์— ์˜์กดํ•˜๋Š”๋ฐ,
    ComposeView๋งŒ ์กด์žฌํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” ์ด๋“ค์ด ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

  • ๋Œ€์‹  EntryPoint๋ฅผ ํ†ตํ•ด ViewModel์„ ์ˆ˜๋™์œผ๋กœ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿงถ ๋งˆ๋ฌด๋ฆฌ ์ •๋ฆฌ

์œ„์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋กœ ๊ฐœ๋ฐœ์„ ํ•œ๋‹ค๋ฉด ๊ธฐ์กด Compose ๊ฐœ๋ฐœ๊ณผ ํฐ ์ฐจ์ด์  ์—†์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๊ฐœ๋ฐœ์ด ๊ฐ€๋Šฅํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Service + Compose + ViewModel + Hilt๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๊ณผ์ •์—์„œ
๊ฒช์—ˆ๋˜ ์‹œํ–‰์ฐฉ์˜ค๋“ค์„ ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•œ ์˜ˆ์ œ๋กœ ํ’€์–ด๋‚ด๋ ค ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” ์‹ค์ œ๋กœ ComposeOverlayService๋ฅผ ์‚ฌ์šฉํ•œ ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ
Camera, Stream ์ œ์–ด, ์„œ๋ฒ„ ํ†ต์‹  ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์•ˆ์ •์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


์ด ๊ธ€์ด Service ์œ„์— Compose UI๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋Š” ๋ถ„๋“ค๊ป˜
๊ตฌ์กฐ์ ์ธ ์ดํ•ด์™€ ์‹ค์ „ ์ ์šฉ์— ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

์ € ์—ญ์‹œ ์ž๋ฃŒ๊ฐ€ ๋งŽ์ง€ ์•Š์•„ ์—ฌ๋Ÿฌ ๋ฒˆ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ฒช์—ˆ์ง€๋งŒ,
์ด ๊ตฌ์กฐ ๋•๋ถ„์— ์œ ์—ฐํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ UI๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์šด์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์—์„œ๋„ ์ด ๋ฐฉ์‹์ด ์ข‹์€ ์ถœ๋ฐœ์ ์ด ๋˜๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค. ๐Ÿ™Œ

profile
Android Developer

0๊ฐœ์˜ ๋Œ“๊ธ€