Mengintegrasi Compose dengan arsitektur aplikasi yang sudah ada

Pola arsitektur Aliran Data Searah (UFD) berfungsi lancar dengan Compose. Jika aplikasi menggunakan jenis pola arsitektur lain, seperti Presenter View Model (MVP), sebaiknya migrasikan bagian UI tersebut ke UDF sebelum atau saat mengadopsi Compose.

ViewModel dalam Compose

Jika menggunakan library Architecture Components ViewModel, Anda dapat mengakses ViewModel dari fungsi mana pun dengan memanggil fungsi viewModel(), seperti yang dijelaskan dalam Dokumentasi integrasi Compose dengan library umum.

Saat menggunakan Compose, berhati-hatilah saat menggunakan jenis ViewModel yang sama dalam berbagai komponen karena elemen ViewModel mengikuti cakupan siklus proses View. Cakupan akan berupa aktivitas host, fragmen, atau grafik navigasi jika library Navigasi digunakan.

Misalnya, jika komponen dapat dihosting dalam aktivitas, viewModel() selalu menampilkan instance yang sama yang hanya akan dihapus saat aktivitas telah selesai. Pada contoh berikut, pengguna yang sama akan disapa dua kali karena instance GreetingViewModel yang sama digunakan kembali di semua komponen dalam aktivitas host. Instance ViewModel pertama yang telah dibuat digunakan kembali di komponen lain.

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    Greeting("user1")
                    Greeting("user2")
                }
            }
        }
    }
}

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.message.observeAsState("")

    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String): ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

Karena grafik navigasi juga mencakup elemen ViewModel, komponen yang merupakan tujuan dalam grafik navigasi memiliki instance ViewModel yang berbeda. Dalam hal ini, ViewModel dimasukkan ke siklus proses tujuan dan akan dihapus saat tujuan dihapus dari backstack. Pada contoh berikut, saat pengguna membuka layar Profil, instance baru GreetingViewModel akan dibuat.

@Composable
fun MyScreen() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            Greeting(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.message.observeAsState("")

    Text(messageUser)
}

Sumber status kebenaran

Saat Anda mengadopsi Compose di satu bagian UI, Compose dan kode sistem View mungkin perlu membagikan data. Jika memungkinkan, sebaiknya Anda merangkum status bersama tersebut di class lain yang mengikuti praktik terbaik UDF dan digunakan oleh kedua platform tersebut, misalnya, di ViewModel yang mengekspos aliran data dari data bersama untuk mengurangi update data.

Namun, hal ini tidak selalu mungkin jika data yang akan dibagikan dapat diubah atau terikat erat dengan elemen UI. Dalam hal ini, satu sistem harus menjadi sumber kebenaran, dan sistem tersebut perlu membagikan setiap update data ke sistem lain. Sebagai aturan umum, sumber kebenaran harus dimiliki oleh elemen mana pun yang lebih dekat dengan root hierarki UI.

Compose sebagai sumber kebenaran

Gunakan fungsi SideEffect untuk memublikasikan status Compose ke kode non-compose. Dalam hal ini, sumber kebenaran disimpan dalam properti yang dapat disusun yang mengirim update status.

Contohnya, library analisis Anda dapat digunakan untuk mengelompokkan populasi pengguna dengan melampirkan metadata khusus (properti pengguna pada contoh ini) ke semua peristiwa analisis berikutnya. Untuk memberitahukan jenis pengguna dari pengguna saat ini ke library analisis Anda, gunakan SideEffect untuk memperbarui nilainya.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

Untuk mengetahui informasi selengkapnya, lihat dokumentasi efek samping.

Sistem View sebagai sumber kebenaran

Jika sistem View memiliki status dan membagikannya dengan Compose, sebaiknya gabungkan status dalam objek mutableStateOf agar Compose aman dari thread. Jika Anda menggunakan pendekatan ini, fungsi yang dapat dikomposisi akan disederhanakan karena tidak lagi memiliki sumber kebenaran, tetapi sistem View harus mengupdate status yang dapat diubah dan View yang menggunakan status tersebut.

Dalam contoh berikut, CustomViewGroup berisi TextView dan ComposeView dengan komponen TextField di dalamnya. TextView harus menampilkan konten yang diketik pengguna dalam TextField.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}