저번 포스팅에서는 Jetpack의 LiveData 사용법과 장점등에 대해서 정리해보았습니다.
이번에는 추가적인 개념들 및 작성법에 대해서 정리해보려고 합니다.
LiveData in an app's architecture
이전 포스팅에서 다룬 바와같이, LiveData는 Activity, Fragment와 같은 엔티티의 수명주기에 따라서 인식을 합니다.
이러한 수명주기를 관리하는 주체와 ViewModel과 같은 다른 객체간에 통신을 하기위해 LiveData를 사용합니다.
ViewModel의 주요 역할은 UI 관련 데이터를 관리하고 보여주는 것이므로, LiveData 객체를 가지고 있기 좋습니다. 이러한 이유로, LiveData 객체는 UI에 상태를 보여주기 위해 ViewModel 안에서 주로 생성하여 사용합니다.
Activity와 Fragment는 LiveData 인스턴스를 가지고 있지 않아야 하는데, 왜냐하면 Activity와 Fragment는 데이터를 보여주고 출력하는 역할이지 상태를 가지고 있는 것은 아니기 때문입니다.
위의 개념을 통해 유닛 테스트를 쉽게 수행할 수 있습니다.
LiveData를 데이터 계층에 사용하려고 할 수도 있지만, LiveData는 비동기 스트림에서 데이터를 핸들링하게 설계되어있지 않습니다. (방법은 있지만, 그 방법에 단점이 있습니다.)
즉, 데이터 스트림을 결합하는 기능은 매우 제한적이고, LiveData 객체는 메인 스레드에서 수행됩니다.
예시) 저장소에 LiveData를 가지고 있는것을 메인 스레드에서 어떻게 차단하는지
class UserRepository {
// LiveData는 Repository에 있으면 안됨 > X
fun getUsers(): LiveData<List<User>> {
...
}
fun getNewPremiumUsers(): LiveData<List<User>> {
return getUsers().map { users ->
// 메인 스레드에서 만들어진 비용적으로 많은 호출이라, UI에서 버벅거림이 생길 수 있습니다.
users
.filter { user ->
user.isPremium
}
.filter { user ->
val lastSyncedTime = dao.getLastSyncedTime()
user.timeCreated > lastSyncedTime
}
}
}
앱의 다른 계층에서 데이터의 스트림을 사용하려면, Kotlin의 Flow을 사용하여 데이터를 ViewModel의 asLiveData() 함수로 LiveData로 변환하는 것을 생각해볼 수 있습니다.
Extend LiveData
LiveData는 Observer가 STARTED나 RESUMED 상태일 때, Observer가 활성 상태로 간주합니다.
예시) LiveData의 확장
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
override fun onInactive() {
stockManager.removeUpdates(listener)
}
}
코드 설명 1
- onActive() : LiveData 객체가 활성상태인 Observer를 가지고 있을 때 호출된다. 즉, 이 함수로부터 stock price를 업데이트 하는 것을 관찰하기 시작할 필요가 있다는 것입니다.
- onInactive() : LiveData 객체가 아무런 Observer를 가지고 있지 않을 때 호출된다. 모든 Observer가 수신하지 않고 있으므로, StockManager로 연결이 될 이유가 없습니다.
- setValue(T) : LiveData 인스턴스의 값을 업데이트 하고 Observer에 변화에 대해서 알립니다.
public class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val myPriceListener: LiveData<BigDecimal> = ...
myPriceListener.observe(viewLifecycleOwner, Observer<BigDecimal> { price: BigDecimal? ->
// UI 업데이트
})
}
}
코드 설명 2
- observe() 함수가 fragment 화면에 연결된 LifecycleOwner를 첫번째 인수로 전달합니다.
- 이렇게 하면, Observer와 수명주기를 가진 개체와의 연결이 완료됩니다.
- Lifecycle 객체가 활성 상태가 아니라면, 값이 변경되어도 Observer는 호출하지 않습니다.
- Lifecycle 객체가 종료된 후에 Observer는 자동으로 삭제됩니다.
LiveData가 수명주기를 인식한다는 것은 수명주기에 대해 여러개의 Activity, Fragment 및 Service 사이에서 공유할 수 있다는 것입니다.
예시) LiveData를 간단하게 사용하면서 공유하기 위한 싱글톤
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager: StockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
override fun onInactive() {
stockManager.removeUpdates(listener)
}
companion object {
private lateinit var sInstance: StockLiveData
@MainThread
fun get(symbol: String): StockLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
return sInstance
}
}
}
예시) 싱글톤으로 구현산 LiveData를 Fragment에서 사용
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
StockLiveData.get(symbol).observe(viewLifecycleOwner, Observer<BigDecimal> { price: BigDecimal? ->
// UI 업데이트
})
}
Transform LiveData
Observer에 전달하기 전, LiveData에 저장된 값을 변환할 수 있기도 하고, 다른 값을 가지는 다른 LiveData 인스턴스를 전달할 수도 있습니다.
- Transformations.map() : LiveData에 저장된 값에 적용, 변화된 값을 리턴
- Transformations.switchMap() : LiveData에 저장된 값에 적용, LiveData 타입을 리턴
ViewModel을 이용한 변환도 하나의 방법이 될 수 있습니다.
예시) 간단하게 작성한 ViewModel과 getter 함수
class MyViewModel extends ViewModel {
private final PostalCodeRepository repository;
public MyViewModel(PostalCodeRepository repository) {
this.repository = repository;
}
private LiveData<String> getPostalCode(String address) {
// 이렇게 작성하면 안됩니다.
return repository.getPostCode(address);
}
}
위의 코드로는
- getPostCode() 호출시 이전 LiveData는 해제하고, 새로운 LiveData 인스턴스를 등록해야합니다.
- 또한, UI가 재생성시에 매번 getPostCode()가 호출될 것입니다.
예시) 개선된 우편번호 조회 코드
class MyViewModel extends ViewModel {
private final PostalCodeRepository repository;
private final MutableLiveData<String> addressInput = new MutableLiveData();
public final LiveData<String> postalCode =
Transformations.switchMap(addressInput, (address) -> {
return repository.getPostCode(address);
});
public MyViewModel(PostalCodeRepository repository) {
this.repository = repository
}
private void setInput(String address) {
addressInput.setValue(address);
}
}
- postalCode는 addressInput의 변환으로 정의되었습니다.
- postalCode가 활성 Observer와 연결되어있는 한, addressInput이 변경될 때 마다, 값이 다시 계산되고 검색됩니다
Merge multiple LiveData sources
MediatorLiveData
- LiveData의 서브클래스로 여러개의 LiveData를 통합할 수 있게 허용합니다.
- MediatorLiveData 객체의 Observer는 원본 LiveData 객체가 변경될 때 트리거 됩니다
UI에 데이터베이스나 네트워크로부터 업데이트되는 LiveData를 가지고 있는 경우, 아래와 같이 추가합니다.
- 데이터를 저장한 데이터베이스와 LiveData
- 네트워크로부터 접근된 데이터와 연결된 LiveData
즉, MediatorLiveData 객체를 두가지 소스로부터 업데이트를 받을 수 있습니다.
'DEV > Android' 카테고리의 다른 글
[Android] Jetpack - Paging - 1 (0) | 2023.04.11 |
---|---|
[Android] Jetpack - Navigation 1 (0) | 2023.04.10 |
[Android] Jetpack - LiveData - 1 (0) | 2023.03.27 |
[Android] Jetpack - Lifecycle - 2 (0) | 2023.03.21 |
[Android] Jetpack - Lifecycle - 1 (0) | 2023.03.21 |