본문 바로가기

Android/Basic

[Android: Jetpack] ViewModel & Lifecycle 기초(1)

 

 

ViewModel에 대해 정확히 알고 넘어가기 위해서는 먼저, Lifecycle에 대해 알아야한다.

Activity의 생명주기는 예전에 정리해둔 자료를 보도록 하자.

 

ViewModel을 사용하면 좋은 이유를 가장 와닿기 쉽게 떠올린다면, 화면을 회전시키는 상황을 떠올리면 된다.

화면을 회전하게 되면, 화면의 구성이 바꿔면서 그에 맞게 View를 재생성 해야한다. 파괴된 View를 다시 생성할 때, 액티비티가 보유 중이던 데이터가 사라지게 된다.

화면 전환 시, onDestroy()가 호출되고, 다시 onCreate가 실행되며 값이 초기화 되는 것이다.

이에 대한 해결 방법으로는 onSavedInstanceState가 있다.

이는 액티비티를 재생산 할 때 사라져버리는 휘발성 데이터를 메모리에 보존했다가 꺼내올 수 있도록 한다.

  • Map형식으로, Bundle에 저장한다.
  • onCreate 메서드에서 null체크를 한 후에 사용하면 된다.

하지만, 위 방식에는 문제가 있다.

먼저, Bundle의 형식이 거대한 데이터를 다룰 수 있는 포맷이 아니고, 해당 데이터는 IPC를 통과해야 하는데, IPC의 용량 제한은 1mb 정도이다. 결국 프로세스가 죽은(kill) 후, 데이터는 RAM에 보관되기 때문에 가능한 작게 유지되어야 하며, 권장 상 50kb 정도이다(암튼 작다는 뜻) → 자세한 내용은 참조

무엇보다, Bundle은 구조상 serialization에 사용할 수 없다.

 

그래서 ViewModel을 사용해야 한다.

ViewModel


액티비티와 ViewModel은 독립된 생명주기를 가지고 있다.

  • finish()에 의해 액티비티가 파괴되고, 이후에 재생 되는 동안에도 viewModel은 살아있게 된다.
  • ViewModel에서 데이터를 가지고 있도록 설계한 후, viewModel에서 데이터를 가져와 사용하는 방식으로 앱을 구현하면, 앱이 보다 안정적인 상태가 된다.

View Model 사용하기


app gradle: dependency 추가하기

androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1

 

ViewModel 구성하기

class MyViewModel: ViewModel() {
    var counter: Int = 0
}

 

ViewModel 사용하기

ViewModel 인스턴스를 그냥 생성할 경우, 인스턴스가 여러개 만들어 지는 문제가 발생할 수 있다.

그렇기 때문에, ViewModelPovider을 통해 인스턴스를 싱글톤으로 생성해야한다.

class MainActivity : AppCompatActivity() {
		// ViewBinding
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        val myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)
        myViewModel.counter = 100
        binding.textView.text = myViewModel.counter.toString()

        binding.button.setOnClickListener {
            myViewModel.counter += 1
            binding.textView.text = myViewModel.counter.toString()
        }
    }
}

여기서 잠깐 보고 넘어 가자.

ViewModelProvider의 생성자 원형은 다음과 같다.

⇒ 여기서 ViewModelStore은 HashMap으로, ViewModel 객체(object)를 관리하는 저장소 역할을 한다. 그리고 ViewModelStoreOwner는 이런 ViewModelStore을 소유하는 역할을 하는 인터페이스이다. ViewModelStoreOwner의 인스턴스는 ViewModelStore를 가지고 있다가 필요한 시점에 적절히 복원하거나 파괴해야 한다.

ViewModel을 선언하는 클래스가 다르더라도 생성자로 넘긴 ViewModelStoreOwner 객체가 같다면 ViewModelProvider는 동일한 ViewModel을 반환하게 된다. → Activity에 연결된 Fragment가 동일한 ViewModel을 공유할 수 있는 이유이다.

 

계속 새로운게 뿅뿅나오니 슬슬 머리가 복잡해진다.

ViewModelStoreOwner의 대표적인 인스턴스가 Activity와 Fragment라는 것과 얘를 써야 Activity, Fragment간 데이터 공유가 된다는 점만 알고 넘어가자.

 

