본문 바로가기

TIL

[TIL] Kotlin ViewModel / LiveData / Observer Pattern

[오늘 배운 내용]

-1- ViewModel

ViewModel을 왜 사용하는지

  • 구성 변경에 대한 데이터 보존
    • Activity나 Fragment는 화면 변경 등과 같은 이유로 구성 변경이 이루어지면 onCreate부터 재생성이 된다. Activity나 Fragment가 재생성이 되어버리면 기존의 UI 상태와 Data가 손실될 수 있다. ViewModel은 Activity와 Fragment의 생명주기와 독립적으로 존재하며 더 긴 생명주기를 갖고 있어 구성 변경 시에도 데이터를 보존할 수 있다.
  • 비즈니스 로직 분리
    • Activity나 Fragment에 비즈니스 로직과 UI가 함께 있어 유지보수가 어려우나 ViewModel을 통해 비즈니스 로직을 분리하여 단일 책임 원칙을 따르고, 코드의 가독성 및 유지보수성을 향상시킬 수 있다
  • 데이터 공유
    • ViewModel은 하나의 Activity에서 여러 Fragment가 동작하는 경우와 같이 상황에서 여러 컴포넌트간에 데이터를 공유하는 데 사용될 수 있다.

 

ViewModel의 생명주기 좌측에 Activity의 생명주기와 함께 나열한 그림.

 

 

 

ViewModel이란??

  • 위에서 나열한대로 ViewModel은 앱 개발에 유용한 아키텍처 컴포넌트로 UI와 관련된 데이터를 처리하고 유지하는데에 사용되며 데이터 공유도 ViewModel을 마치 싱글톤 객체처럼 사용이 가능해서 Fragment들 사이에 Activity또는 Fragment서로의 데이터를 쉽게 공유할 수 있도록 해줘 안정성을 향상 시킨다.
  • ViewModel 을 사용할 때 주의해야할 점은 Activity,Fragment,Service의 참조를 가지면 안된다. 이는 메모리 누수로 이어질 수 있으므로 ApplicationContext와 같은 앱 전반적인 리소스에만 접근해야 한다.

 

 

 

XML을 사용하지 않는 Jetpack Compose 또는 View Binding과 Hilt로 의존성관리를 함으로 DataBinding를 사용하지 않게 됨.

ViewModel에 DataBiding을 사용하는 이유

  • DataBinding을 사용하는 이유 변경 → DataBinding을 사용했던 이유, 사용해도 되지 않는 이유
  • 뷰와 데이터의 양방향 바인딩
    • DataBinding은 뷰와 데이터 간의 양방향 바인딩을 지원한다. 즉 UI 요소에 대한 사용자 입력이나 상태 변경이 자동으로 ViewModel에 반영되고, ViewModel의 변경 사항도 자동으로 UI에 업데이트 된다. 이는 사용자 입력 처리, 폼 유효성 검사, 동적 UI 업데이트 등의 작업을 편리하게 처리할 수 있도록 해준다.
    • ✏️사용자 입력은 Activity or Fragment를 통해. Data의 변동사항은 Observer Pattern 등으로 처리.
  • 복잡한 UI 로직 처리 용이
    • DataBinding은 XML 레이아웃 파일에서 조건문, 반복문 등 복잡한 로직을 사용하여 UI를 동적으로 구성할 수 있다. ViewModel에서 제공되는 데이터를 기반으로 XML에서 로직을 작성하면서 재사용 가능하고 유연한 UI 코드를 작성할 수 있다.
    • ✏️ ViewBinding도 DataBinding에 비하면 비교적 제한적이지만 동적으로 UI를 구성할 수 있으며 Compose를 사용하면 보다 정교한 동적 UI 처리가 가능하다.
  • 변환과 포맷팅
    • DataBinding은  XML에서 데이터 변환과 포맷팅을 쉽게 처리할 수 있는 기능을 제공한다. ViewModel에서 제공되는 데이터를 필요한 형식으로 반환하거나 날짜, 숫자, 등 값을 원하는 형식으로 포맷팅할 수 있다. 이로써 UI에 표시되는 데이터의 가독성과 일관성을 유지하며, 데이터가 추가적인 변환 작업을 거치지 않아도 된다.
    • ✏️ 데이터 변환 및 포맷팅은 View에서 처리하는 것보다, 비즈니스 로직에서 어떤식으로 데이터를 유저에게 보여줄 것인지 변환하는 것이 바람직함.
  • 이벤트 처리
    • DataBiding은 XML에서 이벤트 처리를 간편하게 할 수 있는 기능을 제공한다. ViewModel에서 정의된 메서드를 DataBinding을 통해 이벤트 핸들러로 연결할 수 있으며, 사용자 입력 등의 이벤트에 대한 응답 로직을 구현할 수 있다. 이로써 UI와 관련된 로직이 ViewModel에 명확히 분리되고, 코드의 가독성과 유지보수성이 개선된다.
    • ✏️이벤트 처리 로직은 Activity, Fragment에서 작성하는 것이 오히려 View(XML, Activity or Fragment ) 내부에서 나름의 이벤트 처리 로직은 Activity, Fragment로, XML은 View의 기본 토대만을 제공하는 것으로 분리하여 가독성이 좋아보임.

 

 

 

 

