Tworzenie wywołań zwrotnych stanu za pomocą funkcji RememberObserver i RetainObserver

W Jetpack Compose obiekt może implementować interfejs RememberObserver, aby otrzymywać wywołania zwrotne, gdy jest używany z funkcją remember, i wiedzieć, kiedy zaczyna i kończy być zapamiętywany w hierarchii kompozycji. Podobnie możesz użyć RetainObserver, aby otrzymywać informacje o stanie obiektu używanego z retain.

W przypadku obiektów, które korzystają z tych informacji o cyklu życia z hierarchii kompozycji, zalecamy kilka sprawdzonych metod, aby upewnić się, że obiekty działają prawidłowo na platformie i chronią przed niewłaściwym użyciem. Używaj wywołań zwrotnych onRemembered (lub onRetained) do uruchamiania pracy zamiast konstruktora, anuluj całą pracę, gdy obiekty przestaną być zapamiętywane lub przechowywane, i unikaj wycieków implementacji RememberObserverRetainObserver, aby zapobiec przypadkowym wywołaniom. W następnej sekcji znajdziesz szczegółowe informacje o tych rekomendacjach.

Inicjowanie i czyszczenie za pomocą RememberObserverRetainObserver

W przewodniku po myśleniu w Compose opisujemy model myślowy związany z kompozycją. Podczas pracy z RememberObserverRetainObserver należy pamiętać o 2 sposobach działania kompozycji:

  • Ponowne komponowanie jest optymistyczne i może zostać anulowane
  • Wszystkie funkcje kompozycyjne nie powinny mieć efektów ubocznych

Uruchamiaj efekty uboczne inicjowania podczas onRemembered lub onRetained, a nie podczas tworzenia.

Gdy obiekty są zapamiętywane lub zachowywane, funkcja lambda obliczeń jest uruchamiana w ramach kompozycji. Z tych samych powodów, dla których nie wykonujesz efektów ubocznych ani nie uruchamiasz korutyny podczas kompozycji, nie powinieneś też wykonywać efektów ubocznych w wyrażeniu lambda obliczeń przekazywanym do funkcji remember, retain i ich odmian. Dotyczy to konstruktora zapamiętanych lub zachowanych obiektów.

Zamiast tego podczas implementowania RememberObserver lub RetainObserver sprawdź, czy wszystkie efekty i uruchomione zadania są wysyłane w wywołaniu zwrotnym onRemembered. Zapewnia to takie samo wyczucie czasu jak interfejsy SideEffect API. Gwarantuje też, że te efekty będą wykonywane tylko wtedy, gdy kompozycja zostanie zastosowana, co zapobiega osieroconym zadaniom i wyciekom pamięci, jeśli ponowna kompozycja zostanie porzucona lub odroczona.

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() { /* ... */ }

    // ...
}

Demontaż w przypadku zapomnienia, wycofania lub porzucenia

Aby uniknąć wycieku zasobów lub pozostawienia osieroconych zadań w tle, należy również usunąć zapamiętane obiekty. W przypadku obiektów, które implementują RememberObserver, oznacza to, że wszystko, co zostało zainicjowane w onRemembered, musi mieć odpowiednie wywołanie zwolnienia w onForgotten.

Kompozycję można anulować, więc obiekty implementujące interfejs RememberObserver muszą też po sobie posprzątać, jeśli zostaną porzucone w kompozycjach. Obiekt jest porzucany, gdy jest zwracany przez remember w kompozycji, która zostaje anulowana lub kończy się niepowodzeniem. (Dzieje się tak najczęściej w przypadku korzystania z PausableComposition, a także podczas korzystania z szybkiego przeładowania w narzędziach do podglądu funkcji kompozycyjnych w Android Studio).

Gdy zapamiętany obiekt zostanie porzucony, otrzyma tylko wywołanie metody onAbandoned (bez wywołania metody onRemembered). Aby zaimplementować metodę porzucania, usuń wszystko, co zostało utworzone między zainicjowaniem obiektu a momentem, w którym obiekt otrzymałby wywołanie zwrotne 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()
    }
}

Zachowywanie prywatności implementacji RememberObserverRetainObserver

