LiveData와 Observer Pattern 기초

강민아·2024년 3월 12일
post-thumbnail

LiveData


📖 값의 변경을 감지할 수 있는 데이터 holder

LiveData is a data holder class that can be observed within a given lifecycle.

This class is designed to hold individual data fields of ViewModel, but can also be used for sharing data between different modules in your application in a decoupled fashion.

  • ViewModel과 결합할 때 시너지 효과를 얻을 수 있음.
  • LiveData를 사용하면 값의 변경을 감지해서 UI의 변화를 자동으로 반영되게 할 수 있도록 함.

⇒ 이는 MVVM에서 구현해야하는 Databinding이라고 할 수 있다.

ref) https://developer.android.com/reference/androidx/lifecycle/LiveData

Observer Pattern


📖 Subject의 상태 변화를 관찰하는 Observer들을 객체와 연결하고 Subject의 상태 변화를 초래하는 Event가 발생하면 객체가 그 Event를 직접 Observer에게 통지하는 구조의 디자인 패턴

LiveData vs Observerable


Observable

private val observableString = ObservableField<String>("Default value")
observableString.**addOnPropertyChangedCallback**(object : Observable.OnPropertyChangedCallback() {
		override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
				//Do something
		}
})
  • 콜백 등록
    • lifecycle을 알 수 없으므로 등록한 콜백이 상시 작동되어야 함
    • 작동이 필요없어지면 removeOnPropertyChangedCallback을 호출하여 콜백을 수동으로 직접 제거해야 함

LiveData

private val observableString = MutableLiveData<String>("Default value")
observableString.observe(lifecycleOwner, Observer {
		//Do something
})

LiveData의 이점


UI와 데이터 상태의 일치 보장

LiveData는 Observer패턴을 따릅니다. LiveData는 기본 데이터가 변경될 때 [Observer](https://developer.android.com/reference/androidx/lifecycle/Observer?hl=ko) 객체에 알립니다. 코드를 통합하여 이러한 Observer 객체에 UI를 업데이트할 수 있습니다. 이렇게 하면 앱 데이터가 변경될 때마다 관찰자가 대신 UI를 업데이트하므로 개발자가 업데이트할 필요가 없습니다.

메모리 누수 없음

관찰자는 [Lifecycle](https://developer.android.com/reference/androidx/lifecycle/Lifecycle?hl=ko) 객체에 결합되어 있으며 연결된 수명 주기가 끝나면 자동으로 삭제됩니다.

중지된 활동으로 인한 비정상 종료 없음

활동이 백 스택에 있을 때를 비롯하여 관찰자의 수명 주기가 비활성 상태에 있으면 관찰자는 어떤 LiveData 이벤트도 받지 않습니다.

수명 주기를 더 이상 수동으로 처리하지 않음

UI 구성요소는 관련 데이터를 관찰하기만 할 뿐 관찰을 중지하거나 다시 시작하지 않습니다. LiveData는 관찰하는 동안 관련 수명 주기 상태의 변경을 인식하므로 이 모든 것을 자동으로 관리합니다.

최신 데이터 유지

수명 주기가 비활성화되면 다시 활성화될 때 최신 데이터를 수신합니다. 예를 들어 백그라운드에 있었던 활동은 포그라운드로 돌아온 직후 최신 데이터를 받습니다.

적절한 구성 변경

기기 회전과 같은 구성 변경으로 인해 활동 또는 프래그먼트가 다시 생성되면 사용 가능한 최신 데이터를 즉시 받게 됩니다.

리소스 공유

앱에서 시스템 서비스를 공유할 수 있도록 싱글톤 패턴을 사용하는 [LiveData](https://developer.android.com/reference/androidx/lifecycle/LiveData?hl=ko) 객체를 확장하여 시스템 서비스를 래핑할 수 있습니다. LiveData 객체가 시스템 서비스에 한 번 연결되면 리소스가 필요한 모든 관찰자가 LiveData 객체를 볼 수 있습니다. 자세한 내용은 LiveData 확장을 참고하세요.

LiveData 개요  |  Android 개발자  |  Android Developers

Example


1) LiveData 사용

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val binding:ActivityMainBinding by lazy{
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        //factory
        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() 
					 */ 
					// liveData를 observing하면서 더 이상 UI를 표현하는 로직을 clickListener안에 둘 필요가 없어짐

            myViewModel.liveCounter.value = myViewModel.liveCounter.value?.plus(1)
        }
        myViewModel.liveCounter.observe(this) { counter ->
            binding.textView.text = counter.toString() //계속 observing
        }
    }
}

MyViewModelFactory.kt

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을 반환할 때 handle울 같이 반환하도록 함.
        }
        throw IllegalArgumentException("Viewmodel class not found")
    }
}

MyViewModel.kt

class MyViewModel(_counter: Int, private val savedStateHandle: SavedStateHandle) : ViewModel() {

