Callback status Compose dengan RememberObserver dan RetainObserver

Di Jetpack Compose, objek dapat mengimplementasikan RememberObserver untuk menerima callback saat digunakan dengan remember untuk mengetahui kapan objek mulai dan berhenti diingat dalam hierarki komposisi. Demikian pula, Anda dapat menggunakan RetainObserver untuk menerima informasi tentang status objek yang digunakan dengan retain.

Untuk objek yang menggunakan informasi siklus proses ini dari hierarki komposisi, sebaiknya ikuti beberapa praktik terbaik untuk memverifikasi bahwa objek Anda bertindak sebagai warga yang baik di platform dan melindungi dari penyalahgunaan. Secara khusus, gunakan callback onRemembered (atau onRetained) untuk meluncurkan pekerjaan, bukan konstruktor, batalkan semua pekerjaan saat objek berhenti diingat atau dipertahankan, dan hindari kebocoran implementasi RememberObserver dan RetainObserver untuk menghindari panggilan yang tidak disengaja. Bagian berikutnya menjelaskan rekomendasi ini secara lebih mendalam.

Inisialisasi dan pembersihan dengan RememberObserver dan RetainObserver

Panduan Berpikir dalam Compose menjelaskan model mental di balik komposisi. Saat menggunakan RememberObserver dan RetainObserver, Anda harus mengingat dua perilaku komposisi:

  • Rekomposisi bersifat optimistis dan dapat dibatalkan
  • Semua fungsi composable tidak boleh memiliki efek samping

Jalankan efek samping inisialisasi selama onRemembered atau onRetained, bukan konstruksi

Saat objek diingat atau dipertahankan, lambda penghitungan berjalan sebagai bagian dari komposisi. Karena alasan yang sama dengan Anda tidak akan melakukan efek samping atau meluncurkan coroutine selama komposisi, Anda juga tidak boleh melakukan efek samping dalam lambda perhitungan yang diteruskan ke remember, retain, dan variasinya. Ini termasuk sebagai bagian dari konstruktor untuk objek yang diingat atau dipertahankan.

Sebagai gantinya, saat menerapkan RememberObserver atau RetainObserver, pastikan semua efek dan tugas yang diluncurkan dikirim dalam callback onRemembered. Hal ini menawarkan pengaturan waktu yang sama dengan API SideEffect. Hal ini juga menjamin bahwa efek ini hanya dijalankan saat komposisi diterapkan, yang mencegah tugas yang tidak terkait dan kebocoran memori jika rekomposisi dibatalkan atau ditangguhkan.

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

    // ...
}

Pembongkaran saat dilupakan, dihentikan, atau ditinggalkan

Untuk menghindari kebocoran resource atau membiarkan pekerjaan latar belakang tidak terikat, objek yang diingat juga harus dibuang. Untuk objek yang mengimplementasikan RememberObserver, ini berarti bahwa apa pun yang diinisialisasi di onRemembered harus memiliki panggilan pelepasan yang cocok di onForgotten.

Karena komposisi dapat dibatalkan, objek yang menerapkan RememberObserver juga harus membersihkan diri sendiri jika ditinggalkan dalam komposisi. Objek ditinggalkan saat ditampilkan oleh remember dalam komposisi yang dibatalkan atau gagal. (Hal ini paling sering terjadi saat menggunakan PausableComposition, dan juga dapat terjadi saat menggunakan hot reload dengan alat pratinjau composable Android Studio.)

Saat objek yang diingat ditinggalkan, objek tersebut hanya menerima panggilan ke onAbandoned (dan tidak ada panggilan ke onRemembered). Untuk menerapkan metode pelepasan, buang semua yang dibuat antara saat objek diinisialisasi dan saat objek akan menerima callback 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()
    }
}

Menjaga kerahasiaan implementasi RememberObserver dan RetainObserver

Saat menulis API publik, berhati-hatilah saat memperluas RememberObserver dan RetainObserver dalam membuat class yang ditampilkan secara publik. Pengguna mungkin tidak mengingat objek Anda saat Anda mengharapkan mereka mengingatnya, atau mungkin mengingat objek Anda dengan cara yang berbeda dari yang Anda maksudkan. Oleh karena itu, sebaiknya jangan mengekspos konstruktor atau fungsi factory untuk objek yang menerapkan RememberObserver atau RetainObserver. Perhatikan bahwa hal ini bergantung pada jenis runtime class, bukan jenis yang dideklarasikan — mengingat objek yang mengimplementasikan RememberObserver atau RetainObserver, tetapi di-casting ke Any, masih menyebabkan objek menerima callback.

Tidak direkomendasikan:

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

Direkomendasikan:

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
}

Pertimbangan saat mengingat objek

Selain rekomendasi sebelumnya terkait RememberObserver dan RetainObserver, sebaiknya perhatikan dan hindari mengingat ulang objek secara tidak sengaja baik untuk performa maupun kebenaran. Bagian berikut membahas lebih mendalam skenario mengingat kembali yang spesifik dan alasan mengapa skenario tersebut harus dihindari.

Hanya mengingat objek sekali

Mengingat kembali objek dapat berbahaya. Dalam skenario terbaik, Anda mungkin membuang-buang resource untuk mengingat nilai yang sudah diingat. Namun, jika objek mengimplementasikan RememberObserver dan diingat dua kali secara tidak terduga, objek tersebut akan menerima lebih banyak callback daripada yang diharapkan. Hal ini dapat menyebabkan masalah, karena logika onRemembered dan onForgotten akan dieksekusi dua kali, dan sebagian besar penerapan RememberObserver tidak mendukung kasus ini. Jika panggilan remember kedua terjadi dalam cakupan berbeda yang memiliki masa aktif berbeda dari remember asli, banyak penerapan RememberObserver.onForgotten menghapus objek sebelum selesai digunakan.

val first: RememberObserver = rememberFoo()

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

Saran ini tidak berlaku untuk objek yang diingat lagi secara transitif (seperti, objek yang diingat yang menggunakan objek lain yang diingat). Biasanya, Anda akan menulis kode yang terlihat seperti berikut, yang diizinkan karena objek yang berbeda sedang diingat dan oleh karena itu tidak menyebabkan penggandaan callback yang tidak terduga.

val foo: Foo = rememberFoo()

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

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

Mengasumsikan argumen fungsi sudah diingat

Fungsi tidak boleh mengingat parameter apa pun karena dapat menyebabkan pemanggilan callback ganda untuk RememberObserver dan karena tidak diperlukan. Jika parameter input harus diingat, verifikasi bahwa parameter tersebut tidak menerapkan RememberObserver, atau mewajibkan pemanggil untuk mengingat argumennya.

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

Hal ini tidak berlaku untuk objek yang diingat secara transitif. Saat mengingat objek yang berasal dari argumen fungsi, pertimbangkan untuk menentukannya sebagai salah satu kunci ke remember:

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

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

Jangan mempertahankan objek yang sudah diingat

Mirip dengan mengingat kembali objek, Anda harus menghindari mempertahankan objek yang diingat untuk mencoba memperpanjang masa pakainya. Hal ini merupakan akibat dari saran dalam Masa aktif status: retain tidak boleh digunakan dengan objek yang memiliki masa aktif yang tidak sesuai dengan penawaran retensi masa aktif. Karena objek remembered memiliki masa aktif yang lebih pendek daripada objek retained, Anda tidak boleh mempertahankan objek yang diingat. Sebagai gantinya, lebih baik mempertahankan objek di situs asal daripada mengingatnya.