๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/Android

[Android] ViewModel - View์— Event ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ

interfacer_han 2024. 2. 27. 14:20

#1 ์ƒ˜ํ”Œ ์•ฑ

 

[Android] ViewModel - View์— ๊ฐ์ฒด(ViewModel) ์ „๋‹ฌ

#1 ๊ฐœ์š” #1-1 Data Binding๊ณผ ViewModel [Android] Data Binding - View์— ๊ฐ์ฒด ์ „๋‹ฌ #1 ๊ฐ์ฒด ์ „๋‹ฌ์˜ ํ•„์š”์„ฑ #1-1 ์ด์ „ ๊ธ€ Data Binding - ๊ธฐ์ดˆ #1 ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ ์ „ #1-1 ์˜ˆ์‹œ ์•ฑ ์œ„๊ณผ ๊ฐ™์€ ๊ฐ„๋‹จํ•œ ์•ฑ์ด ์žˆ๋‹ค. Button์„

kenel.tistory.com

์•„๋ž˜์— ์žˆ๋Š” ์ฝ”๋“œ๋“ค์€ ์œ„ ๊ฒŒ์‹œ๊ธ€์˜ ์™„์„ฑ๋œ ์•ฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ˆ˜์ •๋œ ๊ฒƒ๋“ค์ด๋‹ค.
 

#2 ๋ชฉํ‘œ (์ˆ˜์ •ํ•  ๋ถ€๋ถ„)

// package com.example.eventwithview

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainActivityViewModel : ViewModel() {
    private var count: MutableLiveData<Int> = MutableLiveData(0)

    fun getCurrentCount(): MutableLiveData<Int> {
        return count
    }

    fun updateCount() {
        count.value = (count.value)?.plus(1)
    }
}

์ด ์ฝ”๋“œ๋ฅผ count.value๊ฐ€ 10์ด์ƒ์ด๋ฉด Activity์—์„œ Toast ๋ฉ”์‹œ์ง€๋กœ count.value ๊ฐ’์„ ํ‘œ์‹œํ•˜๊ฒŒ ๋งŒ๋“ค ๊ฒƒ์ด๋‹ค. ViewModel ์ž์ฒด์— Toast.makeText(...).show()๋ฅผ ๋งŒ๋“ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ๋œ๋‹ค.
 

#3 ์ฝ”๋“œ ์ˆ˜์ • - MVVM ํŒจํ„ด ์œ„๋ฐฐ

'๋”๋ณด๊ธฐ'์— ์žˆ๋Š” ์ฝ”๋“œ๋“ค์€ ํ˜น์‹œ๋‚˜ ํ•ด์„œ ์ฒจ๋ถ€ํ•˜๊ธด ํ–ˆ์œผ๋‚˜, ๋”ฑํžˆ ๋ณผ ํ•„์š”๋Š” ์—†๋Š” ์ฝ”๋“œ๋“ค์ด๋‹ค.
 

#3-1 ViewModelFactory ์ถ”๊ฐ€

๋”๋ณด๊ธฐ
// package com.example.eventwithview

import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class MainActivityViewModelFactory(private val activityContext: Context) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainActivityViewModel::class.java)) {
            return MainActivityViewModel(activityContext) as T
        }
        throw IllegalArgumentException("Unknown ViewModel Class")
    }
}

์ฐธ์กฐ
 

#3-2 Activity ์ˆ˜์ •

๋”๋ณด๊ธฐ
...

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: MainActivityViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val factory = MainActivityViewModelFactory(this)
        viewModel = ViewModelProvider(this, factory)[MainActivityViewModel::class.java]

        binding.myViewModel = viewModel
        viewModel.getCurrentCount().observe(this, Observer {
            binding.countText.text = it.toString()
        })
    }
}

์ฐธ์กฐ
 

#3-3 ViewModel ์ˆ˜์ •

...

class MainActivityViewModel(private val activityContext: Context) : ViewModel() {
    ...

    fun updateCount() {
        count.value = (count.value)?.plus(1)

        if (10 <= count.value!!) {
            Toast.makeText(activityContext, "${count.value}", Toast.LENGTH_SHORT).show()
        }
    }
}

Toast ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ์— ํ•„์š”ํ•œ Activity์˜ Context๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„ ์‹คํ–‰ํ•˜๋Š” ์ฝ”๋“œ๋‹ค. ํ•˜์ง€๋งŒ, ์ด ์ฝ”๋“œ๋Š” MVVM ํŒจํ„ด์„ ์ •๋ฉด์œผ๋กœ ์œ„๋ฐ˜ํ•˜๊ณ  ์žˆ๋‹ค.
 

