使用 RememberObserver 和 RetainObserver 編寫狀態回呼

在 Jetpack Compose 中,物件可以實作 RememberObserver,以便在與 remember 搭配使用時接收回呼,瞭解物件在組合階層中開始和停止記憶的時間。同樣地,您可以使用 RetainObserver 接收與 retain 搭配使用的物件狀態相關資訊。

對於使用組合階層中生命週期資訊的物件,我們建議採用幾項最佳做法,確認物件在平台中運作良好,並防範濫用行為。具體來說,請使用 onRemembered (或 onRetained) 回呼啟動工作,而不是建構函式;在物件停止記憶或保留時取消所有工作;避免洩漏 RememberObserverRetainObserver 的實作項目,以免意外呼叫。下一節將深入說明這些最佳化建議。

使用 RememberObserverRetainObserver 進行初始化和清除作業

「以 Compose 思考」指南說明瞭組合背後的心智模型。使用 RememberObserverRetainObserver 時,請務必記住組合的兩項行為:

  • 重組程序具有樂觀的完成率,也可能會遭到取消
  • 所有可組合函式都不得有附帶影響

onRememberedonRetained 期間執行初始化副作用,而非建構期間

系統記住或保留物件時,計算 lambda 會在組合期間執行。基於您不會在組合期間執行副作用或啟動協同程式的相同原因,您也不應在傳遞至 rememberretain 和其變化的計算 lambda 中執行副作用。包括記憶體或保留物件的建構函式。

請改為在實作 RememberObserverRetainObserver 時,確認所有效果和啟動的作業都會在 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 Studio 的可組合項預覽工具進行熱重載時,也可能發生這種情況)。

如果捨棄已記憶的物件,系統只會呼叫 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()
    }
}

確保 RememberObserverRetainObserver 實作項目為私有

撰寫公開 API 時,請務必謹慎擴充 RememberObserverRetainObserver,建立公開傳回的類別。使用者可能不會在您預期的時間記住物件,也可能以您意想不到的方式記住物件。因此,建議您不要公開實作 RememberObserverRetainObserver 的物件建構函式或工廠函式。請注意,這取決於類別的執行階段型別,而非宣告的型別。請記住,實作 RememberObserverRetainObserver 但轉換為 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
}

記憶物件時的注意事項

除了先前有關 RememberObserverRetainObserver 的建議外,我們也建議您留意並避免意外重新記憶物件,以免影響效能和正確性。以下各節將深入探討特定重新記憶情境,以及為何應避免這些情境。

只記憶物件一次

重新記憶物件可能會有危險。在最佳情況下,您可能會浪費資源記住已記住的值。但如果物件實作 RememberObserver,且意外記住兩次,就會收到超出預期的回呼。這可能會導致問題,因為 onRememberedonForgotten 邏輯會執行兩次,而大多數 RememberObserver 實作都不支援這種情況。如果第二個 remember 呼叫發生在與原始 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 物件短,因此不應保留記憶物件。建議您在原始網站保留物件,而不是記住物件。