[Android] UI Event with SingleLiveData

๐Ÿ’ก๊ตฌ์„ฑ ๋ณ€๊ฒฝ ์‹œ ํ•œ ๋ฒˆ์˜ ๋ฐ์ดํ„ฐ ๋ณ€ํ™”๋ฅผ ๊ด€์ฐฐํ•˜๊ธฐ ์œ„ํ•œ UI Event ํ™œ์šฉ๋ฒ•์— ๋Œ€ํ•˜์—ฌ ๊ธฐ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

Event

  • LiveData์™€ ๊ฐ™์€ Observable ํƒ€์ž…์„ ํ™œ์šฉํ•˜๋ฉด ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ตฌ๋…ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ง€์†์ ์œผ๋กœ ๋…ธ์ถœํ•˜๊ณ  ๊ด€์ฐฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์Šค๋‚ต๋ฐ”, ํ† ์ŠคํŠธ, navigation ์ „ํ™˜ ๋“ฑ์˜ ์ผํšŒ์„ฑ ์ด๋ฒคํŠธ๋Š” ํ•œ ๋ฒˆ์˜ ๋ฐ์ดํ„ฐ ๋ณ€ํ™”๋งŒ์„ ํ•„์š”๋กœ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ๋•Œ ์—ฌ๋Ÿฌ ๋ฒˆ ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ๋ณ„๋„์˜ Event wrapper class๋ฅผ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ SingleLiveData ํƒ€์ž… ๋“ฑ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์•ˆ์€ SingleLiveData์™€ Event ํŒจํ„ด์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. LiveData๋ฅผ ์ด๋Ÿฌํ•œ ํ™˜๊ฒฝ์— ํ™œ์šฉํ•  ๊ฒฝ์šฐ ํ™œ์„ฑํ™”๋  ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ์ „๋‹ฌํ•˜๊ฒŒ ๋˜๋ฉฐ, ๋น„ํšจ์œจ์ ์ธ ๋ฆฌ์†Œ์Šค๋ฅผ ๋‚ญ๋น„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

SingleLiveData

  • SingleLiveData ํŒจํ„ด์„ ํ™œ์šฉํ•˜๋ฉด LiveData๋ฅผ ํ™•์žฅํ•˜์—ฌ ํ•œ ๋ฒˆ๋งŒ ๋ฐ์ดํ„ฐ๊ฐ€ ์†Œ๋น„๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๊ฐ€ ๋œ ํ›„์—๋Š” ๋” ์ด์ƒ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š๋„๋ก ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.

MutableLiveData

  • SingleLiveData๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ MutableLiveData๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค.
  • AtomicBoolean์„ ํ™œ์šฉํ•˜์—ฌ ์Šค๋ ˆ๋“œ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ boolean ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ ์ค‘์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ์ถ”์ ํ•˜์—ฌ true๋กœ ์ „ํ™˜ํ•˜๊ณ , ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋ฅผ ์™„๋ฃŒํ•˜๋ฉด false๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด true์ธ ์ƒํƒœ์—์„œ๋งŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
class SingleLiveData<T> : MutableLiveData<T>() {
    
    private val pending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        // Observe the internal MutableLiveData
        super.observe(owner, { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    override fun setValue(value: T?) {
        pending.set(true)
        super.setValue(value)
    }

    // ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ
    fun call() {
        value = null
    }
}

ViewModel์—์„œ ํ™œ์šฉ

  • ViewModel์—์„œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ , ์ด๋ฅผ ๊ด€์ฐฐํ•˜๋Š” Activity ๋˜๋Š” Fragment์—์„œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ์— ๋”ฐ๋ฅธ UI ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
class MyViewModel : ViewModel() {
    val singleLiveEvent = SingleLiveData<String>()

    fun triggerEvent() {
        singleLiveEvent.value = "ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋  ์ด๋ฒคํŠธ"
    }
}

// in Activity or Fragment
viewModel.singleLiveEvent.observe(this, Observer { event ->
    showToast(event)
})

Event Wrapper

  • Event Wrapper ํŒจํ„ด์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ์ถ”์ ํ•˜์—ฌ, ๋™์ผํ•œ ์ด๋ฒคํŠธ๊ฐ€ ๋‹ค์‹œ ์ „๋‹ฌ๋˜์ง€ ์•Š๋„๋ก ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  • Event๋ผ๋Š” ๋ž˜ํผ ํด๋ž˜์Šค๋ฅผ ๋„์ž…ํ•ด, ํ•œ ๋ฒˆ๋งŒ ์†Œ๋น„๋  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
class Event<out T>(private val content: T) {

    private var hasBeenHandled = false

    /**
     * ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ์—๋Š” null์„ ๋ฐ˜ํ™˜
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * ์ด๋ฒคํŠธ๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€์™€ ๊ด€๊ณ„์—†์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜
     */
    fun peekContent(): T = content
}

ViewModel์—์„œ ํ™œ์šฉ

  • SingleLiveData์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ViewModel์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
class MyViewModel : ViewModel() {
    private val _event = MutableLiveData<Event<String>>()
    val event: LiveData<Event<String>> get() = _event

    fun triggerEvent() {
        _event.value = Event("ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ๋  ์ด๋ฒคํŠธ")
    }
}

viewModel.event.observe(this, Observer { event ->
    event.getContentIfNotHandled()?.let {
        // ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ์ฒ˜๋ฆฌ
        showToast(it)
    }
})

์–ด๋–ค ๋ฐฉ์‹์ด ์ข‹์„๊นŒ?

  • SingleLiveData๋Š” ํ•œ ๋ฒˆ๋งŒ ์ „๋‹ฌ๋˜๋Š” ๊ฐ„๋‹จํ•œ ์ด๋ฒคํŠธ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋Š” ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€๋‚˜ ๋„ค๋น„๊ฒŒ์ด์…˜๊ณผ ๊ฐ™์ด ํ•œ ๋ฒˆ ๋ฐœ์ƒํ•œ ํ›„ ๋‹ค์‹œ ๋ฐœ์ƒํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • Event Wrapper ํŒจํ„ด์€ ๋ณต์žกํ•œ UI ์ƒํƒœ ๊ด€๋ฆฌ์— ๋” ์œ ์—ฐํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • UI ์ƒํƒœ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ๋‘ ํŒจํ„ด์€ UI ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ค‘๋ณต ํ˜ธ์ถœ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋ฉฐ, ์ด ํŒจํ„ด๋“ค์„ ํ™œ์šฉํ•˜๋ฉด UI ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ํ•œ ๋ฒˆ๋งŒ ์†Œ๋น„๋˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ 

https://charlezz.com/?p=44609