아직까지는 ViewModel을 따로 분리한 의미가 없다. 위 로직에서는 myViewModel.counter = 100이게 onCreate()가 호출될 때 마다 수행된다.

그렇담, 이제 ViewModel을 초기화 할 때 100으로 counter를 초기화 해주는 로직을 추가하면 액티비티가 파괴후 재생성 되어도, myViewModel.counter의 값이 액티비티 생명주기의 영향에서 벗어나게 된다.

하지만, 여기에는 문제가 있다.

ViewModelProvider로 ViewModel 객체를 만들 때, 초기값을 전달하는 것이 금지되어 있다.

그렇기 때문에 Factory Pattern을 사용해야 한다.

 

그럼 코드를 리팩토링 해보자.

Factory class

viewModelProvier.Factory를 상속받는 팩토리 클래스를 생성한다.

class MyViewModelFactory(private val counter: Int): ViewModelProvider.Factory {
    override fun <T: ViewModel> create(modelClass: Class<T>): T{
        if(modelClass.isAssignableFrom(MyViewModel::class.java)){ //받은 modelClass가 우리가 원하는 MyViewModel일 경우
            @Suppress("UNCHECKED_CAST")
            return MyViewModel(counter) as T // 초기값을 담아 생성한 viewModel을 반환한다.
        }
        throw IllegalArgumentException("viewModel class not found")
    }
}

이 팩토리 클래스를 통해서 viewModel을 만든다.

 

이제 초기값을 전달 받을 수 있도록 viewModel을 수정한다.

class MyViewModel(_counter: Int): ViewModel() {
	var counter: Int = _counter
}

 

다음으로 액티비티 코드를 수정한다.

class MainActivity : AppCompatActivity() {
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        val factory = MyViewModelFactory(100) //⏪ 초기값을 팩토리에 전달
        val myViewModel = ViewModelProvider(this, factory)[MyViewModel::class.java] // ⏪ viewModelProvider가 팩토리를 통해 뷰모델 생성
        binding.textView.text = myViewModel.counter.toString()

        binding.button.setOnClickListener {
            myViewModel.counter += 1
            binding.textView.text = myViewModel.counter.toString()
        }

    }
}

 

자 이렇게 하면, ViewModel에 초기값이 전달 되었고, 위에 발생한 myViewModel.counter의 값이 액티비티 생명주기에서 완전히 벗어나게 되었다.

 

마지막으로, ViewModel의 또 다른 생성 방법을 알아보겠다.

ViewModel 생성방법(2) - by 키워드 사용


by 키워드를 통해 생성하는 방법이다.

먼저 gradle dependencies에 아래와 같은 내용을 추가해 준다.

implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.4'
  • 각각 activityViewModelLazy, fragmentViewModelLazy에 작업을 위임한다.

 

다음으로 액티비티에서 viewModel 인스턴스를 초기화 하는 방법이다.

val myViewModel by viewModels<MyViewModel>()

팩토리 패턴을 적용해서 초기값을 넘겨줘야하는 경우는 다음과 같다.

val Factory = MyViewModelFactory(100)
val myViewModel by viewModels<MyViewModel>{ factory } // 중괄호 "{"과 "}"을 사용한다.

 

Fragment에서의 ViewModel 초기화도 동일하다.

viewModels 또는 activityViewModels()를 사용할 수 있다.

  • viewModel 객체의 lifecycle이 현재 초기화를 수행하는 액티비트 혹은 프래그먼트와 연동된다.
  • 프래그먼트에서 activityViewModels()를 통해 초기화를 수행하게 되면, 프래그먼트가 아닌 현재 프래그먼트와 연결된 액티비티 lifecycle에 종속시킨다.
  • 만약, 한 액티비티에서 여러 프래그먼트를 생성하고(주로 Navigation기능을 사용 시), 해당 프래그먼트들 사이에서 viewModel을 공유해야할 때 사용할 수 있는 옵션이다.

 

다음 글로는 앱이 강제 종료 될 경우 데이터를 유지하는 savedStateHandle을 사용하는 방법을 겸해서 작성하겠다.


Uploaded by

N2T