ViewModel 사용방법

  • ViewModel과 DataBinding을 사용하기위해 build.gradle에 android 확장기능 및 의존성에 추가 해주어야 한다.
  • fragment도 추가하는 이유는 Activity, Fragment에 ViewModel을 생성할 떄 필요하기 때문이다.

build.gradle

android {

	...

	dataBinding {
        enable = true
    }
}

dependencies {
	...
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
    implementation("androidx.fragment:fragment-ktx:1.6.1")

	...
}

 

 

activity_main.xml // UI 화면 구성

  • activity.main.xml 에서 EditText에 값을 입력하고 버튼 클릭 시 TextView에 값에 더해지는 xml 을 만들어준다.
  • 이때 DataBinding 을 적용하기 위해서 루트 요소에 <layout > 태그를 추가하고 그 안에 UI요소들을 정리한다
<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <EditText
        android:id="@+id/main_edit_input_num"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:ems="10"
        android:inputType="text"
        android:hint="InputNumber"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/main_tv_count"
        android:layout_width="140dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        android:gravity="center"
        android:text="0"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/main_edit_input_num" />

    <Button
        android:id="@+id/main_btn_plus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        android:text="더하기"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/main_tv_count" />


</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
  • xml 작업이 끝났으면 app/java/프로젝트이름 -> new 를 통해  MainActivityViewModel.kt를 생성해주자. 

 

 

MainActivityViewModel.kt

// ViewModel 생성

  • ViewModel 내부에서 UI에 보여질 Data에 대한 로직을 작성한다.
// ViewModel 생성
class MainActivityViewModel: ViewModel() {
    private var count = 0

    fun getCurrentCount():Int{
        return count
    }

    fun getUpdatedCount(plusCount: Int):Int{
        count += plusCount
        return count
    }
}

 

 

MainActivity.kt

// Activity 또는 Fragment 에서 ViewModel 연결

  • ViewModel 클래스를 생성하고 난 뒤 MainAcitivty 또는 Fragment에서 ViewModel 인스턴스를 가져온다.
  • 이때 Activity의 경우 by viewModels()로 적용하며 Fragment의 경우 by activityViewModels() 로 입력해주어야 한다.
  • dependencies에 Fragment 추가해주어야함.
// Activity에서 ViewModel인스턴스를 가져오는 경우
class MainActivity : AppCompatActivity()  {
    private lateinit var binding : ActivityMainBinding
    private val viewModel : MainActivityViewModel by viewModels()
    ...
    
    
}
// Fragment에서 ViewModel인스턴스를 가져오는 경우
class TestFragment : Fragment()  {
    private lateinit var binding : FragmentMainBinding
    private val viewModel : MainActivityViewModel by activityViewModels()
    
    ...
}

 


Fragment 에서 ViewModel 생성 시 activityViewModels() 를 사용하는 이유

더보기

1. Scope, Lifecycle

Fragment에는 Fragment 자체 생명 주기가 있어서 상위 Activity와 별개로 생성, 삭제, 재생성 될 수 있는데 viewModels()를 사용하게 되면 Fragment 자체의 생명주기에 영향을 받기 때문에 Fragment와 연결된 상위Activity의 생명주기와 상관없이 화면 전환등과 같이 구성이 변경 될 떄, Fragment의 생명주기가 끝나고 Activity에서 Fragment를 재생성 하게 되므로 ViewModel이 화면 회전과 같은 구성변경에서 데이터 보존을 할 수 없게 되어버린다.

 

2. Fragment 간의 데이터 공유

대부분의 경우 동일한 Activity에서 호스팅 하는 Fragment 간에 Data를 공유해야 한다. 이떄 activityViewModels()를 사용하지 않으면 각자 다른 ViewModels를 생성해 각자 다른 데이터에 액세스 하고 업데이트 하게 되어버린다. activityViewModels()로 ViewModel을 생성함으로 Fragment간의 일관되고 동기화된 Data를 공유할 수 있도록 한다.

 

3. 데이터 중복 방지

❌ Fragment 생성 시 피해야 하는 ViewModel 생성 방법

class MainActivity: AppCompatActivty() {
	...
	private lateinit var viewModel: MainActivityViewModel
    
