ViewModel과 Lifecycle 기초

강민아·2024년 1월 23일
post-thumbnail

Activity 생명주기


The activity lifecycle  |  Android Developers

  • Activity생명주기

onSaveInstanceState에 의한 상태 보존


Example

기본적인 코드

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

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

        // 리셋되는 카운터
        var counter = 100
        binding.textView.text = counter.toString()

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

문제

  • Activity가 onDestroy에 의해 파괴되고 onCreate에 의해 재생성될 때 var counter = 100 이 수행되어 값이 초기화됨
  • 재생성이 일어나면 Activity가 가지고 있던 Data가 초기화되는 문제가 발생함

onSaveInstanceState

  • 구글에서는 Activity를 재생성할 때 사라지는 휘발성 Data를 잠깐 보관하기 위해 onSaveInstanceState를 만들어 제공함
    ref) 표 출처
    이를 사용하면 Map형식으로 Bundle에 데이터를 저장하고 onCreate에서 null체크를 한 뒤 값을 가져오면 된다.
    그러나) 이때 사용되는 Bundle이 거대한 Data를 다루기 위해 만들어진 포맷이 아님
    → Data의 크기를 50k로 제한하도록 권장
    → 구조상 Serialization에 사용할 수 없음

Jetpack ViewModel의 제안


ViewModel overview  |  Android Developers

ViewModel의 생명주기

Activity와는 독립된 생명주기를 가짐

→ Actvitiy가 파괴되고 재생성되는 동안에도 ViewModel은 살아있음

Example


1) ViewModel만 사용

MainActivity.kt

class MainActivity : AppCompatActivity() {
    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)
				//viewModel 인스턴스를 그냥 생성하면 경우에 따라서는 인스턴스가 여러 개 만들어지는 문제가 발생하기 떄문에
				// ViewModelProvider을 통해서 인스턴스를 싱글톤으로 생성한다.
        myViewModel.counter = 100
        binding.textView.text = myViewModel.counter.toString()

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

MyViewModel.kt

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

정리

[문제]

위 코드는 ViewModel을 사용하고 있지만 onCreate에서 counter값을 100으로 지정하는 로직이 존재해 Activity가 재생성될 때마다 counter값이 100으로 돌아가게 됨.

[해결]

ViewModel을 초기화할 때 counter의 초기값인 100을 건네주고 나머지 로직에서는 저장된 값을 사용하도록 함

\therefore ViewModelProvider로 ViewModel 객체를 만들 때 초기값을 전달하는 것이 금지되어 있으므로 Factory 패턴을 사용해야 한다.

→ ViewModelProviderFactory를 상속받는 Factory 클래스를 만들어준다.

2) ViewModelFactory 적용

MyViewModelFactory.kt

ViewModelProvider.Factory를 상속받는 Factory 클래스

  • ViewModel을 만들 때 Factory를 통해서 만들게 됨
  • ViewModel을 Factory가 받아서 형식이 MyViewModel일 경우 counter을 담아가지고 ViewModel에 반환한다.
@Suppress("UNCHECKED_CAST")
class MyViewModelFactory(private val counter:Int): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if(modelClass.isAssignableFrom(MyViewModel::class.java)) {
           return MyViewModel(counter) as T
        }
        throw IllegalArgumentException("Viewmodel class not found")
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val binding:ActivityMainBinding by lazy{
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        //factory
        val factory = MyViewModelFactory(100)
				//ViewModel에 전달하고 싶은 초기값을 Factory에 전달한다.
        val myViewModel = ViewModelProvider(this, factory).get(MyViewModel::class.java)
				//ViewModelProvider는 Factory를 같이 받는다. 
				//=> ViewModelProvider가 Factory를 통해서 MyViewModel을 생성할 수 있도록 한다.		
        binding.textView.text = myViewModel.counter.toString()
				
        binding.button.setOnClickListener {
            myViewModel.counter += 1
            binding.textView.text = myViewModel.counter.toString()
        }
    }
}

MyViewModel.kt

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

정리

Activity가 재생성되어도 ViewModel에 있는 값을 가져오기 때문에 유지가 된다.

Activity가 아닌 ViewModel이 데이터를 가지게 하면 Lifecycle이 분리되기 때문에 화면이 회전해도 상태를 신경쓸 필요가 없다. 다만 앱이 강제 종료될 경우에는 ViewModel도 데이터를 유지할 수 없다.

→ 앱이 종료되어도 viewModel의 값을 저장하고 싶다면 SavedStateHandle 을 사용하면 된다.

3. SavedStateHandle 적용

ViewModel의 저장된 상태 모듈  |  Android 개발자  |  Android Developers

MyViewModelFactory.kt

@Suppress("UNCHECKED_CAST")
class MyViewModelFactory(
    private val counter: Int,
    owner: SavedStateRegistryOwner,
    defaultArgs: Bundle? = null,
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {package com.getupmina.android.viewmodellifecycle

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.ViewModelProvider
import com.getupmina.android.viewmodellifecycle.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private val binding:ActivityMainBinding by lazy{
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

//        var counter = 100
//        binding.textView.text = counter.toString()
//
//        binding.button.setOnClickListener{
//            counter += 1
//            binding.textView.text = counter.toString()
//        }
/*        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()
        }
        */

        //factory
        val factory = MyViewModelFactory(100,this)
        val myViewModel by viewModels<MyViewModel>(){factory}

        binding.textView.text = myViewModel.counter.toString()
        binding.button.setOnClickListener {
            myViewModel.counter += 1
            binding.textView.text = myViewModel.counter.toString()
            myViewModel.saveState() //counter값을 늘릴 때마다 저장
        }
    }
}
    override fun <T : ViewModel?> create(
        key: String,
        modelClass: Class<T>,
        handle: SavedStateHandle,
    ): T {
        if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
            return MyViewModel(counter, handle) as T 
						//viewModel을 반환할 때 handle울 같이 반환하도록 함.
        }
        throw IllegalArgumentException("Viewmodel class not found")
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val binding:ActivityMainBinding by lazy{
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        //factory
        val factory = MyViewModelFactory(100,this)
        val myViewModel by viewModels<MyViewModel>(){factory}

        binding.textView.text = myViewModel.counter.toString()
        binding.button.setOnClickListener {
            myViewModel.counter += 1
            binding.textView.text = myViewModel.counter.toString()
            myViewModel.saveState() //counter값을 늘릴 때마다 저장
        }
    }
}

MyViewModel.kt

class MyViewModel(_counter: Int, private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var counter:Int = savedStateHandle.get<Int>(SAVE_STATE_KEY)?:_counter
    //null이면 전달받은 초기값을 사용할 수 있도록 설정
    fun saveState(){
        savedStateHandle.set(SAVE_STATE_KEY,counter)
    }

    companion object{
        private const val SAVE_STATE_KEY = "counter"
    } //key를 설정
}

출처


강의

냉동코더의 알기 쉬운 Modern Android Development 입문 강의 - 인프런

출처 : 냉동코더의 알기 쉬운 Modern Android Development 입문

코드

https://github.com/cliearl/book-search-app

Designed and developed by 2022 FrozenCoder

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

썸네일 사진

UnsplashMohamed Nohassi

profile
개발자꿈나무

0개의 댓글