Jetpack Compose에서 객체는 RememberObserver를 구현하여 remember와 함께 사용될 때 컴포지션 계층 구조에서 기억되기 시작하고 중지되는 시점을 알 수 있는 콜백을 수신할 수 있습니다. 마찬가지로 RetainObserver를 사용하여 retain와 함께 사용되는 객체의 상태에 관한 정보를 수신할 수 있습니다.
컴포지션 계층 구조에서 이 수명 주기 정보를 사용하는 객체의 경우 객체가 플랫폼에서 좋은 시민 역할을 하고 오용을 방지하는지 확인하기 위한 몇 가지 권장사항이 있습니다. 특히 생성자 대신 onRemembered (또는 onRetained) 콜백을 사용하여 작업을 실행하고, 객체가 더 이상 기억되거나 유지되지 않으면 모든 작업을 취소하고, 실수로 호출되지 않도록 RememberObserver 및 RetainObserver 구현을 누수하지 마세요. 다음 섹션에서는 이러한 권장사항을 자세히 설명합니다.
RememberObserver 및 RetainObserver을 사용한 초기화 및 정리
Compose로 생각하기 가이드에서는 컴포지션의 멘탈 모델을 설명합니다. RememberObserver 및 RetainObserver을 사용할 때는 컴포지션의 두 가지 동작을 염두에 두어야 합니다.
- 리컴포지션은 낙관적이며 취소될 수 있습니다.
- 모든 컴포저블 함수에는 부작용이 없어야 합니다.
생성 중이 아닌 onRemembered 또는 onRetained 중에 초기화 부작용 실행
객체가 기억되거나 유지되면 계산 람다가 컴포지션의 일부로 실행됩니다. 컴포지션 중에 부작용을 실행하거나 코루틴을 실행하지 않는 것과 같은 이유로 remember, retain 및 변형에 전달된 계산 람다에서도 부작용을 실행해서는 안 됩니다.
여기에는 기억되거나 유지된 객체의 생성자 부분이 포함됩니다.
대신 RememberObserver 또는 RetainObserver를 구현할 때 모든 효과와 실행된 작업이 onRemembered 콜백에서 디스패치되는지 확인하세요.
이렇게 하면 SideEffect API와 동일한 타이밍이 제공됩니다. 또한 이러한 효과는 컴포지션이 적용될 때만 실행되므로 리컴포지션이 중단되거나 지연되는 경우 고아 작업과 메모리 누수를 방지할 수 있습니다.
class MyComposeObject : RememberObserver { private val job = Job() private val coroutineScope = CoroutineScope(Dispatchers.Main + job) init { // Not recommended: This will cause work to begin during composition instead of // with other effects. Move this into onRemembered(). coroutineScope.launch { loadData() } } override fun onRemembered() { // Recommended: Move any cancellable or effect-driven work into the onRemembered // callback. If implementing RetainObserver, this should go in onRetained. coroutineScope.launch { loadData() } } private suspend fun loadData() { /* ... */ } // ... }
잊혀지거나, 중단되거나, 버려진 경우의 철거
리소스가 누수되거나 백그라운드 작업이 분리되지 않도록 하려면 기억된 객체도 삭제해야 합니다. RememberObserver를 구현하는 객체의 경우 onRemembered에서 초기화된 모든 항목에 onForgotten의 일치하는 출시 호출이 있어야 합니다.
컴포지션은 취소될 수 있으므로 RememberObserver를 구현하는 객체는 컴포지션에서 버려진 경우 자체적으로 정리해야 합니다. 취소되거나 실패하는 컴포지션에서 remember에 의해 반환되면 객체가 폐기됩니다. (이 문제는 PausableComposition를 사용할 때 가장 흔하게 발생하며 Android 스튜디오의 컴포저블 미리보기 도구로 핫 리로드를 사용할 때도 발생할 수 있습니다.)
기억된 객체가 포기되면 onAbandoned 호출만 수신합니다 (onRemembered 호출은 수신하지 않음). 포기 메서드를 구현하려면 객체가 초기화된 시점과 객체가 onRemembered 콜백을 수신했을 시점 사이에 생성된 항목을 삭제하세요.
class MyComposeObject : RememberObserver { private val job = Job() private val coroutineScope = CoroutineScope(Dispatchers.Main + job) // ... override fun onForgotten() { // Cancel work launched from onRemembered. If implementing RetainObserver, onRetired // should cancel work launched from onRetained. job.cancel() } override fun onAbandoned() { // If any work was launched by the constructor as part of remembering the object, // you must cancel that work in this callback. For work done as part of the construction // during retain, this code should will appear in onUnused. job.cancel() } }
RememberObserver 및 RetainObserver 구현을 비공개로 유지
공개 API를 작성할 때는 공개적으로 반환되는 클래스를 만들 때 RememberObserver 및 RetainObserver를 확장할 때 주의해야 합니다. 사용자가 예상한 시점에 객체를 기억하지 못하거나 의도한 것과 다른 방식으로 객체를 기억할 수 있습니다. 따라서 RememberObserver 또는 RetainObserver를 구현하는 객체의 생성자나 팩토리 함수를 노출하지 않는 것이 좋습니다. 이는 선언된 유형이 아닌 클래스의 런타임 유형에 따라 달라집니다. RememberObserver 또는 RetainObserver를 구현하지만 Any로 캐스팅되는 객체를 기억하면 객체가 콜백을 수신하게 됩니다.
권장하지 않음:
abstract class MyManager
// Not Recommended: Exposing a public constructor (even implicitly) for an object implementing
// RememberObserver can cause unexpected invocations if it is remembered multiple times.
class MyComposeManager : MyManager(), RememberObserver { ... }
// Not Recommended: The return type may be an implementation of RememberObserver and should be
// remembered explicitly.
fun createFoo(): MyManager = MyComposeManager()
권장:
abstract class MyManager class MyComposeManager : MyManager() { // Callers that construct this object must manually call initialize and teardown fun initialize() { /*...*/ } fun teardown() { /*...*/ } } @Composable fun rememberMyManager(): MyManager { // Protect the RememberObserver implementation by never exposing it outside the library return remember { object : RememberObserver { val manager = MyComposeManager() override fun onRemembered() = manager.initialize() override fun onForgotten() = manager.teardown() override fun onAbandoned() { /* Nothing to do if manager hasn't initialized */ } } }.manager }
객체를 기억할 때 고려사항
RememberObserver 및 RetainObserver과 관련된 이전 권장사항 외에도 성능과 정확성을 위해 객체를 실수로 다시 기억하지 않도록 주의하는 것이 좋습니다. 다음 섹션에서는 특정 다시 기억 시나리오와 이러한 시나리오를 피해야 하는 이유를 자세히 설명합니다.
객체를 한 번만 기억
객체를 다시 기억하는 것은 위험할 수 있습니다. 최적의 시나리오에서는 이미 기억된 값을 기억하는 데 리소스를 낭비할 수 있습니다. 하지만 객체가 RememberObserver를 구현하고 예상치 않게 두 번 기억되면 예상보다 많은 콜백을 수신하게 됩니다. onRemembered 및 onForgotten 로직이 두 번 실행되고 대부분의 RememberObserver 구현이 이 사례를 지원하지 않으므로 문제가 발생할 수 있습니다. 두 번째 기억 호출이 원래 remember와 수명이 다른 범위에서 발생하면 RememberObserver.onForgotten의 많은 구현에서 객체가 사용이 완료되기 전에 객체를 삭제합니다.
val first: RememberObserver = rememberFoo()
// Not Recommended: Re-remembered `Foo` now gets double callbacks
val second = remember { first }
이 조언은 전이적으로 다시 기억되는 객체 (예: 다른 기억된 객체를 소비하는 기억된 객체)에는 적용되지 않습니다. 다음과 같은 코드를 작성하는 것이 일반적입니다. 다른 객체가 기억되므로 허용되며 예기치 않은 콜백 더블링이 발생하지 않습니다.
val foo: Foo = rememberFoo() // Acceptable: val bar: Bar = remember { Bar(foo) } // Recommended key usage: val barWithKey: Bar = remember(foo) { Bar(foo) }
함수 인수가 이미 기억되었다고 가정
함수는 RememberObserver의 콜백 호출이 두 번 발생할 수 있고 불필요하기 때문에 매개변수를 기억해서는 안 됩니다. 입력 매개변수를 기억해야 하는 경우 RememberObserver를 구현하지 않는지 확인하거나 호출자가 인수를 기억하도록 요구하세요.
@Composable
fun MyComposable(
parameter: Foo
) {
// Not Recommended: Input should be remembered by the caller.
val rememberedParameter = remember { parameter }
}
전이적으로 기억된 객체에는 적용되지 않습니다. 함수의 인수에서 파생된 객체를 기억할 때는 remember의 키 중 하나로 지정하는 것이 좋습니다.
@Composable fun MyComposable( parameter: Foo ) { // Acceptable: val derivedValue = remember { Bar(parameter) } // Also Acceptable: val derivedValueWithKey = remember(parameter) { Bar(parameter) } }
이미 기억된 객체는 보관하지 않음
객체를 다시 기억하는 것과 마찬가지로 수명을 연장하기 위해 기억된 객체를 유지해서는 안 됩니다. 이는 상태 수명의 조언에 따른 결과입니다. retain는 수명이 유지 제안의 수명과 일치하지 않는 객체와 함께 사용하면 안 됩니다. remembered 객체는 retained 객체보다 수명이 짧으므로 기억된 객체를 유지해서는 안 됩니다. 대신 객체를 기억하는 대신 원본 사이트에 유지하는 것이 좋습니다.