경험의 기록

2021.04.19 - [안드로이드/기본] - [Android] 안드로이드 AAC & MVVM

 

[Android] 안드로이드 AAC & MVVM

액티비티, 프래그먼트에 너무 많은 코드를 넣게 되면 점점 무거워져 다루기 힘들어지게 된다. 앱이 카메라 인텐트를 트리거합니다. 그러면 Android OS에서 요청을 처리하기 위해 카메라 앱을 실행

hanyeop.tistory.com

안드로이드에서 MVVM의 구현을 위해 View Model, LiveData 을 사용해보고자 한다.


※ View Model 이란?

  • MVVM 패턴에서 수명주기를 고려하여 데이터를 저장하고 관리한다.
  • 회전 등의 화면 변화가 있을때에도 데이터를 유지한다.
  • View Model 객체는 자동 보관되어 다른 activity나 fragment에서도 사용할 수 있다.

 

View Model을 사용하면 화면이 변경될 때 onSaveInstanceState() 등의 활용 없이 데이터를 보관하여 유지할 수 있다.

 

뷰모델은 액티비티가 종료될 때 까지 유지된다.

하지만 Lifecycle 또는 view, activity context를 참조하면 메모리 누수가 발생하므로, 이러한 객체를 참조해서는 안된다.

 

또한 LiveData와 같은 LifecycleObserver들을 포함할 수 있지만, LiveData와 같이 수명 주기를 인식하는 Observable의 변경사항을 관찰하면 안 된다. 

context가 필요하다면 AndroidViewModel을 상속하여 application 생성자를 사용하면 된다.

 

즉, ViewModle은 생명주기의 영향을 받지 않고 데이터를 유지, 보관하기 위해 사용한다고 볼 수 있다.

또한 UI와 컨트롤러가 분리되고, 다른 액티비티와 프래그먼트 간의 데이터 공유가 쉬워지는 장점도 있다.

 

 

※ LiveData 란?

  • 관찰자 (액티비티, 프래그먼트) 의 생명주기를 알고 있는 간단한 observable
  • observer 패턴을 구현하기 위하여 사용됨.
  • 보통 ViewModel 과 함께 사용된다.
  • 수명주기를 수동으로 처리하지 않아도 되고, 메모리 누수가 사라진다.

 


 

사용해보기

dependencies 추가

def lifecycle_version = "2.3.0"

    // ViewModel - 라이프 사이클
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // LiveData - 데이터의 변경 사항을 알 수 있음
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

dependencies 에 뷰모델과 라이브데이터를 추가해준다.

 

레이아웃 만들기

그 후 임의의 레이아웃을 만들어준다.

UP 을 누르면 텍스트뷰의 숫자가 올라가고, DOWN 을 누르면 숫자가 내려가는 간단한 테스트를 해보고자 한다.

 

ViewModel 추가

enum class actionType{
    UP, DOWN
}
// 데이터의 변경사항을 알려주는 라이브 데이터를 가지는 뷰모델
class MainViewModel : ViewModel() {

    // 변경가능한 Mutable 타입의 LiveData
    private val _currentValue = MutableLiveData<Int>()

    // 무결성을 위한 Getter
    val currentValue : LiveData<Int>
        get() = _currentValue

    // 초기값
    init{
        _currentValue.value = 0
    }

    // Setter
    fun updateValue(type : actionType){
        when(type){
            actionType.UP ->
                _currentValue.value = _currentValue.value?.plus(1)
            actionType.DOWN ->
                _currentValue.value = _currentValue.value?.minus(1)
        }
    }
}

ViewModel을 상속하는 클래스를 생성해주고,

옵저버 패턴을 위한 MutableLiveData를 생성해준다.

이 currentValue가 메인에서 텍스트뷰에 사용할 값이다.

udateValue에서 UP 을 받을때마다 1을 증가시키고, DOWN을 받을때마다 1을 감소시킨다.

 

MainAcitivity

class MainActivity : AppCompatActivity(), View.OnClickListener {

    lateinit var mainViewModel : MainViewModel
    lateinit var button1 : Button
    lateinit var button2 : Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val text = findViewById<TextView>(R.id.numberText)

        button1 = findViewById(R.id.button)
        button2 = findViewById(R.id.button2)
        button1.setOnClickListener (this)
        button2.setOnClickListener (this)

        // 뷰모델 가져오기
        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        // 관찰하여 데이터 값이 변경되면 호출
        mainViewModel.currentValue.observe(this, Observer {
            text.text = it.toString()
        })

    }

    override fun onClick(p0: View?) {
        when(p0){
            button1 ->
                mainViewModel.updateValue(actionType.UP)
            button2 ->
                mainViewModel.updateValue(actionType.DOWN)
        }
    }
}

ViewModelProvider로 뷰모델을 가져오고

observe로 현재값을 관찰하여 바뀔때마다 텍스트뷰에 할당해준다.

 

잘 구현되었다.

❗ 단, 여기서 주의할 점이 있다.

여기서는 옵저버패턴을 메인액티비티에서 구현하였기 때문에 문제가 없지만,

프래그먼트에서 옵저버 패턴을 구현할 경우 화면 회전등의 경우에 프래그먼트는 생명주기가 다르기 때문에 onDestroy를 거치지 않아서 중복으로 옵저버 구독이 되는 일이 발생한다.