    override fun onCreate(saveInstanceState: Bundle?){
    super.onCreate(saveInstanceState)
    ...
    viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
    }
}

 

ViewModelProvider(this)를 사용하여 새 ViewModel 인스턴스를 생성하는 경우 각 Fragment에 각각 다른 별도의 ViewModel 인스턴스가 생성된다 이로 인해 여러 Fragment가 관련되어 지다보면 데이터 중복 및 불일치가 발생.

 

 

 

 

MainActivity.kt

// ViewModel을 사용하여 데이터와 관련된 작업.

class MainActivity : AppCompatActivity()  {
    private lateinit var binding : ActivityMainBinding
    private val viewModel : MainActivityViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        
        // ViewModel을 사용하여 데이터 관련 작업
        binding.mainTvCount.text = viewModel.getCurrentCount().toString()
        binding.mainBtnPlus.setOnClickListener {
            binding.mainTvCount.text = viewModel.getUpdatedCount(Integer.parseInt(binding.mainEditInputNum.text.toString())).toString()
        }
    }
}

 

 

 

작동 화면

 

 

-2- Observer 패턴

  • LiveData는 Observer 패턴을 기반으로 작동한다. 때문에 LiveData에 대해서 공부하기 전에 Observer 패턴에 대해서 간단하게 알아보는 것이 좋을 것 같다
  • Observer Pattern이란?
    • 이벤트 발행과 구독
      • A데이터(Subject - 주체)가 변화(이벤트) 발생했을 때 이를 관찰하여 A객체의 이벤트를 구독하고 있는 객체(Observer)에게 알려 주고, 이벤트가 발생할 때마다 구독하고 있는 객체는 미리 정의해둔 어떠한 동작을 즉각 수행하게 해주는 프로그래밍 패턴이다.
      • A데이터(Subject - 주체)에 여러개의 구독하는 객체(Observer)를 등록할 수도 있다. 여러 개의 객체가 동시에 A데이터의 이벤트에 대해서 각각 다르게 정의해둔 동작을 수행할 수 있다.
      • Observer 패턴을 사용하면 특정 객체의 상태 변화를 별도의 함수 호출 없이 즉각적으로 알 수 있기 때문에 이벤트에 대한 처리를 효율적으로 처리할 수 있다.
    • 인터페이스와 추상 클래스 활용
      • 일반적으로 Subject와 구독하는 객체 사이의 의사 소통을 위해 인터페이스나 추상 클래스(abstract class)를 사용한다. Subject는 Observer가 구현해야 할 인터페이스를 정의하고, Observer들은 해당 인터페이스를 구현하여 상태 변화에 대한 업데이트 메서드를 정의한다.
  • Observer Pattern에 대해서 더 자세하게 알고 싶으면 아래 블로그에 그림과 같이 보는 게 좋을 것 같다
 

Observer

/ Design Patterns / Behavioral Patterns Observer Also known as: Event-Subscriber, Listener Intent Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object t

refactoring.guru

 

 

 

-3- LiveData

  • LiveData는 Observer Pattern을 기반으로 하는 Data의 변경을 관찰 가능한 데이터 홀더 클래스이다.
  • LiveData의 Data 관찰은 Observer가 한다. Observer는 Data가 변경되는지 관찰하고 있다가 Data가 변경이 되면 UI컨트롤러에게 알려 UI 컨트롤러가 변경된 Data를 받아 변경된 Data에 일치하는 UI를 업데이트 한다. Observer는 LiveData라는 데이터 홀더 클래스가 가지고 있는 데이터만 관찰할 수 있다. 

LiveData의 장점

  • 앱의 생명주기 인식 
    • LiveData는 앱 전체의 수명주기를 인식하여 UI 컴포넌트가 활성 상태일 때만 Observer(구독하는 객체)에게 변경된 데이터를 전달하고, 비활성화 상태일 때는 업데이트를 하지 않을 수 있다. 이러한 점 때문에 중지된 액티비티로 인한 비정상적인 종료가 되는 상황이 발생하지 않고 메모리 누수도 방지할 수 있다.
  • UI와 데이터 상태의 일치
    • LiveData는 데이터가 변경될 때 Observer에게 알림을 보내어 UI 업데이트를 처리한다.
    • Observer에서 LiveData의 변경값을 받아와 UI를 업데이트하는 로직을 구현해놓으면 LiveData 객체의 값이 변경되었을 때마다 Observer가 UI를 업데이트 하므로 개발자가 별도로 업데이트 할 필요가 없다.
  • Room, Paging3 등 다른 Jetpack 라이브러리와의 높은 호환성

 

LiveData의 단점

  • 단일쓰레드 제한
    • LiveData는 UI와 밀접하게 연관되어 있어 오직 메인쓰레드에서만 읽고 쓸 수 있다. 따라서 DataLayer에서 데이터를 처리할 때에는 사용하기 어렵다. 데이터를 I/O 할 때에는 메인쓰레드가 아닌 작업쓰레드에서 비동기 방식으로 처리되어야 하기 때문이다.
  • 데이터 유실 가능성
    • LiveData는 현재 상태만을 관리하고, 초기 상태나 과거 상태에 대한 정보를 제공하지 않는다. 따라서 앱이 백그라운드로 이동하거나, 구성 변경 등의 사건이 발생할 때 데이터가 유실될 가능성이 있다.
  • 메모리 누수 주의
    • UI 컴포넌트의 생명주기와 연결되지 않은 Observer를 등록하게 될 경우 메모리 누수가 발생될 수 있다.

 

 

ViewModel에서 LiveData 사용예제

  • TextView가 5초 마다 ("one","two","three") 의 순서로 바뀌고 Button을 누르면 현재 TextView에 있는 Text을 토스트창에 표시되도록 만들어 보자.

 

 

 

build.gradle 추가

android {

	...

	dataBinding {
        enable = true
    }
}

dependencies {
	...
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
    implementation("androidx.fragment:fragment-ktx:1.6.1")

	...
}

 

 

TestViewModel.kt

// ViewModel 생성 및 MutableLiveData 생성

class TestViewModel : ViewModel() {
    private val _textData = MutableLiveData<String>()
    val textData:LiveData<String> get() = _textData

    private val textArr = arrayOf("One","Two","Three")
    private var i = 0

    init {
        _textData.value = textArr[i]
        Timer().scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                _textData.postValue(textArr[i])
                i = (i + 1) % textArr.size
            }
        }, 0, 5000)
    }
}

 

 

 

activity_main.xml

<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewModel"
            type="com.example.livedata_with_viewmodel.TestViewModel" />
    </data>
    
    ...
    
    
</layout>
  • DataBinding을 위해서 루트(기존 layout바깥 부분)layout으로 감싸주고 루트Layout 내부에 data 태그를 넣어 ViewModel 타입의 Data로 지정해준다.

 

 

<androidx.constraintlayout.widget.ConstraintLayout

   ... >
    
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="120dp"
        android:text="@{viewModel.textData}"
        android:textSize="30dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

	...
    
</androidx.constraintlayout.widget.ConstraintLayout>
  • TextView가 ViewModel 의 LiveData의 변동에 따라 text가 바뀔 수 있게 현재 LiveData의 Data로 값이 바뀌도록 지정.

 

 

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        android:text="Show Toast"
        android:onClick="showToast"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />
  • 버튼 클릭시 MainActivity의 showToast 함수가 호출될 수 있도록 지정.

 

 

 

activity_main.xml 코드

<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewModel"
            type="com.example.livedata_with_viewmodel.TestViewModel" />
    </data>
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="120dp"
        android:text="@{viewModel.textData}"
        android:textSize="30dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        android:text="Show Toast"
        android:onClick="showToast"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

 

MainActivity.kt

// ViewModel 생성 및 DataBinding 적용 후 Toast의 메시지가 LiveData의 Value에 따라 text가 변하도록 함수 정의.

class MainActivity : AppCompatActivity() {
    private val viewModel: TestViewModel by viewModels()
    private lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this
    }

    fun showToast(view: View) {
        val currentText = viewModel.textData.value
        Toast.makeText(this,currentText,Toast.LENGTH_SHORT).show()
    }
}

 

 

 

 

 

 

 


 

[오늘 복습한 내용]

1. 복습할 시간이 없었던 것 같다.

 

 


[오류,에러 등등]

1. dataBinding으로 TextView의 Text를 설정하다가 오류가 발생

import com.example.livedata_with_viewmodel.databinding.ActivityMainBindingImpl; ^ symbol: class ActivityMainBindingImpl location: package com.example.livedata_with_viewmodel.databinding

 

android:text="@={viewModel.textData}"

@ 뒤에 = 를 잘못 넣어서 해당 오류가 발생했다. =를 빼면 해결됨

 

 


[느낀 점]

1. 어렵다

 

2. 영어로 되어있는 docs를 한글로 번역하지 않고 읽는 습관을 들여야겠다.

 

3. 궁금한 것을 잘 정리해서 모두가 잘 이해할 수 있게끔 질문을 하는 것이 쉽지 않다 꾸준히 연습해야겠

 

 


[Reference]

 

 

// ViewModel

https://velog.io/@cksgodl/LiveData-%EB%B0%8F-ViewModel

https://youngdroidstudy.tistory.com/entry/Kotlin-%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-ViewModel%EA%B3%BC-LiveData

https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko

https://todaycode.tistory.com/33

https://kbw1101.tistory.com/59