API Interoperabilitas

Saat mengadopsi Compose di aplikasi Anda, UI berbasis View dan UI Compose dapat digabungkan. Berikut adalah daftar API, rekomendasi, dan tips untuk mempermudah transisi ke Compose.

Compose di View

Anda dapat menambahkan UI berbasis Compose ke dalam aplikasi yang sudah ada dan menggunakan desain berbasis View.

Untuk membuat layar baru yang sepenuhnya berbasis Compose, minta aktivitas Anda memanggil metode setContent(), dan teruskan fungsi composable, yang Anda suka.

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

        setContent { // In here, we can call composables!
            MaterialTheme {
                Greeting(name = "compose")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

Kode ini terlihat seperti yang Anda temukan di aplikasi khusus Compose.

ViewCompositionStrategy untuk ComposeView

Secara default, Compose akan menghapus Komposisi setiap kali tampilan dilepas dari jendela. Jenis View UI Compose seperti ComposeView dan AbstractComposeView menggunakan ViewCompositionStrategy yang menentukan perilaku ini.

Secara default, Compose menggunakan strategi DisposeOnDetachedFromWindowOrReleasedFromPool. Namun, nilai default ini mungkin tidak diinginkan dalam beberapa situasi saat jenis View UI Compose digunakan di:

  • Fragmen. Komposisi harus mengikuti siklus proses tampilan fragmen untuk jenis View UI Compose untuk menyimpan status.

  • Transisi. Setiap kali UI Compose View digunakan sebagai bagian dari transisi, UI tersebut akan dilepaskan dari jendelanya saat transisi dimulai, bukan saat transisi berakhir, sehingga menyebabkan composable Anda dibuang statusnya saat berada di layar.

  • View kustom yang dikelola siklus proses Anda sendiri.

Dalam beberapa situasi semacam ini, aplikasi juga dapat secara perlahan membocorkan memori dari instance Komposisi, kecuali jika Anda memanggil AbstractComposeView.disposeComposition secara manual.

Untuk menghapus Komposisi secara otomatis saat tidak diperlukan lagi, tetapkan strategi yang berbeda atau buat strategi sendiri dengan memanggil metode setViewCompositionStrategy. Misalnya, strategi DisposeOnLifecycleDestroyed membuang Komposisi saat lifecycle dihancurkan. Strategi ini sesuai untuk jenis View UI Compose yang memiliki hubungan 1-1 dengan LifecycleOwner yang dikenal. Jika LifecycleOwner tidak diketahui, DisposeOnViewTreeLifecycleDestroyed dapat digunakan.

Lihat cara kerja API ini di ComposeView di Fragmen.

ComposeView di Fragment

Jika Anda ingin menggabungkan konten UI Compose dalam fragmen atau tata letak View yang sudah ada, gunakan ComposeView dan panggil metode setContent(). ComposeView adalah View Android.

Anda dapat menempatkan ComposeView di tata letak XML, seperti View lainnya:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello Android!" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

Dalam kode sumber Kotlin, inflate tata letak dari resource tata letak yang ditentukan dalam XML. Kemudian, dapatkan ComposeView menggunakan ID XML, tetapkan strategi Komposisi yang paling sesuai untuk View host, dan panggil setContent() untuk menggunakan Compose.

class ExampleFragment : Fragment() {

    private var _binding: FragmentExampleBinding? = null

    // This property is only valid between onCreateView and onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentExampleBinding.inflate(inflater, container, false)
        val view = binding.root
        binding.composeView.apply {
            // Dispose of the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Dua elemen teks yang sedikit berbeda, satu di atas yang lain

Gambar 1. Ini menunjukkan output kode yang menambahkan elemen Compose dalam hierarki UI View. Teks "Hello Android!" ditampilkan oleh widget TextView. Teks "Hello Compose!" ditampilkan oleh elemen teks Compose.

Anda juga dapat menyertakan ComposeView secara langsung dalam fragmen jika layar penuh Anda dibuat dengan Compose, yang memungkinkan Anda menghindari penggunaan file tata letak XML sepenuhnya.

class ExampleFragmentNoXml : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            // Dispose of the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
    }
}

Beberapa ComposeView dalam tata letak yang sama

Jika ada beberapa elemen ComposeView dalam tata letak yang sama, masing-masing harus memiliki ID unik agar savedInstanceState dapat berfungsi.

class ExampleFragmentMultipleComposeView : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View = LinearLayout(requireContext()).apply {
        addView(
            ComposeView(requireContext()).apply {
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                id = R.id.compose_view_x
                // ...
            }
        )
        addView(TextView(requireContext()))
        addView(
            ComposeView(requireContext()).apply {
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                id = R.id.compose_view_y
                // ...
            }
        )
    }
}

ID ComposeView ditentukan dalam file res/values/ids.xml:

<resources>
    <item name="compose_view_x" type="id" />
    <item name="compose_view_y" type="id" />
</resources>

View di Compose

Anda bisa menyertakan hierarki Android View di UI Compose. Pendekatan ini sangat berguna jika Anda ingin menggunakan elemen UI yang belum tersedia di Compose, seperti AdView. Pendekatan ini juga memungkinkan Anda menggunakan kembali tampilan kustom yang mungkin sudah Anda desain.

Untuk menyertakan elemen tampilan atau hierarki, gunakan composable AndroidView. AndroidView dimasukkan lambda yang menampilkan View. AndroidView juga menyediakan callback update yang dipanggil saat tampilan di-inflate. AndroidView akan merekomposisi setiap kali pembacaan State dalam callback berubah. AndroidView, seperti banyak composable bawaan lainnya, memerlukan parameter Modifier yang dapat digunakan, misalnya, untuk menyetel posisinya di composable induk.

@Composable
fun CustomView() {
    var selectedItem by remember { mutableStateOf(0) }

    // Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates view
            MyView(context).apply {
                // Sets up listeners for View -> Compose communication
                setOnClickListener {
                    selectedItem = 1
                }
            }
        },
        update = { view ->
            // View's been inflated or state read in this block has been updated
            // Add logic here if necessary

            // As selectedItem is read here, AndroidView will recompose
            // whenever the state changes
            // Example of Compose -> View communication
            view.selectedItem = selectedItem
        }
    )
}

