반응형

안드로이드는 액티비티나 프래그먼트를 관리할 때 Lifecycle 을 토대로 관리합니다.

 

그림 1. 액티비티 라이프 사이클

우리는 이러한 라이프사이클을 직접 관리하는 것이 아니라 안드로이드 시스템이 각 라이프사이클에 맞춰서 호출해주는 콜백 함수(onCreate() 등)을 Override 하여 때에 맞는 로직을 추가할 수 있습니다.

 

안드로이드를 시작한 지 얼마 안되신 분들이 많이 겪는 문제 중 하나가 세로 모드에서 가로 모드 전환시 데이터가 초기화 되거나 날아가는 현상입니다. 앞서 라이프사이클에 대해 말씀드린 이유는 바로 이 문제의 원인이 라이프사이클에 있기 때문입니다.

 

안드로이드의 액티비티는 세로 모드/가로 모드 전환시 라이프사이클 중 onDestory()가 호출된 다음 onCreate()가 다시 호출됩니다. onCreate()가 다시 호출될 때 xml layout이 다시 Inflate 되기 때문에 뭔가 작업 중이던 데이터가 초기화되게 됩니다.

 

이에 대한 해결책으로 onSaveStateInstance()와 ViewModel 클래스가 있습니다만, 오늘은 그 중 ViewModel에 대해서만 다루도록 하겠습니다.

 

AAC ViewModel

ViewModel이면 그냥 ViewModel 이지, 왜 자꾸 AAC ViewModel 이라고 언급할까요? 그건 오늘 다루는 ViewModel 클래스가 MVVM 아키텍쳐에서 말하는 ViewModel 과는 전혀 관련이 없기 때문에 확실히 구분하기 위해서입니다.

 

그렇다면 AAC는 뭘까요?

AAC는 Android Architecture Component의 약자로, Jetpack 라이브러리 중 "아키텍처" 부분에 해당합니다.

이번 포스팅에서는 편의상 ViewModel 이라고 부르겠습니다.

 

ViewModel 클래스는 액티비티와 프래그먼트의 데이터를 관리하는 데 사용됩니다. 그리고 액티비티(또는 프래그먼트)와 어플리케이션 내 다른 클래스들과의 커뮤니케이션 하는데 사용합니다. 

 

ViewModel 클래스는 액티비티나 프래그먼트의 Scope와 함께 생성되고, 이 Scope가 살아있는 동안에 유지됩니다. 다르게 말하면, 액티비티나 프래그먼트가 Screen Rotation 등의 이유로 Destroy 상태가 되더라도 ViewModel은 Destroy 되지 않는 것을 의미합니다.

 

그림 2. AAC ViewModel 의 Scope

 

ViewModel 클래스는 주로 LiveData와 Data Binding과 함께 사용됩니다.
([Android] AAC ViewModel과 찰떡 궁합! LiveData 이해하기)

 

주의하실 점은 ViewModel은 오직 UI에 대한 데이터만을 관리하는 데에만 책임을 가져야 합니다. 그렇기 때문에 ViewModel은 View에 대한 접근을 하거나 액티비티나 프래그먼트에 대한 참조를 갖고 있으면 안됩니다.

 

예제

간단한 예제를 통해 코드와 함께 살펴보겠습니다.

예제는 화면 중앙에 TextView를 두고, EditText를 통해 값을 변경한 다음에 화면을 가로/세로 전환했을 때 데이터가 유지되는지 ViewModel 사용한 버전과 사용하지 않은 버전을 비교해보겠습니다.

 

예제 화면

 

공통

먼저 버전에 상관 없이 공통으로 사용되는 layout을 설정합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edit_text"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:inputType="text"
        android:layout_margin="10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/ok_button" />

    <Button
        android:id="@+id/ok_button"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:gravity="center"
        android:text="OK"
        android:layout_margin="10dp"
        android:background="@drawable/button_selector"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

ViewModel 사용하지 않은 버전

ViewModel을 사용하지 않은 버전의 액티비티 코드입니다.

/* ViewModel 사용하지 않은 버전 */
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        ok_button.setOnClickListener {
            sample_text.text = edit_text.text
        }
    }
}

여기까지 작성하시고 앱을 실행해보시면 EditText에 텍스트를 입력 후 버튼을 누르면 중앙에 있는 TextView가 변경되지만, 화면을 전환하면 다시 초기값으로 돌아가는 것을 확인할 수 있습니다.

 

ViewModel을 사용한 버전

ViewModel을 정의하는 방법은 androidx.lifecycle 패키지의 ViewModel 추상 클래스를 상속하면 됩니다.

만약 어플리케이션과 생명주기를 함께하는 싱글톤 ViewModel을 선언하고 싶으시다면 AndroidViewModel을 상속하시면 됩니다.

 

먼저, ViewModel과 LiveData를 사용하기 위해서는 모듈 수준의 build.gradle 파일에 androidx.lifecycle 패키지에 대한 dependency 를 설정해줍니다.

dependencies {
    // ...
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}

 

다음으로 ViewModel 클래스를 다음과 같이 정의해줍니다.

원활한 예제를 위해 LiveData를 간단하게 사용하였습니다.

class MainViewModel : ViewModel() {
    private var inputText: MutableLiveData<String> = MutableLiveData()

    fun getInputText() = inputText

    fun updateText(newText: String) {
        inputText.value = newText
    }
}

 

다음으로 액티비티 코드를 작성해줍니다.

한 가지 알아야 하는 것은 우리는 ViewModel 클래스를 선언할 때 직접 생성자를 사용하여 인스턴스를 생성할 수 없습니다. 안드로이드에게 생성에 대하여 위임해야 합니다. 따라서 우리는 ViewModelProvider 를 통해 뷰모델 객체를 생성해야 하며, 관련해서 다양한 방법들이 있지만 자세한 내용은 별도의 포스팅에서 다루도록 하겠습니다.

/* ViewModel 사용한 버전 */
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewModel.getInputText().observe(this, Observer<String> { newStr ->
            sample_text.text = newStr
        })

        ok_button.setOnClickListener {
            viewModel.updateText(edit_text.text.toString())
        }
    }
}

여기서 주의깊게 보실 부분은 ViewModelProvider(this) 의 this 입니다.

이는 ViewModel 객체의 ViewModelStoreOwner를 넘겨주는 것이며, 여기서 어떤 액티비티나 프래그먼트를 설정해주느냐에 따라 ViewModel의 생명주기가 결정됩니다.

 

이렇게 작성하신 다음 실행해보시면 중앙에 있는 TextView 값을 변경한 후에 화면을 전환 하더라도 값이 그대로 유지가 되어있는 것을 확인하실 수 있습니다.

 

onCleared()

ViewModel을 더이상 사용하지 않거나, 뷰모델이 관찰하고 있는 데이터를 초기화해야할 때는 onCleared()를 호출하시면 됩니다. 이를 잘 활용하면 메모리 누수(Memory Leak)을 예방할 수 있습니다.

 


AAC ViewModel은 단독으로 쓰일때보다 LiveData와 DataBinding 등과 함께 사용될 때 진가를 발휘합니다. 이번 포스팅에서 간단하게 ViewModel을 살펴보았으니 추후 Data Binding과 함께 MVVM 아키텍쳐 포스트를 할 때 다시 다루도록 하겠습니다.

 

예제 코드는 Github 저장소에서 확인하실 수 있습니다.

 

 

반응형
반응형