Как создавать обратные вызовы состояния с помощью RememberObserver и RetainObserver

В Jetpack Compose объект может реализовать интерфейс RememberObserver, чтобы получать обратные вызовы при использовании с remember и узнавать, когда он начинает и перестает запоминаться в иерархии композиции. Аналогичным образом можно использовать RetainObserver, чтобы получать информацию о состоянии объекта, используемого с retain.

Для объектов, которые используют информацию о жизненном цикле из иерархии композиции, мы рекомендуем несколько способов убедиться, что объекты корректно работают на платформе и защищены от неправильного использования. В частности, используйте обратные вызовы onRemembered (или onRetained) для запуска задач вместо конструктора, отменяйте все задачи, когда объекты перестают запоминаться или сохраняться, и избегайте утечек реализаций RememberObserver и RetainObserver, чтобы предотвратить случайные вызовы. В следующем разделе эти рекомендации описаны более подробно.

Инициализация и очистка с помощью RememberObserver и RetainObserver

В руководстве по Compose описана ментальная модель, лежащая в основе композиции. При работе с RememberObserver и RetainObserver важно помнить о двух особенностях композиции:

  • Рекомпозиция выполняется с оптимизмом и может быть отменена
  • У всех композируемых функций не должно быть побочных эффектов

Выполняйте побочные эффекты инициализации во время onRemembered или onRetained, а не во время создания.

Когда объекты запоминаются или сохраняются, лямбда-функция расчета выполняется как часть композиции. По тем же причинам, по которым не следует выполнять побочные эффекты или запускать сопрограмму во время композиции, не следует выполнять побочные эффекты в лямбда-выражении, переданном в remember, retain и их варианты. в том числе в конструкторе для запомненных или сохраненных объектов.

Вместо этого при реализации RememberObserver или RetainObserver убедитесь, что все эффекты и запущенные задания отправляются в обратном вызове onRemembered. Это соответствует времени, которое требуется для работы API SideEffect. Кроме того, он гарантирует, что эти эффекты будут выполняться только при применении композиции, что предотвращает появление потерянных заданий и утечек памяти, если повторная композиция будет отменена или отложена.

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, должно иметь соответствующий вызов release в onForgotten.

Поскольку композицию можно отменить, объекты, реализующие RememberObserver, должны также очищать за собой, если они были оставлены в композициях. Объект считается оставленным, если он возвращается функцией remember в композиции, которая отменяется или завершается с ошибкой. (Чаще всего это происходит при использовании PausableComposition, а также при горячей перезагрузке с помощью инструментов предварительного просмотра composable-функций Android Studio.)

Когда сохраненный объект удаляется, он получает только вызов метода onAbandoned (без вызова метода onRemembered). Чтобы реализовать метод abandon, удалите все, что было создано в период между инициализацией объекта и моментом, когда объект должен был получить обратный вызов 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 происходит в другой области действия с другим сроком жизни, чем у исходного объекта 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, не следует сохранять запомненный объект. Вместо этого лучше сохранить объект на исходном сайте, а не запоминать его.