#3-4 MVVM ํŒจํ„ด์˜ ์กฐ๊ฑด

 

[Android] MVVM ๊ตฌ์กฐ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

#1 ์•ˆ๋“œ๋กœ์ด๋“œ ์•ฑ์˜ '์ „ํ†ต์ ์ธ' ๋ฐฉ์‹ vs MVVM ํŒจํ„ด #1-1 ๋„์‹๋„์™€ ์ข…์†์„ฑ ํ™”์‚ดํ‘œ๋Š” ํด๋ž˜์Šค ๊ฐ„์˜ ์ข…์†์„ฑ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, View๋Š” ViewModel์— ์ข…์†๋œ๋‹ค. ์ข…์†์˜ ์‚ฌ์ „์  ์˜๋ฏธ๋Š” '์ž์ฃผ์„ฑ์ด ์—†์ด ์ฃผ๊ฐ€

kenel.tistory.com

์™œ๋ƒํ•˜๋ฉด ๊ตฌ๊ธ€์ด ๊ถŒ์žฅํ•˜๋Š” MVVM ํŒจํ„ด์—์„œ ViewModel์€ Activity๋ฅผ ์ฐธ์กฐํ•ด์„œ๋Š” ์•ˆ ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ViewModel์€ Activity๋ฅผ ๋ชฐ๋ผ์•ผ ํ•œ๋‹ค.
 

#4 ์˜ฌ๋ฐ”๋ฅธ ์ˆ˜์ • ๋ฐฉํ–ฅ (LiveData ์ด์šฉ)

#4-1 ๊ฐœ์š”

ํ—ˆ๋‚˜, Toast ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•˜๋ ค๋ฉด, Activity์˜ Context๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ์ด๋Ÿด ๋• ViewModel์— Toatst ๋ฉ”์‹œ์ง€๋ฅผ ์‹คํ–‰ํ•  ํƒ€์ด๋ฐ๊ณผ ๊ทธ ๋ฉ”์‹œ์ง€์˜ ๋‚ด์šฉ๋งŒ์„ ๊ธฐ์ˆ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ง์ ‘์ ์ธ ์‹คํ–‰์€ Activity์—๊ฒŒ ๋งก๊ธด๋‹ค. ํƒ€์ด๋ฐํ•˜๋ฉด ์ƒ๊ฐ๋‚˜๋Š” ๊ฒƒ์€ LiveData๋‹ค. Toast ๋ฉ”์‹œ์ง€์˜ ๋‚ด์šฉ์„ MutableLiveData<String>์— ํ• ๋‹นํ•˜๋ฉด ๊ทธ ์ž์ฒด๋กœ ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€์˜ ๋‚ด์šฉ์„ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์ด๋ฉฐ ๊ทธ๋ฆฌ๊ณ  Activity ์† LiveData.observe()์— ํ•ด๋‹น ์ตœ์‹  ๋ฉ”์‹œ์ง€์˜ ๋‚ด์šฉ์„ ํ‘œ์‹œํ•  ํƒ€์ด๋ฐ๊นŒ์ง€ ์•Œ๋ ค์ค„ ์ˆ˜ ์žˆ์ง€ ์•Š์€๊ฐ€.
 
ํ•˜์ง€๋งŒ LiveData<String>์—๋Š” ๋ฌธ์ œ์ ์ด ์กด์žฌํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ์•กํ‹ฐ๋น„ํ‹ฐ๋กœ ๋„˜์–ด๊ฐ”๋‹ค๊ฐ€ ๋‹ค์‹œ ๋Œ์•„์˜ค๋ฉด, LiveDate.observer()๊ฐ€ ๋‹ค์‹œ ์ž‘๋™๋˜์–ด ์ด๋ฏธ ํ‘œ์‹œํ–ˆ๋˜ Toast ๋ฉ”์‹œ์ง€๊ฐ€ ๋‹ค์‹œ ํ‘œ์‹œ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. 
 

#4-2 LiveData์— ๋‹ด์„ Event ๊ฐ์ฒด

 

LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)

2021 Update: This guidance is deprecated in favor of the official guidelines.

medium.com

์œ„ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๊ธ€์—์„  LiveData์— ๋‹ด์„ ๊ฐ์ฒด์—, ๊ทธ ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ(Toast ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ๋“ฑ)๊ฐ€ ์ด๋ฏธ ์ˆ˜ํ–‰๋˜์—ˆ๋Š”์ง€ ์ฒดํฌํ•˜๋Š” ํ”„๋กœํผํ‹ฐ๋ฅผ ๋„ฃ์Œ์œผ๋กœ์จ, LiveData<String>์„ ์“ฐ๋Š” ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ ์„ ํ•ด๊ฒฐํ–ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€, ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์—์„œ ๋™์‹œ์— ๊ฐ™์€ Event์— ์ ‘๊ทผํ•˜๋Š” ๊ฒฝ์šฐ๊นŒ์ง€ ๊ณ ๋ คํ•ด ์„ค๊ณ„ํ–ˆ๋‹ค๊ณ  ํ•œ๋‹ค.
 
