여기서 좋은 코드라는 것은 남이 심지어 내가 N개월, N년 뒤에 읽기만 해도 문맥이 파악되는 코드를 의미하며 여기에 좋은 유지보수성을 가지고 있다면 리팩토링을 하거나 기능 업데이트를 할 때에도 훨씬 편할 것이다.
아직 나는 개발 자체를 시작하지 않은 학생 개발자이지만 코드를 작성하기 위해 가장 먼저
1. 자신만의 코딩 컨벤션(코딩 규칙)을 정하고
2. 기능을 함수/클래스 단위로 분리(관심사 분리, Seperate Of Concern)시켜 코드의 재사용성을 높여야
할 것이다.
이번 게시글에서는 뷰(Activity, Fragment) 단에서 어떻게 기능을 분리시킬 수 있는 지, 그 기능을 도와주는 Android Architecture Component(AAC)인 ViewModel을 소개하고자 한다.
와 같은 작업이 발생할 것이다. 그렇다면 이 모든 과정을 뷰에서 처리하는 것이 좋을까? Google에서 제시한 앱 아키텍처 가이드에 의하면 기존 코드를 화면 처리(View), UI에서 발생하는 이벤트/데이터 로직 처리(ViewModel), 앱 내/리모트 데이터 송수신 처리(Repository, Model) 로 분리할 수 있다.
위와 같이 기능을 분리할 시 우선 코드가 Clean해지고 유지보수성이 높아진다는 장점이 있으며 더욱이 앱 사용자는 다양한 사용자 환경에서 앱과 상호작용을 할 것이다.
예를 들어 동영상을 시청할 경우 세로로 동영상을 시청하다가 핸드폰을 돌려 가로로 동영상을 시청할 수도 있을 것이다. 이 때, 뷰는 종료되었다가 다시 만들어지는데 뷰에 있었던 모든 데이터들이 화면전환 한 번 했다고 모두 사라지는 것이다. 이외에도 모든 환경 변경 사항을 일일이 대응하는 것보다 뷰와는 독립된 클래스를 만들어서 각기의 기능을 수행하는 것이 개발자의 개발 능률을 더 높일 수 있을 것이다.
뷰의 수명주기에 관계없이, 뷰가 종료되거나 숨겨져도 데이터를 계속 보존할 수 없을까? Google 진영에서 내놓은 해답은 바로 ViewModel이다.
ViewModel은 UI 컨트롤러(뷰)에서 데이터 로직을 분리시키기 위해 만든 클래스로 뷰에서 데이터 입력을 받거나 특정 이벤트로 인해 데이터 처리를 해야할 때, ViewModel에서 이 업무를 하도록 책임을 할당한다.
이는 ViewModel의 특이한 생명주기에서 기인하는데, ViewModel은 ViewModelProvider라는 클래스에서 생명주기를 정해준다. 이런 생명주기로 인해 뷰모델 객체는 뷰가 완전히 종료될 때까지 존속한다.
우선 이번 실습의 의의는 다음과 같다
즉 이번 실습을 통해서 뷰모델에 데이터를 저장하고 역할 분리를 시도할 수 있을 것이다.
kotlin-android-extension은 제거한 채로 실습을 진행했다. 이후 게시글을 통해 더 발전시킬 것이다.
// Android-KTX
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.fragment:fragment-ktx:1.2.5"
ViewModel은 ViewModelProvider를 통해 객체를 제공받는다. 그러나 KTX(안드로이드 코틀린 확장함수 라이브러리)를 활용하면 by viewModels()
(Fragment에서는 by activityViewModels()
)로 객체를 제공받는다.
class MainViewModel : ViewModel() {
private var name = ""
fun setName(newName: String) {
name = newName
}
fun getName() = name
}
ViewModel을 상속받는 클래스를 만들고 이름을 저장하는 변수와 setter, getter를 만들어준다
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btn_main_input).setOnClickListener {
val word = findViewById<EditText>(R.id.et_main_name).text.toString()
mainViewModel.setName(word)
Snackbar.make(
findViewById(R.id.main_layout),
"ViewModel에 ${word}가 저장되었습니다.",
Snackbar.LENGTH_SHORT
).show()
}
findViewById<Button>(R.id.btn_main_get).setOnClickListener {
findViewById<TextView>(R.id.txt_name).text = mainViewModel.getName()
}
}
}
Q. 왜 ViewModel은 뷰에서 직접 생성하지 않나요?
뷰에서는 ViewModel 객체를 직접 생성받지 않는다. 이는 위에서 언급한 생명주기와 관련되어 있기 때문인데, 객체를 클래스 내부에서 생성하면 그 객체는 그 클래스의 생명주기에 영향을 받는다.
또한 ViewModel은 위에 생명주기 그림에서 보듯이 onCreate가 몇 번 호출되어도 단 하나의 객체로 존재해야한다. 만약 내부에서 객체를 생성하는 방식으로 ViewModel을 주입한다면 뷰의 환경이 변할 때마다 계속 생성해야할 것이다. 따라서 ViewModel은 외부(Delegate Pattern 혹은 ViewModelProvider)에서 제공받는 방식으로 생성한다.
위와 같이 뷰에 뷰모델을 생성하였다면 클릭 리스너를 달아서 해당 기능에 ViewModel 내의 멤버 변수를 조작하는 방식으로 데이터를 처리한다.
실습 결과
<참고 자료>
실습 코드
https://github.com/l2hyunwoo/SampleDataBinding 에서 feature/viewmodel 참고
잘 보고 갑니다! 도움이 많이 되네요