		//LiveData 변수 생성
    var liveCounter:MutableLiveData<Int> = MutableLiveData(_counter)

	   // var counter:Int = savedStateHandle.get<Int>(SAVE_STATE_KEY)?:_counter
    //null이면 전달받은 초기값을 사용할 수 있도록 설정
    /* 
		fun saveState(){
        savedStateHandle.set(SAVE_STATE_KEY,counter)
    }

    companion object{
        private const val SAVE_STATE_KEY = "counter"
    } //key를 설정
		*/
}

[문제]

liveData는 값을 변경할 수 없는 타입이지만 데이터를 다루다보면 값을 변경해야할 일이 생긴다.

이 때, ViewModel이 데이터 원본을 조작하도록 하면 데이터와 ViewModel의 결합도가 강해지는 문제가 발생한다.

[해결]

Transformations를 사용해 LiveData의 값을 변경한다.

2) Transformations 사용

Transformations

Transformations는 단어 뜻 그 자체로 LiveData를 변형한다고 생각하면 된다. Transformations는 LiveData를 위한 클래스로써, LiveData를 사용하면서 데이터를 가공하거나, 다른 LiveData를 만들고 싶다면 Transformations을 사용하면 된다.

  • 전달받은 LiveData의 변경이 일어났을 때 Lamda 함수를 실행시키고 다시 LiveData를 반환한다.
  • 따라서 원본 데이터의 변경없이 새로운 LiveData를 만들어서 사용할 수 있다.

Method

  • map()
    LiveData<Y> map (LiveData<X> source, Function<X, Y> func)
    map()은 LiveData를 반환하며, 첫 번째 인자로 받는 LiveData에 이벤트가 생길 때마다 메인 스레드에서 두 번째 인자인 Function이 수행된다. Function의 파라미터 타입은 source로 넘겨준 LiveData의 value 타입과 같으며, 함수의 return 값은 어떠한 값이 와도 상관없다.
    val userLiveData: MutableLiveData<User> = ...
    
    val userNameLLiveData  = Transformations.map(userLiveData) { user ->
         user.firstName + " " + user.lastName
    }
  • switchMap()
    LiveData<Y> switchMap (LiveData<X> trigger,  Function<X, LiveData<Y>> func)
    switchMap()은 map()과 거의 유사하지만, 두 번째 파라미터로 넘겨준 함수의 반환 값이 LiveData라는 차이점이 있다. map()과 마찬가지로 첫 번째 파라미터로 LiveData를 넘겨준다. 넘겨준 LiveData에 이벤트가 생길 때마다 Function에서 반환하는 새로운 LiveData의 value 역시 새롭게 갱신된다. map()의 경우 람다 함수의 return 값이 각 요소의 값들을 변경시키는 것에 불과해서 자동으로 LiveData가 반환되었지만, switchMap의 경우엔 실제로 LiveData 하나를 반환해야 한다. 따라서 switchMap은 RoomDatabase와 같이 LiveData를 반환하는 기능들과 많이 사용한다.
val userIdLiveData: MutableLiveData<Int> = ...

val userLiveData: LiveData = Transformations.switchMap(userIdLiveData) { id ->
    repository.getUser(id)
}

fun setUserId(id: Int) {
    userIdLiveData.setValue(id)
}

MyViewModel.kt

class MyViewModel(_counter: Int, private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var liveCounter:MutableLiveData<Int> = MutableLiveData(_counter)
    var counter:Int = savedStateHandle.get<Int>(SAVE_STATE_KEY)?:_counter
    //null이면 전달받은 초기값을 사용할 수 있도록 설정

    val modifiedCounter : LiveData<String> = Transformations.map(liveCounter) { counter ->
        "$counter 입니다"
    }

    fun saveState(){
        savedStateHandle.set(SAVE_STATE_KEY,counter)
    }

    companion object{
        private const val SAVE_STATE_KEY = "counter"
    } //key를 설정
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val binding:ActivityMainBinding by lazy{
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        //factory
        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() //counter값을 늘릴 때마다 저장*/
            myViewModel.liveCounter.value = myViewModel.liveCounter.value?.plus(1)
        }
        myViewModel.liveCounter.observe(this) { counter ->
            binding.textView.text = counter.toString()
        }
        // 라이브데이터 값 변경
        myViewModel.modifiedCounter.observe(this) { counter ->
            binding.textView.text = counter
        }
    }
}

출처


강의

냉동코더의 알기 쉬운 Modern Android Development 입문 강의 - 인프런

출처 : 냉동코더의 알기 쉬운 Modern Android Development 입문

코드

https://github.com/cliearl/book-search-app

Designed and developed by 2022 FrozenCoder

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

썸네일 사진

UnsplashMohamed Nohassi

profile
개발자꿈나무

0개의 댓글