Podczas pisania publicznych interfejsów API zachowaj ostrożność przy rozszerzaniu RememberObserverRetainObserver podczas tworzenia klas zwracanych publicznie. Użytkownik może nie pamiętać Twojego obiektu, gdy tego oczekujesz, lub może pamiętać go w inny sposób, niż zamierzasz. Dlatego zalecamy, aby nie udostępniać konstruktorów ani funkcji fabrycznych dla obiektów, które implementują interfejs RememberObserver lub RetainObserver. Pamiętaj, że zależy to od typu środowiska wykonawczego klasy, a nie od zadeklarowanego typu. Zapamiętanie obiektu, który implementuje interfejs RememberObserver lub RetainObserver, ale jest rzutowany na typ Any, nadal powoduje, że obiekt otrzymuje wywołania zwrotne.

Niezalecane:

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()

Zalecane:

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
}

Co wziąć pod uwagę podczas zapamiętywania obiektów

Oprócz poprzednich zaleceń dotyczących RememberObserverRetainObserver zalecamy też, aby pamiętać o tym, aby nie przypominać sobie przypadkowo obiektów, zarówno ze względu na wydajność, jak i poprawność. W kolejnych sekcjach znajdziesz więcej informacji o konkretnych scenariuszach ponownego zapamiętywania i o tym, dlaczego należy ich unikać.

Zapamiętywanie obiektów tylko raz

Ponowne zapamiętywanie obiektu może być niebezpieczne. W najlepszym przypadku możesz marnować zasoby na zapamiętywanie wartości, która jest już zapamiętana. Jeśli jednak obiekt implementuje interfejs RememberObserver i zostanie nieoczekiwanie zapamiętany 2 razy, otrzyma więcej wywołań zwrotnych, niż się spodziewa. Może to powodować problemy, ponieważ logika onRememberedonForgotten zostanie wykonana 2 razy, a większość implementacji RememberObserver nie obsługuje tego przypadku. Jeśli drugie wywołanie funkcji remember nastąpi w innym zakresie, który ma inny okres ważności niż pierwotny remember, wiele implementacji funkcji RememberObserver.onForgotten usunie obiekt, zanim skończy się jego używanie.

val first: RememberObserver = rememberFoo()

// Not Recommended: Re-remembered `Foo` now gets double callbacks
val second = remember { first }

Ta rada nie dotyczy obiektów, które są ponownie zapamiętywane w sposób przechodni (czyli zapamiętanych obiektów, które wykorzystują inny zapamiętany obiekt). Często pisze się kod, który wygląda tak jak poniżej. Jest to dopuszczalne, ponieważ zapamiętywany jest inny obiekt, a więc nie powoduje to nieoczekiwanego podwojenia wywołania zwrotnego.

val foo: Foo = rememberFoo()

// Acceptable:
val bar: Bar = remember { Bar(foo) }

// Recommended key usage:
val barWithKey: Bar = remember(foo) { Bar(foo) }

Załóż, że argumenty funkcji są już zapamiętane

Funkcja nie powinna zapamiętywać żadnych parametrów, ponieważ może to prowadzić do podwójnego wywoływania zwrotnego w przypadku RememberObserver i jest niepotrzebne. Jeśli parametr wejściowy musi być zapamiętany, sprawdź, czy nie implementuje on interfejsu RememberObserver, lub poproś wywołujących o zapamiętanie argumentu.

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

Nie dotyczy to obiektów zapamiętanych przechodnio. Jeśli chcesz zapamiętać obiekt pochodzący z argumentów funkcji, rozważ określenie go jako jednego z kluczy do remember:

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

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

Nie przechowuj obiektu, który jest już zapamiętany

Podobnie jak w przypadku ponownego zapamiętywania obiektu, nie należy zachowywać zapamiętanego obiektu, aby przedłużyć jego żywotność. Jest to konsekwencja porady w sekcji Okresy istnienia stanów: retain nie należy używać w przypadku obiektów, których okres istnienia nie pasuje do okresu istnienia ofert zachowania. Obiekty remembered mają krótszy okres istnienia niż obiekty retained, więc nie należy przechowywać zapamiętanego obiektu. Zamiast zapamiętywać obiekt, lepiej zachować go w witrynie źródłowej.