API tương tác

Khi sử dụng Compose trong ứng dụng, bạn có thể kết hợp Compose với giao diện người dùng dựa trên Khung hiển thị. Dưới đây là danh sách các API, kiến nghị và mẹo để khiến việc chuyển đổi sang Compose dễ dàng hơn.

Compose trong Chế độ xem

Bạn có thể thêm giao diện người dùng của Compose vào ứng dụng hiện có sử dụng thiết kế dựa trên thành phần hiển thị

Để tạo một màn hình mới hoàn toàn dựa trên Compose, hãy gọi phương thức setContent() và truyền bất kỳ hàm có khả năng kết hợp nào mà bạn muốn.

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!")
}

Đoạn mã trên được viết giống hệt những gì bạn sẽ thấy trong bất kỳ ứng dụng thuần Compose nào

Phương thức ViewCompositionStrategy của lớp ComposeView

Theo mặc định, Compose sẽ huỷ bỏ Cấu trúc (Composition) bất cứ khi nào thành phần hiển thị bị tách khỏi cửa sổ. Các loại View giao diện người dùng của Compose như lớp ComposeView và lớp AbstractComposeView sử dụng lớp giao tiếp ViewCompositionStrategy để thực hiện tác vụ này.

Theo mặc định, Compose sẽ sử dụng lớp giao tiếp DisposeOnDetachedFromWindowOrReleasedFromPool. Tuy nhiên, giá trị mặc định này có thể gây phiền phức trong một số trường hợp khi sử dụng các loại View Giao diện người dùng của Compose trong:

  • Mảnh. Cấu trúc sử dụng phải tuân thủ theo vòng đời của thành phần hiển thị mảnh dành cho View Giao diện người dùng của Compose để lưu lại trạng thái.

  • Chuyển đổi. Bất cứ khi nào View trên giao diện người dùng Compose được dùng như một phần của quá trình chuyển đổi, nó sẽ bị tách ra khỏi cửa sổ ngay khi quá trình chuyển đổi bắt đầu chứ không phải là khi quá trình chuyển đổi kết thúc, điều này khiến cho thành phần kết hợp bỏ qua trạng thái của nó trong khi vẫn hiện trên màn hình.

  • View tuỳ chỉnh của riêng bạn do vòng đời quản lý.

Trong một số trường hợp như vậy, ứng dụng cũng có thể dần bị rò rỉ bộ nhớ từ các thực thể Cấu trúc (Composition), trừ phi bạn gọi phương thức AbstractComposeView.disposeComposition theo cách thủ công.

Để tự động huỷ bỏ Cấu trúc không cần dùng đến, hãy đặt một chiến lược khác hoặc tự tạo chiến lược riêng bằng cách gọi phương thức setViewCompositionStrategy. Ví dụ: chiến lược DisposeOnLifecycleDestroyed sẽ huỷ bỏ Các thành phần khi lifecycle bị huỷ. Chiến lược này phù hợp với các loại View Giao diện người dùng Compose có chung mối liên kết 1:1 với một lớp giao tiếp LifecycleOwner đã biết. Khi LifecycleOwner là không xác định, DisposeOnViewTreeLifecycleDestroyed sẽ được dùng.

Xem API này trong thực tế tại ComposeView trong Mảnh (Fragment).

ComposeView trong Mảnh (Fragment)

Nếu bạn muốn kết hợp nội dung từ Giao diện người dùng Compose trong một mảnh (fragment) hoặc bố cục Thành phần hiển thị (View layout) hiện có, hãy sử dụng lớp ComposeView và gọi phương thức setContent(). Lớp ComposeView là một lớp cơ sở View của Android.

Bạn có thể đặt lớp ComposeView vào bố cục XML giống như đối với bất kỳ lớp cơ sở View nào khác:

<?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>

Trong mã nguồn Kotlin, hãy tăng cường sử dụng bố cục trong tài nguyên bố cục được định nghĩa trong XML. Tiếp theo, tạo lớp ComposeView bằng mã XML, đặt chiến lược Bố cục phù hợp nhất với máy chủ View và gọi phương thức setContent() để sử dụng tính năng từ 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
    }
}

Hai thành phần văn bản văn có chút khác biệt, một thành phần ở trên thành phần khác

Hình 1. Hình trên thể hiện kết quả chạy của đoạn mã trên sau khi thêm các phần tử Compose vào một hệ phân cấp Giao diện người dùng. Dòng chữ "Hello Android!" ("Xin chào Android!") hiển thị nhờ tiện ích TextView. Dòng chữ "Hello Compose!" ("Xin chào Compose!") hiển thị nhờ phần tử Compose.

Bạn cũng có thể trực tiếp đưa lớp ComposeView vào một mảnh nếu chế độ toàn màn hình của bạn được tạo bằng Compose. Điều này cho phép bạn tránh hoàn toàn việc sử dụng tệp bố cục XML.

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!")
                }
            }
        }
    }
}

Nhiều ComposeView trong cùng một bố cục

Nếu có nhiều phần tử ComposeView trong cùng một bố cục, thì mỗi phần tử phải có một mã nhận dạng duy nhất để biến savedInstanceState hoạt động.

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

