RememberObserver と RetainObserver を使用して状態コールバックを構成する

Jetpack Compose では、オブジェクトは RememberObserver を実装して、remember とともに使用されたときにコールバックを受け取り、コンポジション階層での記憶の開始と停止のタイミングを把握できます。同様に、RetainObserver を使用して、retain で使用されるオブジェクトの状態に関する情報を受け取ることができます。

構成階層からこのライフサイクル情報を使用するオブジェクトについては、オブジェクトがプラットフォームで適切な動作をし、誤用を防ぐことを確認するためのベスト プラクティスをいくつかおすすめします。具体的には、コンストラクタの代わりに onRemembered(または onRetained)コールバックを使用して処理を開始し、オブジェクトが記憶または保持されなくなったらすべての処理をキャンセルし、RememberObserverRetainObserver の実装が誤って呼び出されないようにリークを回避します。次のセクションでは、これらの推奨事項について詳しく説明します。

RememberObserverRetainObserver による初期化とクリーンアップ

Compose の考え方ガイドでは、コンポジションの背後にあるメンタルモデルについて説明しています。RememberObserverRetainObserver を使用する場合は、コンポジションの 2 つの動作に注意することが重要です。

  • 再コンポーズは厳密なものではなく、キャンセルされる場合がある
  • コンポーズ可能な関数はすべて、副作用がないようにする必要があります

初期化の副作用はコンストラクタではなく onRemembered または onRetained で実行する

オブジェクトが記憶または保持されると、計算ラムダがコンポジションの一部として実行されます。コンポジション中に副作用を実行したり、コルーチンを起動したりしないのと同じ理由で、rememberretain、およびそれらのバリエーションに渡される計算ラムダで副作用を実行することも避けるべきです。これには、記憶または保持されたオブジェクトのコンストラクタの一部として含まれます。

代わりに、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 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 を拡張する際に注意してください。ユーザーは、あなたが期待するタイミングでオブジェクトを思い出せないかもしれませんし、あなたが意図した方法とは異なる方法でオブジェクトを思い出すかもしれません。このため、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
}

オブジェクトを記憶する際の考慮事項

RememberObserverRetainObserver に関する前述の推奨事項に加えて、パフォーマンスと正確性の両方の観点から、オブジェクトの再記憶を誤って行わないように注意することをおすすめします。以降のセクションでは、特定の再記憶シナリオと、それを回避すべき理由について詳しく説明します。

オブジェクトを 1 回だけ記憶する

オブジェクトを再記憶することは危険な場合があります。最良のケースでは、すでに記憶されている値を記憶するためにリソースを無駄にしている可能性があります。ただし、オブジェクトが RememberObserver を実装し、予期せず 2 回記憶されると、予期したよりも多くのコールバックを受け取ります。onRememberedonForgotten のロジックが 2 回実行され、RememberObserver のほとんどの実装がこのケースをサポートしていないため、問題が発生する可能性があります。元の remember と異なるライフサイクルを持つ別のスコープで 2 回目の 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 のコールバックが 2 回呼び出される可能性があるため、また、不要であるため、パラメータを記憶すべきではありません。入力パラメータを記憶する必要がある場合は、RememberObserver を実装していないことを確認するか、呼び出し元に引数を記憶するよう要求します。

@Composable
fun MyComposable(
    parameter: Foo
) {
    // Not Recommended: Input should be remembered by the caller.
    val rememberedParameter = remember { parameter }
}

これは、推移的に記憶されたオブジェクトには適用されません。関数の引数から派生したオブジェクトを記憶する場合は、remember のキーの 1 つとして指定することを検討してください。

@Composable
fun MyComposable(
    parameter: Foo
) {
    // Acceptable:
    val derivedValue = remember { Bar(parameter) }

    // Also Acceptable:
    val derivedValueWithKey = remember(parameter) { Bar(parameter) }
}

すでに記憶されているオブジェクトを保持しない

オブジェクトの再記憶と同様に、寿命を延ばすために記憶されたオブジェクトを保持することは避けるべきです。これは、状態のライフサイクルの推奨事項の結果です。retain は、ライフサイクルが保持オファーのライフサイクルと一致しないオブジェクトでは使用しないでください。remembered オブジェクトのライフサイクルは retained オブジェクトよりも短いため、記憶されたオブジェクトを保持しないでください。代わりに、オブジェクトを記憶するのではなく、元のサイトに保持することをおすすめします。