@Composable
fun ContentExample() {
    Column(Modifier.fillMaxSize()) {
        Text("Look at this CustomView!")
        CustomView()
    }
}

Untuk menyematkan tata letak XML, gunakan AndroidViewBinding API, yang disediakan oleh library androidx.compose.ui:ui-viewbinding. Untuk melakukannya, project Anda harus mengaktifkan binding tampilan.

@Composable
fun AndroidViewBindingExample() {
    AndroidViewBinding(ExampleLayoutBinding::inflate) {
        exampleView.setBackgroundColor(Color.GRAY)
    }
}

Fragment di Compose

Gunakan composable AndroidViewBinding untuk menambahkan Fragment di Compose. AndroidViewBinding memiliki penanganan khusus fragmen seperti menghapus fragmen saat composable keluar dari komposisi.

Lakukan dengan meng-inflate XML yang berisi FragmentContainerView sebagai holder untuk Fragment.

Misalnya, jika memiliki my_fragment_layout.xml yang ditentukan, Anda dapat menggunakan kode seperti ini saat mengganti atribut XML android:name dengan nama class Fragment:

<androidx.fragment.app.FragmentContainerView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/fragment_container_view"
  android:layout_height="match_parent"
  android:layout_width="match_parent"
  android:name="com.example.MyFragment" />

Inflate fragmen ini di Compose sebagai berikut:

@Composable
fun FragmentInComposeExample() {
    AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
        val myFragment = fragmentContainerView.getFragment<MyFragment>()
        // ...
    }
}