Mã nhận dạng ComposeView được định nghĩa trong tệp res/values/ids.xml:

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

Khung hiển thị trong Compose

Bạn có thể đưa một hệ phân cấp khung hiển thị Android vào giao diện người dùng Compose. Phương pháp này đặc biệt hữu ích nếu bạn muốn sử dụng các thành phần trên giao diện người dùng chưa có trong Compose, chẳng hạn như AdView. Phương pháp này cũng cho phép bạn tái sử dụng các khung hiển thị tuỳ chỉnh mà bạn đã thiết kế.

Để gộp thêm một phần tử hoặc một hệ thành phần hiển thị phân cấp, hãy sử dụng thành phần kết hợp AndroidView. AndroidView sẽ được truyền một hàm lambda để trả về một lớp View. AndroidView cũng cung cấp một lệnh gọi lại lệnh update khi lượt xem tăng cao. AndroidView sẽ tái cấu trúc lại bất cứ khi nào một biểu thị State giữa các hàm callback thay đổi. Cũng như nhiều thành phần kết hợp được tích hợp sẵn khác, AndroidView sẽ sử dụng một tham số Modifier để đặt vị trí của nó trong thành phần kết hợp cha, chẳng hạn như vậy.

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

Để nhúng bố cục XML bất kỳ, hãy sử dụng API AndroidViewBinding do thư viện androidx.compose.ui:ui-viewbinding cung cấp. Để làm được điều này, dự án của bạn cần bật tính năng liên kết khung hiển thị.

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

Các mảnh trong Compose

Sử dụng thành phần kết hợp AndroidViewBinding để thêm một Fragment vào Compose. AndroidViewBinding có quy trình xử lý dành riêng cho mảnh, chẳng hạn như xoá mảnh khi thành phần kết hợp rời khỏi cấu trúc.

Hãy làm việc này bằng cách tăng cường tệp XML chứa FragmentContainerView dưới dạng chủ sở hữu của Fragment.

Chẳng hạn nếu đã xác định được my_fragment_layout.xml, bạn có thể sử dụng mã như thế này trong khi thay thế thuộc tính XML android:name bằng tên lớp Fragment của bạn:

<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" />

Tăng cường mảnh này trong Compose như sau:

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

Nếu cần sử dụng nhiều mảnh trong cùng một bố cục, hãy đảm bảo là bạn đã xác định một mã nhận dạng duy nhất cho mỗi FragmentContainerView.

Gọi khung Android qua Compose

Tính năng compose hoạt động trong các lớp của khung Android. Ví dụ: tệp được lưu trữ trên các lớp Khung hiển thị Android (như Activity hoặc Fragment) và có thể cần tận dụng các lớp khác của khung Android (như Context), tài nguyên hệ thống, Service hoặc BroadcastReceiver.

Để tìm hiểu thêm về tài nguyên hệ thống, hãy xem thêm tài liệu Tài nguyên trong Compose.

Composition Locals

Lớp CompositionLocal cho phép truyền dữ liệu trực tiếp thông qua các hàm tổng hợp. Những lớp này thường được cấp một giá trị nào đó ở một nút nhất định trong cây Giao diện người dùng. Giá trị con có thể kết hợp của hàm đó có thể sử dụng giá trị đó mà không cần khai báo lớp CompositionLocal dưới dạng tham số trong hàm có khả năng kết hợp.

Lớp CompositionLocal được dùng để truyền giá trị cho các loại khung Android trong Compose như Context, Configuration hoặc View mà trong đó mã Compose lưu trữ bởi các biến LocalContext, LocalConfiguration, hoặc LocalView tương ứng. Lưu ý rằng các lớp CompositionLocal có tiền tố Local để chức năng tự động điền trong IDE dễ nhận diện hơn.

Truy cập giá trị hiện tại của CompositionLocal bằng cách sử dụng thuộc tính current. Ví dụ: mã dưới đây sẽ hiển thị một thông báo ngắn bằng cách đưa LocalContext.current vào phương thức Toast.makeToast.

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

Để có ví dụ hoàn chỉnh hơn, hãy xem thêm mục Nghiên cứu điển hình: BroadcastReceivers ở cuối tài liệu này.

Các hoạt động tương tác khác

Nếu không có tiện ích nào được xác định cho các hoạt động tương tác bạn cần thì cách tốt nhất là làm theo hướng dẫn Compose chung, dữ liệu chạy xuống, sự kiện chạy lên (được thảo luận kỹ hơn trong phần Tư duy trong Compose). Ví dụ, hoạt động tổng hợp này sẽ khởi chạy một hoạt động khác:

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

Nghiên cứu điển hình: BroadcastReceivers

Để có một ví dụ thực tế hơn về các tính năng mà bạn muốn di chuyển hoặc triển khai trong Compose và để hiển thị CompositionLocal với các hiệu ứng phụ, hãy giả sử BroadcastReceiver cần được đăng ký qua một hàm có khả năng kết hợp.

Giải pháp này tận dụng LocalContext để sử dụng trong trường hợp hiện tại, và rememberUpdatedState cũng như hiệu ứng lề 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 */
}