BuNa_
IT Story
BuNa_
전체 방문자
오늘
어제
  • 분류 전체보기 (117)
    • CS (14)
      • 운영체제 (8)
      • 네트워크 (0)
      • Design Pattern (1)
      • OOP (4)
    • 대외활동 (24)
      • 우아한테크코스 (14)
      • DND 동아리 (4)
      • UMC 동아리 (5)
      • 해커톤 (1)
    • Android (29)
      • MVVM (2)
      • 스터디 (11)
      • Compose (3)
      • Unit Test (1)
    • Project (5)
      • 어따세워 (5)
      • DnD 과외 서비스 (0)
    • Programming (11)
      • Kotlin (4)
      • 파이썬 (7)
    • Git (1)
    • 인공지능 (22)
    • 백준 (8)
    • 기타 (3)
      • IntelliJ (1)
      • 일상 (0)

블로그 메뉴

  • 홈

공지사항

인기 글

태그

  • 다이나믹 프로그래밍
  • Compose
  • UMC
  • k-means++
  • 인공지능
  • Android
  • 셀레니움
  • K-means
  • 운영체제
  • 인공지능 분류
  • 우아한테크코스
  • 우테코
  • 우테코 5기
  • 딥러닝
  • 안드로이드
  • 파이썬
  • 어따세워
  • 우테코 프리코스
  • Ai
  • External fragmentation
  • 컴공선배
  • 원시값 포장
  • RecyclerView
  • 객체지향 생활체조
  • 선형회귀
  • 외부 단편화
  • Baekjoon
  • 백준
  • ViewModel
  • MVVM

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
BuNa_

IT Story

[Android] ViewModel + Event Wrapper Pattern 단일 이벤트 처리
Android

[Android] ViewModel + Event Wrapper Pattern 단일 이벤트 처리

2022. 12. 12. 00:36

💬   Single Event 처리

ViewModel의 LiveData를 Observe하고 있으며 값이 True로 변경되면 Toast 메시지를 출력합니다.

그러나, 만약, 디바이스 화면을 회전(Configuration)하였더니 Toast가 한 번 더 호출되는 이슈가 발생합니다

이는 Configuration이 발생하면, 기존 Activity가 Destroy되고 화면이 새롭게 그려지면서 onCreate() 콜백 메서드가 다시금 호출되기 때문입니다.

ViewModel은 일반적으로 Activity보다 생명주기가 길기 때문에, Activity가 파괴된다고 하더라도 ViewModel 내부의 필드는 그대로 유지됩니다.

그렇기 때문에 LiveData가 감싸고 있는 데이터는 True 상태를 유지하고 있으며, Activity에서는 화면이 다시 그려지면서 LiveData를 다시 Observe()하여 Observer의 상태가 inactive -> active로 변경되면서 Toast를 출력하게 되는 것입니다.

 

이에 대해 Google에서 제안하는 해결책이 있습니다.

 

💡  Event Wrapper 

open class Event<out T>(private val content: T) {
    var hasBeenHandled = false
        private set

    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    fun peekContent(): T = content
}

class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
    override fun onChanged(event: Event<T>?) {
        event?.getContentIfNotHandled()?.let { value ->
            onEventUnhandledContent(value)
        }
    }
}

 

Event클래스는 Generic 타입의 content를 입력받습니다.

그리고 만약 해당 content를 getContentIfNotHandled() 라는 메서드를 통해 접근할 수 있습니다.

여기서 핵심은 hasBeenHandled라는 필드를 두어 만약 한 번 접근한 적이 있다면 null을 반환하여 두 번 이상 다루지 않도록 구현할 수 있습니다.

 

반면, EventObserver클래스는 Observer를 상속받고 있습니다.

그리고 값이 변경되었을 때 getContentIfNotHandled() 메서드를 통해 접근하는데, 여기서 위에 설명한 바와 같이 이미 한 번 접근한 적이 있다면 null이 반환되므로 event가 실행되지 않도록 구현되어 있습니다.

 

이 2가지 클래스를 적절히 활용하여 불필요하게 두 번 이상 event가 호출되는 상황을 방지할 수 있습니다.

 

 

아래 예시 코드를 통해 활용을 살펴보겠습니다.

 

class MainViewModel : ViewModel() {
    private val _error = MutableLiveData<Event<Boolean>>()
    val error: LiveData<Event<Boolean>> = _error

    fun invokeError() {
        _error.value = Event(true)
    }
}

 

backingField를 사용하는 등 기존에 ViewModel에서 LiveData를 사용하는 방법을 동일합니다.

하지만, 조금 다른 점은 위에서 제공하는 Event클래스로 Boolean을 한 번 더 감싸고 있다는 점입니다.

만약 invokeError() 메서드를 호출하면 Event(true)로 값이 업데이트되면서 Observer에 이벤트 변경 감지가 일어날 것입니다.

 

 

class MainActivity : AppCompatActivity() {
    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val errorButton = findViewById<Button>(R.id.error_invoke_button)
        errorButton.setOnClickListener {
            mainViewModel.invokeError()
        }

        mainViewModel.error.observe(this, EventObserver { isOccurredError ->
            if (isOccurredError) {
                Toast.makeText(this, "에러가 발생했습니다.", Toast.LENGTH_SHORT).show()
            }
        })
    }
}

 

반면, MainActivity는 ViewModel의 error를 관찰하고 있습니다.

이 또한, 기존에 사용하던 Observer 대신에 새롭게 만들어준 EventObserver를 사용하고 있다는 점 외에는 달라진 부분이 없습니다.

하지만 이런 간단한 방법만으로 불필요하게 변경이 두 번 감지되는 상황을 방지할 수 있습니다.

 

'Configuration이 발생해도 Toast가 두 번 발생하지 않는다.'

 

⭐  결론

  • ViewModel + Event Wrapepr 패턴을 활용하여 변경이 한 번만 감지되어야 하는 상황을 구현할 수 있다.

 

 

 

github : https://github.com/tmdgh1592

 

tmdgh1592 - Overview

느리더라도 천천히..!! 😁. tmdgh1592 has 21 repositories available. Follow their code on GitHub.

github.com

 

 

저작자표시 비영리 변경금지 (새창열림)

'Android' 카테고리의 다른 글

[Android] Listview vs RecyclerView  (1) 2023.04.20
[Android] This version of the Android Support plugin for IntelliJ IDEA (or Android Studio) 오류 해결  (2) 2022.12.23
[Android] Firebase Dynamic Link를 활용하여 사용자 초대링크 생성하기  (2) 2022.02.08
[Android] 자바 코틀린 (Pattern, Matcher)정규식을 사용하여 패스워드 조건을 만들어보자  (0) 2022.02.05
[Android][어따세워] 음성인식으로 주차장을 검색해보자! STT(Speech To Text) SpeechRecognizer  (2) 2021.12.11
    'Android' 카테고리의 다른 글
    • [Android] Listview vs RecyclerView
    • [Android] This version of the Android Support plugin for IntelliJ IDEA (or Android Studio) 오류 해결
    • [Android] Firebase Dynamic Link를 활용하여 사용자 초대링크 생성하기
    • [Android] 자바 코틀린 (Pattern, Matcher)정규식을 사용하여 패스워드 조건을 만들어보자
    BuNa_
    BuNa_
    안드로이드 개발자를 향해 달리고 있는 공대생입니다! 🧑 Android, Kotlin, Java, Python 등 학습하고 있는 내용과 프로젝트를 주로 업로드하고 있습니다. 지적과 조언은 언제나 환영입니다!😊 github : https://github.com/tmdgh1592

    티스토리툴바