์•„๋ž˜ ์ฝ”๋“œ๋Š” ์œ„ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๊ธ€์— ์žˆ๋Š” Event ํด๋ž˜์Šค์˜ ์ฝ”๋“œ๋ฅผ ๋ณต์‚ฌ/๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•œ ๊ฒƒ์ด๋‹ค. ์•ˆํƒ€๊น๊ฒŒ๋„(?) ์œ„ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๊ธ€์˜ ์ฝ”๋“œ๋Š” ์ตœ์‹ ํ™”๋œ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ณต์‹ ๊ฐ€์ด๋“œ๋ผ์ธ์œผ๋กœ ๋Œ€์ฒด๋˜์–ด Deprecated๋˜์—ˆ๋‹ค๊ณ  ํ•œ๋‹ค. ํ•˜์ง€๋งŒ, ํ•ด๋‹น ๊ฐ€์ด๋“œ๋ผ์ธ์€ StateFlow๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. StateFlow๋Š” ์•„์ง ๋‚ด๊ฐ€ ๋ชจ๋ฅด๋Š”(ํ•˜์ง€๋งŒ ๊ณง ์•Œ๊ฒŒ ๋ ) ๊ฐœ๋…์ด๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๊ธฐ์„  ์œ„ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๊ธ€์˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค.
 

#4-3 Event.kt

// https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

/* ์‚ฌ์šฉ ๋ก€

๋ทฐ ๋ชจ๋ธ์—์„œ,
    class ListViewModel : ViewModel {
        private val _navigateToDetails = MutableLiveData<Event<String>>()

        val navigateToDetails : LiveData<Event<String>>
            get() = _navigateToDetails


        fun userClicksOnButton(itemId: String) {
            _navigateToDetails.value = Event(itemId)  // Trigger the event by setting a new Event as a new value
        }
    }

์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ,
    myViewModel.navigateToDetails.observe(this, Observer {
        it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
            startActivity(DetailsActivity...)
        }
    })

*/

์œ„ ํด๋ž˜์Šค๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•œ๋‹ค.
 

#5 ์ฝ”๋“œ ์ˆ˜์ • - MVVM ํŒจํ„ด ์ค€์ˆ˜

#5-1 ViewModel ์ˆ˜์ •

...

class MainActivityViewModel : ViewModel() {
    ...

    private val _toastMessage = MutableLiveData<Event<String>>()
    val toastMessage: LiveData<Event<String>>
        get() = _toastMessage

    ...

    fun updateCount() {
        count.value = (count.value)?.plus(1)

        if (10 <= count.value!!) {
            _toastMessage.value = Event("${count.value}")
        }
    }
}

 

#5-2 Activity ์ˆ˜์ •

...

class MainActivity : AppCompatActivity() {

    ...
    private lateinit var viewModel: MainActivityViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        displayToastEvent()
    }

    private fun displayToastEvent() {
        viewModel.toastMessage.observe(this, Observer {
            it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
                Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
            }
        })
    }
}

#4-3์˜ ์•„๋ž˜์ชฝ ์ฃผ์„์— ์žˆ๋Š” ์‚ฌ์šฉ ๋ก€๋ฅผ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์™”๋‹ค. ์ด ์ฝ”๋“œ ๋˜ํ•œ #4-2์— ์žˆ๋Š” ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๊ธ€์—์„œ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๋‹ค.
 

#6 ์ž‘๋™ ํ™•์ธ

 

#7 ์š”์•ฝ

๋ณธ ๊ฒŒ์‹œ๊ธ€์—์„œ์™€ ๊ฐ™์ด MVVM ํŒจํ„ด์˜ ๋ถ€๋ถ„์ ์ธ ๋ฌด๊ฒฐ์„ฑ์„ ์œ ์ง€ํ•˜๋‹ค๋ณด๋ฉด, ์ „์ฒด์ ์ธ ๊ทธ๋ฆผ๋„ ์ž˜ ๋ณด์ผ ๋‚ ์ด ์˜ฌ ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•œ๋‹ค.
 

#8 ์™„์„ฑ๋œ ์•ฑ

 

android-practice/view-model/EventWithView at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com