Jika Anda perlu menggunakan beberapa fragmen dalam tata letak yang sama, pastikan Anda telah menentukan ID unik untuk setiap FragmentContainerView.

Memanggil framework Android dari Compose

Compose beroperasi dalam class framework Android. Misalnya, class tersebut dihosting di class Android View, seperti Activity atau Fragment, dan mungkin perlu menggunakan class framework Android seperti Context, resource sistem, Service, atau BroadcastReceiver.

Untuk mempelajari resource sistem lebih lanjut, lihat dokumentasi Resource dalam Compose.

Lokal Komposisi

Dengan class CompositionLocal, penerusan data secara implisit bisa dilakukan melalui fungsi composable. Fungsi ini biasanya diberi nilai dalam node tertentu dari pohon UI. Nilai tersebut dapat digunakan oleh turunan composable-nya tanpa mendeklarasikan CompositionLocal sebagai parameter dalam fungsi composable.

CompositionLocal digunakan untuk mengisi nilai jenis framework Android di Compose seperti Context, Configuration, atau View di mana kode Compose dihosting dengan kode yang sesuai LocalContext, LocalConfiguration, atau LocalView. Perhatikan bahwa class CompositionLocal diawali dengan Local agar mendapatkan visibilitas yang lebih baik dengan pelengkapan otomatis di IDE.

Akses nilai CompositionLocal saat ini menggunakan properti current. Misalnya, kode di bawah ini menampilkan pesan toast dengan menyediakan LocalContext.current ke dalam metode Toast.makeToast.

@Composable
fun ToastGreetingButton(greeting: String) {
    val context = LocalContext.current
    Button(onClick = {
        Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show()
    }) {
        Text("Greet")
    }
}

Untuk contoh yang lebih lengkap, lihat bagian Studi Kasus: BroadcastReceivers di akhir dokumen ini.

Interaksi lainnya

Jika tidak ada utilitas yang ditetapkan untuk interaksi yang Anda butuhkan, praktik terbaiknya adalah mengikuti pedoman Compose umum, data mengalir ke bawah, peristiwa mengalir ke atas (dibahas lebih jauh lagi di bagian Paradigma Compose). Misalnya, composable ini akan meluncurkan aktivitas yang berbeda:

class OtherInteractionsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // get data from savedInstanceState
        setContent {
            MaterialTheme {
                ExampleComposable(data, onButtonClick = {
                    startActivity(Intent(this, MyActivity::class.java))
                })
            }
        }
    }
}

@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
    Button(onClick = onButtonClick) {
        Text(data.title)
    }
}

Studi Kasus: BroadcastReceivers

Untuk contoh fitur yang lebih realistis, Anda mungkin ingin bermigrasi atau menerapkannya di Compose dan menampilkan CompositionLocal dan efek samping, misalnya BroadcastReceiver harus didaftarkan dari fungsi composable.

Solusi ini memanfaatkan LocalContext untuk menggunakan konteks saat ini, serta efek samping rememberUpdatedState dan DisposableEffect.

@Composable
fun SystemBroadcastReceiver(
    systemAction: String,
    onSystemEvent: (intent: Intent?) -> Unit
) {
    // Grab the current context in this part of the UI tree
    val context = LocalContext.current

    // Safely use the latest onSystemEvent lambda passed to the function
    val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)

    // If either context or systemAction changes, unregister and register again
    DisposableEffect(context, systemAction) {
        val intentFilter = IntentFilter(systemAction)
        val broadcast = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentOnSystemEvent(intent)
            }
        }

        context.registerReceiver(broadcast, intentFilter)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            context.unregisterReceiver(broadcast)
        }
    }
}

@Composable
fun HomeScreen() {

    SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
        val isCharging = /* Get from batteryStatus ... */ true
        /* Do something if the device is charging */
    }

    /* Rest of the HomeScreen */
}