그것을 방지하기 위하여

 mainViewModel.currentValue.observe(this, Observer {
            //~~~~~
        })

위와 같은 코드에서 owner를 this가 아닌

 mainViewModel.currentValue.observe(viewLifecycleOwner, Observer {
            //~~~~~
        })

onCreateView부터 onDestroyView까지의 생명주기를 가지는 viewLifecycleOwner 를 사용해야 한다.

 

 

 

❗❗ 다시 돌아와서, 이 코드는 버튼이나 텍스트 등이 많아질 경우 findViewById 를 많이 써야 할 것이다. 그것을 보완하여 더 깔끔한 MVVM를 구현하기 위해 DataBinding을 사용한다.

 

DataBinding 사용하기

buildFeatures {
        dataBinding true
    }

데이터바인딩을 사용할 것이기 때문에 bulid.gradle (Module)  android에 추가해준다.

데이터바인딩에 대한 내용은

hanyeop.tistory.com/170

 

[Android] 안드로이드 DataBinding 사용하기

※ DataBinding 이란? 데이터와 뷰를 연결하는 작업을 레이아웃에서 처리 할 수 있게 해주는 라이브러리 findViewById (R.id.sample_text).apply { text = viewModel.userName } 예를들어, textView에 뷰모델에서..

hanyeop.tistory.com

에서 확인할 수 있다.

 

 

레이아웃 수정

추가적으로 밑에 버튼을 누르면 현재 저장된 값을 위의 텍스트뷰에 불러오려고 한다.

 

데이터바인딩을 위해 layout으로 바꿔주고 mainViewModel을 추가한다.

 

그 후 두 텍스트뷰에 뷰모델에 있는 값을 바인딩해준다.

LiveData를 Int로 선언했으므로 toString을 사용하여 String으로 바꿔준다.

 

또한 onClick에 각자 사용할 클릭메소드를 바인딩해준다.

 

ViewModel 수정

// 데이터의 변경사항을 알려주는 라이브 데이터를 가지는 뷰모델
class MainViewModel : ViewModel() {

    // 변경가능한 Mutable 타입의 LiveData
    private val _currentValue = MutableLiveData<Int>()
    private val _currentValue2 = MutableLiveData<Int>()

    // 무결성을 위한 Getter
    val currentValue : LiveData<Int>
        get() = _currentValue
    val currentValue2 : LiveData<Int>
        get() = _currentValue2

    // 초기값
    init{
        _currentValue.value = 0
        _currentValue2.value = 0
    }

    // Setter
    fun updateValue(type : Int){
        when(type){
            1 ->
                _currentValue.value = _currentValue.value?.plus(1)
            2 ->
                _currentValue.value = _currentValue.value?.minus(1)
        }
    }

    fun setValue(){
        _currentValue2.value = _currentValue.value
    }
}

두번째 텍스트뷰를 위한 값을 따로 추가해주고

호출되면 현재 값을 받아오는 setValue 메소드를 추가해준다.

 

 

MainAcitivity 수정

class MainActivity : AppCompatActivity() {

    lateinit var mainViewModel : MainViewModel
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.activity_main)

        // 뷰모델 가져오기
        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        binding.mainViewModel = mainViewModel

        /* 관찰하여 데이터 값이 변경되면 호출, this 와 라이프사이클을 공유함
        (Livedata는 자동으로 Observe 상태를 관리함.) */
        mainViewModel.currentValue.observe(this, Observer {
            Log.d("check","현재 값 : $it")
            binding.numberText.text = it.toString()
        })

        mainViewModel.currentValue2.observe(this, Observer {
            binding.numberText2.text = it.toString()
        })
    }
}

currentValue 와 currentValue2 를 관찰하는 옵저버를 선언해주고

변경될 때 그 값을 텍스트뷰에 할당해준다.

데이터바인딩을 사용하였기 때문에 별도로 findViewById 를 사용할 필요가 없다.

 

이렇게 구현함으로써 코드가 더욱 깔끔해진다.

 

class MainActivity : AppCompatActivity() {

    lateinit var mainViewModel : MainViewModel
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.activity_main)

        // 뷰모델 가져오기
        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        binding.mainViewModel = mainViewModel

        // 뷰모델을 LifeCycle 에 종속시킴, LifeCycle 동안 옵저버 역할을 함
        binding.lifecycleOwner = this
    }
}

 또는 뷰모델을 생명주기에 종속시켜 옵저버 패턴을 구현하면, 데이터바인딩 된 변수가 자동으로 변경된다.

 

밑에 버튼을 누르면 두번째 텍스트뷰의 숫자가 바뀌는 것도 확인할 수 있다.

 

 

github.com/HanYeop/AndroidStudio-Practice/tree/master/ViewModel_LiveData

 

HanYeop/AndroidStudio-Practice

안드로이드 학습 내용 저장소. Contribute to HanYeop/AndroidStudio-Practice development by creating an account on GitHub.

github.com

 

 

 

 

 

참조

developer.android.com/jetpack/guide

www.youtube.com/watch?v=-b0VNKw_niY&list=PLgOlaPUIbynqmlbCQ_dHAgY7lRj5-Ti_f&index=1

https://velog.io/@cmplxn/Fragment-%EB%8B%A4%EB%A3%A8%EA%B8%B0

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading