[Android / Kotlin] ViewModel

Subeen·2023년 12월 22일
0

Android

목록 보기
23/73

ViewModel이란?

ViewModel이란 Android Jepack의 구성 요소 중 하나로, 본래 ViewModel이란 이름은 소프트웨어 디자인 패턴 중 하나인 MVVM(Model - View - ViewModel) 디자인 패턴으로부터 파생되었다.

ViewModel이 필요한 이유

MVVM의 관점에서 봤을 때 ViewModel은 View로부터 독립적이며, View가 필요로 하는 데이터만을 소유한다.
안드로이드 앱 개발시에도 MVVM 디자인 패턴을 적용하면 Activity나 Fragment 같은 UI 컨트롤러의 과도한 책임을 분담하여 클래스가 거대해지는 것을 방지하고, 유지보수, 재사용성 그리고 테스트 등을 용이하게 만들어 준다.

ViewModel 특징

ViewModel은 Activity에서는 Activity가 완전히 종료될 때까지, 그리고 Fragment에서는 Fragment가 분리될 때까지 메모리에 남아있도록 설계 되어있다.

위의 그림을 보면 Activity의 생명주기와 ViewModel의 생명주기를 함께 확인할 수 있다. 액티비티가 최초 생성될 때 일반적으로 ViewModel을 인스턴스화하여 생명주기를 함께 시작한다.

Configuration 변경(예: 화면 회전)이 발생할 때 액티비티가 다시 시작되는 것을 확인할 수 있다.
하지만 ViewModel은 여전히 메모리 상에 남아있는다.
이는 Activity 내부에서 Configuration 변경과 무관하게 유지 되는 NonConfigurationInstances 객체를 따로 관리하기 때문이다.

Activity의 finish() 호출 등에 의한 액티비티가 생명주기가 종료됨에 따라 내부의 LifecycleEventObserver를 통해 ViewModel도 onCleared() 콜백 메서드를 호출하고 종료된다.

버튼을 클릭했을 때 현재 값에서 1을 더해 값을 카운트 해주는 테스트 앱으로 화면을 가로로 회전 했을 때 초기값인 100으로 돌아가는 것을 확인할 수 있다.

// ViewModel을 사용하지 않은 코드
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()
        }
    }
}

ViewModel 구현 (1)

ViewModel을 사용하여 ViewModel을 초기화할 때 초기값 100을 건네주고 나머지는 로직에서 저장 된 값을 사용하도록 할 경우 위의 영상과 같이 화면을 회전해도 값이 초기화 되지 않는 것을 확인할 수 있다.

ViewModel

// ViewModel 클래스를 상속하는 서브 클래스를 정의한다.
class MyViewModel(_counter: Int) : ViewModel() {
    var counter = _counter
}

ViewModelProvider.Factory

  • ViewModelFactory는 생성자 매개변수를 사용하거나 사용하지 않고 ViewModel 개체를 인스턴스화 한다. 즉, ViewModel을 통해 전달되는 인자가 있을 때 사용된다.
/*
ViewModel 객체를 만들 때 초기값을 전달하는 것이 금지되어 있기 때문에 Factory 패턴을 사용해야 됨
ViewModelProvider를 상속 받는 Factory 클래스
*/
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")
    }
}

ViewModelProvider

  • ViewModelProvider는 ViewModel 객체를 생성하기 위해 사용하는 클래스로 Activity나 Fragment에서는 ViewModelProvider를 통해 자기 자신을 생성자로 전달하여 ViewModel 인스턴스를 획득해야 한다.
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        
        // ViewModel에 전달하고 싶은 초기값을 Factory에 전달 
        val factory = MyViewModelFactory(100)
        /*
        ViewModel 인스턴스 생성
        ViewModel 인스턴스를 그냥 생성하면 경우에 따라 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있음
        ViewModelProvider를 통해 인스턴스를 싱글톤으로 생성하도록 함
        */
        val myViewModel = ViewModelProvider(this, factory).get(MyViewModel::class.java)
        binding.textView.text = myViewModel.counter.toString()

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

ViewModel 구현 (2)

