ViewModel을 안전하게 사용하자!

Hanseul Lee·2022년 9월 2일
0

Jetpack을 공부하자!

목록 보기
3/5
post-custom-banner

ViewModel을 구현하는 법은 많은 포스팅에서 봤으니까... 안전하게 사용하는 법을 알아보자!

아키텍쳐의 기본적인 다이어그램

ViewModel은 UI에 필요한 모든 데이터를 보유하고 처리한다. 그래서 뷰 계층 구조에 액세스하거나 Activity나 Fragment의 참조를 보유해서는 안 된다.

Kotlin 속성 위임(property delegate)과 delefate class

  • var은 값을 할당할 때 setter, 속성 값을 읽을 때 getter를 호출한다.
  • val은 read-only기 때문에 기본적으로 getter만 생성된다.

Kotlin에서 이렇게 by를 활용해 속성 위임을 사용하면 getter-setter의 역할을 다른 클래스에 넘길 수 있다. 이때 다른 클래스를 바로 대리자 클래스(delegate-class)라고 한다. 아래와 같이 이용할 수 있다.

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()

왜 이렇게 속성을 위임 받아서 초기화 해야할까? viewModel로 계속 알아보자.

private val viewModel = GameViewModel()

위 코드처럼 기본 생성자를 사용해 초기화하면 ViewModel의 참조 상태를 읽게 된다. 기기를 회전해 Activity가 소멸되었을 때 ViewModel도 초기 상태의 인스턴스가 시작된다는 거다.

private val viewModel: GameViewModel by viewModels()

그러나 속성 위임 접근 방식으로 ViewModel 객체의 책임을 viewModels()라는 별도 클래스에 위임을 하게 되면, 이제 이 객체는 대리자 클래스인 viewModels()에 의해 내부적으로 처리된다. 이러면 첫 번째 액세스 시 자동으로 viewModel 객체를 만들게 되고, 구성이 변경되어도 계속 값을 유지했다가 요청이 들어올 때 반환하게 된다.
cf) Fragment 간 데이터 전달할 수 있는 공유 ViewModel

private val sharedViewModel: OrderViewModel by activityViewModels()

activityViewModels()는 fragment-ktx 모듈에 포함되어 있기 때문에 다음 의존성을 추가한다.

implementation "androidx.fragment:fragment-ktx:1.3.6"

cf) ViewModel의 생명주기

ViewModel의 데이터 안전하게 보호하기(backing property)

잊지 말자. ViewModel의 데이터를 다른 클래스에서 수정할 수 없도록 해야한다. 그러려면 데이터를 public으로 노출하지 말아야 한다.

  • ViewModel 내부에서는 데이터를 수정할 수 있어야 한다. 그래서 privat var이어야 한다.
  • ViewModel 외부에서는 데이터 읽기만 가능하고 수정하면 안 되기 때문에 public val이어야 한다.
내부에서 활용하는 데이터외부에서 활용하는 데이터
privatepublic
varval

그래서 지원 속성(backing property)를 이용한다. 이렇게 하면 앱 데이터가 외부 클래스로부터 원치 않게 변경되지 않게 보호할 수 있다.

  • ViewModel 내부
    // ViewModel 클래스 내에서만 액세스 가능, 수정 가능
    // 이름 앞에 _을 붙여서 표기하자
    private var _count = 0
  • ViewModel 외부
    // UI 컨트롤러(Activity나 Fragment) 같은 다른 클래스에서 액세스 가능
    // get() 메서드만 있기 때문에 재정의 불가, read-only
    val count: Int
       get() = _count

아래와 같이 활용한다.

// GameFragment.kt

private fun updateNextWordOnScreen() {
        binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
    }
// GameViewModel.kt

class GameViewModel : ViewModel() {

    ...
    private lateinit var _currentScrambledWord: String
    val currentScrambledWord: String get() = _currentScrambledWord
    
    init {
        _currentScrambledWord = "test"
    }

}
post-custom-banner

0개의 댓글