일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- material3
- designPattern
- 빌터패턴
- 프로토타입 패턴
- F
- 추상 팩토리
- ㅋㅁ
- Design Pattern
- compose
- 함수형프로그래밍
- Functional Programming
- 디자인패턴 #
- 코틀린
- Abstract Factory
- Singleton
- 디자인패턴
- Kotlin
- builderPattern
- 팩토리 메소드
- android designsystem
- 안드로이드 디자인시스템
- factory method
- 옵저버 패턴
- 추상팩토리패턴
- Observer Pattern
- ㅓ
- PrototypePattern
- El
- r
- 싱글톤
- Today
- Total
오늘도 더 나은 코드를 작성하였습니까?
A safer way to collect flows from Android UIs 본문
Android 앱에서 Kotlin flow는 일반적으로 UI 계층에서 collect되어 화면에 데이터 업데이트를 표시합니다
하지만 flow을 collect하여 필요한 것보다 더 많은 작업을 수행하고, View가 Background로 이동할 때 리소스(CPU 및 메모리 모두)를 낭비하거나 데이터가 누출되지 않도록 해야됩니다.
Lifecycle.repeatOnLifecycle 및 Flow.flowWithLifecycle API가 리소스 낭비로부터 사용자를 보호하는 방법과 UI 계층에서 flow 수집에 사용하기에 좋은 기본값인 이유를 배우게 됩니다.
Wasting resources
Flow Producer 구현 세부 사항에 관계없이 앱 계층의 하위 계층에서 Flow<T> API를 노출하는 것이 좋습니다.
그리고 안전하게 수집해야 합니다.
Activity가 background로 이동할 때 코루틴을 시작한 작업을 수동으로 취소하지 않는 다면,
buffer, conflate, flowOn 또는 shareIn과 같은 버퍼가 있는 연산자를 사용하는 flow나 channel에서 지원하는 cold flow는 CoroutineScope.launch, F low<T>.launchIn 또는 LifecycleCoroutineScope.launchWhenX와 같은 일부 기존 API로 수집하는 것이 안전하지 않습니다.
이러한 API는 background에서 buffer로 항목을 내보내는 동안 기본 flow 생성자를 inActive 상태로 유지하여 리소스를 낭비합니다.
callbackFlow
// Implementation of a cold flow backed by a Channel that sends Location updates
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// clean up when Flow collection ends
awaitClose {
removeLocationUpdates(callback)
}
}
참고: 내부적으로 callbackFlow는 blocking queue과 개념적으로 매우 유사한 channel을 사용하며 기본 용량은 64개 요소입니다.
앞서 언급한 API 중 하나를 사용하여 UI 레이어에서 이 flow을 collecting하면 View가 UI에 flow을 표시하지 않더라도 flow 방출 위치가 유지됩니다!
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Collects from the flow when the View is at least STARTED and
// SUSPENDS the collection when the lifecycle is STOPPED.
// Collecting the flow cancels when the View is DESTROYED.
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
// Same issue with:
// - lifecycleScope.launch { /* Collect from locationFlow() here */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
lifecycleScope.launchWhenStarted는 코루틴 실행을 suspend합니다
새 location 정보는는 처리되지 않지만, callbackFlow 생산자는 그럼에도 불구하고 위치를 계속 보냅니다.
lifecycleScope.launch 또는 launchIn API를 호출하면 View가 백그라운드에 있더라도 location를 계속 처리 까지 하기 때문에 훨씬 더 위험합니다! 잠재적으로 앱이 다운될 수 있습니다.
API로 이 문제를 해결하려면 callbackFlow를 취소하고 위치 제공자가 항목을 내보내고 리소스를 낭비하지 않도록 View가 백그라운드로 이동할 때 수동으로 수집을 취소해야 합니다.
class LocationActivity : AppCompatActivity() {
// Coroutine listening for Locations
private var locationUpdatesJob: Job? = null
override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
override fun onStop() {
// Stop collecting when the View goes to the background
locationUpdatesJob?.cancel()
super.onStop()
}
}
좋은 해결책이지만 상용구가 너무 많습니다., 친구들! 그리고 Android 개발자에 대한 보편적인 진실이 있다면 우리는 상용구 코드 작성을 절대적으로 싫어합니다. 상용구 코드를 작성하지 않아도 되는 가장 큰 이점 중 하나는 코드가 적을수록 실수할 가능성이 적다는 것입니다!
Lifecycle.repeatOnLifecycle
이우리 모두가 같은 상황에 있고, 무엇이 문제인지 알고 있으므로 해결책을 제시할 때입니다.
솔루션은
1) 간단해야 하고,
2) 친숙하거나 기억/이해하기 쉽고,
더 중요하게는 3) 안전해야 합니다!
Flow 구현 세부 정보에 관계없이 모든 사용 사례에서 작동해야 합니다
더 이상 고민하지 않고 사용해야 하는 API는 lifecycle-runtime-ktx 라이브러리에서 사용할 수 있는 Lifecycle.repeatOnLifecycle
이다.
아래 코드를 살펴보자.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// repeatOnLifecycle이 suspend 함수이며 새 코루틴을 생성합니다.
lifecycleScope.launch {
// repeatOnLifecycle에 전달된 블록은 수명 주기가 실행될 때 실행됩니다.
// 최소한 STARTED이고 수명 주기가 STOPPED일 때 취소됩니다.
// 수명 주기가 다시 시작되면 자동으로 블록을 다시 시작합니다.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 수명 주기가 STARTED일 때 locationFlow에서 안전하게 수집
// 수명 주기가 STOPPED일 때 수집을 중지합니다.
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
repeatOnLifecycle은 Lifecycle.State를 매개변수로 받는 suspend function 입니다.
수명 주기가 해당 state(매개변수로 전달된)에 도달할 때 블록이 전달된 새 코루틴을 자동으로 생성 및 시작하고,
수명 주기가 상태 아래로 떨어질 때 블록을 실행하는 진행 중인 코루틴을 취소하는 데 사용되는 매개변수입니다.
더 이상 필요하지 않을 때 코루틴을 취소하는 관련 코드가 repeatOnLifecycle에 의해 자동으로 수행되기 때문에 상용구 코드를 피할 수 있습니다. 예상할 수 있듯이 예기치 않은 동작을 피하기 위해 Activity의 onCreate 또는 Fragment의 onViewCreated 메서드에서 이 API를 호출하는 것이 좋습니다.
프래그먼트를 사용하는 아래 예를 참조하십시오.
class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
!중요
Fragment 항상 viewLifecycleOwner를 사용하여 UI 업데이트를 트리거해야 합니다.
그러나 때때로 View가 없을 수 있는 DialogFragments의 경우는 그렇지 않습니다.
DialogFragments의 경우 lifecycleOwner를 사용할 수 있습니다.
Under the hood!
repeatOnLifecycle은 자신을 호출한 코루틴을 suspend하고, 수명 주기가 새 코루틴에서 매개변수 State 안팎으로 이동할 때 블록을 다시 시작하고, 수명 주기가 파괴되면 자신을 호출 코루틴을 재개합니다.
마지막 점은 매우 중요합니다. repeatOnLifecycle을 호출한 코루틴은 수명 주기가 소멸될 때까지 실행을 재개하지 않습니다.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a coroutine
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
// Repeat when the lifecycle is RESUMED, cancel when PAUSED
}
// `lifecycle` is DESTROYED when the coroutine resumes. repeatOnLifecycle
// suspends the execution of the coroutine until the lifecycle is DESTROYED.
}
}
}
Visual diagram
처음으로 돌아가서 lifecycleScope.launch로 시작된 코루틴에서 직접 locationFlow를 수집하는 것은 View가 백그라운드에 있을 때에도 수집이 계속 발생하기 때문에 위험했습니다.
repeatOnLifecycle은 수명 주기가 대상 상태 안팎으로 이동할 때 Flow 수집을 중지하고 다시 시작하기 때문에 리소스 낭비와 앱 충돌을 방지합니다.
Flow.flowWithLifecycle
수집할 flow이 하나만 있는 경우 Flow.flowWithLifecycle 연산자를 사용할 수도 있습니다.
이 API는 내부에서 repeatOnLifecycle API를 사용하며, Lifecycle이 target state 안/팎으로 이동할 때 항목을 내보내고 기본 생산자를 취소합니다.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Listen to one flow in a lifecycle-aware manner using flowWithLifecycle
lifecycleScope.launch {
locationProvider.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {
// New location! Update the map
}
}
// Listen to multiple flows
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// As collect is a suspend function, if you want to collect
// multiple flows in parallel, you need to do so in
// different coroutines
launch {
flow1.collect { /* Do something */ }
}
launch {
flow2.collect { /* Do something */ }
}
}
}
}
}
참고: 이 API 이름은 Flow.flowWithLifecycle이 upStream flow을 수집하는 데 사용되는 CoroutineContext를 변경하고,
down Stream은 영향을 받지 않은 채로 두기 때문에 Flow.flowOn(CoroutineContext) 연산자를 선례로 사용합니다.
또한 flowOn과 유사하게 Flow.flowWithLifecycle은 소비자가 생산자를 따라가지 못하는 경우를 대비하여 버퍼를 추가합니다. 이는 구현에서 callbackFlow를 사용하기 때문입니다.
Configuring the underlying producer
이러한 API를 사용하더라도 아무도 수집하지 않더라도 리소스를 낭비할 수 있는 핫 플로우에 주의하십시오!
유효한 사용 사례가 몇 가지 있지만 이를 염두에 두고 필요한 경우 문서화하고 주의하여 사용한다.
리소스를 낭비하더라도 백그라운드에서 기본 흐름 생산자를 활성화하면 일부 사용 사례에 유용할 수 있습니다.
즉, 오래된 데이터를 따라잡고 일시적으로 표시하는 대신 즉시 새로운 데이터를 사용할 수 있습니다.
사용 사례에 따라 생산자가 항상 inActive되어야 하는지 여부를 결정합니다.
MutableStateFlow 및 MutableSharedFlow API는 subscriptionCount가 0일 때 기본 생산자를 중지하는 데 사용할 수 있는 subscriptionCount 필드를 제공합니다.
기본적으로 flow 인스턴스를 보유하는 객체가 메모리에 있는 한 생산자를 InActive 상태로 유지합니다.
예를 들어 StateFlow를 사용하여 ViewModel에서 UI로 노출되는 UiState와 같은 몇 가지 유효한 사용 사례가 있습니다.
이런 경우에는 ViewModel이 항상 최신 UI 상태를 View에 제공해야 합니다.
마찬가지로 Flow.stateIn 및 Flow.shareIn 연산자는 공유 시작 정책으로 구성할 수 있습니다.
inActive Observer가 없을 때 WhileSubscribed()는 기본 생산자를 중지합니다!
반대로 Eagerly 또는 Lazily는 사용하는 CoroutineScope가 inActive인 한 기본 생산자를 inActive 상태로 유지합니다.
참고: 이 문서에 표시된 API는 UI에서 flow을 수집하기 위한 좋은 기본값이며 flow 구현 세부 정보와 관계없이 사용해야 합니다.
이러한 API는 필요한 작업을 수행합니다.
UI가 화면에 표시되지 않으면 수집을 중지합니다.
항상 활성화되어야 하는지 아닌지는 flow구현에 달려 있습니다.
Comparison with LiveData
위에서 소개한 API가 LiveData와 유사하게 동작한다는 것을 눈치채셨을 것입니다. 사실입니다!
LiveData는 수명 주기를 인식하고 restarting동작으로 인해 UI에서 데이터 스트림을 관찰하는 데 이상적입니다.
Lifecycle.repeatOnLifecycle 및 Flow.flowWithLifecycle API도 동일합니다.
restarting 동작으로 인해 UI에서 데이터 스트림을 관찰하는 데 이상적입니다
이러한 API를 사용하여 Flow을 수집하는 것은 Kotlin으로 만 작성된 앱에서 LiveData를 자연스럽게 대체할 수있습니다.
flow collect하는데 있어 API(Lifecycle.repeatOnLifecycle 및 Flow.flowWithLifecycle )를 사용하는 경우에 비하여,
LiveData는 코루틴 및 flow에 비해 이점을 제공하지 않습니다.
모든 Dispatcher에서 수집할 수 있고 많은 Flow 연산자를 활용할 수 있어서 flow이 더 유연합니다.
사용 가능한 연산자가 제한적이고 값이 항상 UI 스레드에서 관찰되는 LiveData와 대조됩니다.
StateFlow support in data binding
다른 참고로, LiveData를 사용하는 이유 중 하나는 데이터 바인딩에서 지원되기 때문입니다. StateFlow도 마찬가지입니다! 데이터 바인딩의 StateFlow 지원에 대한 자세한 내용은 공식 문서를 확인하세요.