ViewModel 객체는 ViewModelProvider를 사용해 생성할 수도 있지만 by 키워드를 이용해 간편하게 만들 수도 있다.

build.gradle (:app)

  • build.gradle(앱 수준)에서 라이브러리의 종속 항목을 추가한다.
dependencies {
    ...

    // viewModel 사용을 위한 dependency
    implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")

    implementation("androidx.activity:activity-ktx:1.8.2")  // add
    implementation("androidx.fragment:fragment-ktx:1.6.2")  // add
}

by viewModels()

  • by viewModels()를 사용하면 ViewModelProvider를 사용하지 않고 ViewModel을 지연 생성할 수 있다.
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        
        // ViewModel에 전달하고 싶은 초기값을 Factory에 전달 
        val factory = MyViewModelFactory(100) 
        
		// by viewModels 사용 
        val myViewModel by viewModels<MyViewModel>() {factory}
        binding.textView.text = myViewModel.counter.toString()

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

ViewModel 구현 (3)

앱이 강제 종료 된 경우에도 View Model의 데이터를 저장하려면 Save State 핸들을 사용하면 된다.
Save State는 많은 데이터를 저장할 수는 없지만 시스템에 의한 강제 종료가 발생해도 살아남을 수 있다.

  • UI 상태를 유지하기 위한 옵션
ViewModel저장된 인스턴스 상태영구 저장소
저장소 위치메모리 내디스크에 직렬화디스크 또는 네트워크 내
구성 변경 시에도 유지
시스템에서 시작된 프로세스 중단 시에도 유지아니요
사용자의 완전한 활동 닫기 /onFinish() 시에도 유지아니요아니요
데이터 제한복잡한 객체도 괜찮지만 사용 가능한 메모리에 의해 공간이 제한 됨원시 유형 및 문자열과 같은 단순하고 작은 객체만 해당디스크 공간 또는 네트워크 리소스에서 검색하는 비용/시간에 의해서만 제한됨
읽기/쓰기 시간빠름(메모리 액세스만)느림(직렬화/역직렬화 및 디스크 액세스 필요)느림(디스크 액세스 또는 네트워크 트랜잭션 필요)

build.gradle (:app)

  • build.gradle(앱 수준)에서 라이브러리의 종속 항목을 추가한다.
dependencies {
	...
    // viewModel 사용을 위한 dependency
    implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
    // by viewModels()
    implementation("androidx.activity:activity-ktx:1.8.2")
    implementation("androidx.fragment:fragment-ktx:1.6.2")

    // save state
    implementation("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha11")  // add
}

ViewModel

class MyViewModel(_counter: Int,
    private val savedStateHandle: SavedStateHandle) : ViewModel() {  // 생성자로 savedStateHandle을 받도록 함
    // savedStateHandle에서 값을 가져오게 하는데 만약 null이면 전달 받은 초기값을 사용하도록 함
    var counter = savedStateHandle.get<Int>(SAVE_STATE_KEY) ?: _counter

    fun saveState() {
        savedStateHandle.set(SAVE_STATE_KEY, counter)  // count 값을 저장하는 SaveState를 정의
    }

    companion object {
    	// savedStateHandle은 Key Value 형태로 값을 저장하기 때문에 저장과 복원에 사용할 Key를 정해줌
        private const val SAVE_STATE_KEY = "counter"  
    }

}

AbstractSavedStateViewModelFactory(owner, defaultArgs)

class MyViewModelFactory(
    private val counter: Int,
    owner: SavedStateRegistryOwner,
    defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
    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을 반환할 때 핸들을 함께 반환하도록 함
        }
        throw IllegalArgumentException("Viewmodel class not found")
    }
}

saveStateHandle.set(SAVE_STATE_KEY, counter)

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

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

        // ViewModel의 카운터 값을 save state에 연결해서 시스템에 의한 종료가 발생해도 값이 유지 되도록
        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() // 카운터 값을 늘릴 때마다 그 값을 save state를 통해서 save state 핸들에 값을 저장
        }
    }
}

참조
ViewModel이란 무엇인가?

profile
개발 공부 기록 🌱

0개의 댓글