API hỗ trợ tương tác

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

Khi sử dụng Compose trong ứng dụng, bạn có thể kết hợp Compose với Thành phần hiển thị dựa trên Giao diện người dùng. 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 giao diện màn hình mới, một màn hình hoàn toàn dựa trên Compose, hãy gọi phương thức setContent() và truyền tham số tới bất kỳ hàm 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 Giao diện người dùng của 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à lúc quá trình chuyển đổi kết thúc, điều này khiến cho các thành phần kết hợp sẽ bỏ qua trạng thái hiện tại kể cả khi chúng vẫn hiện trên màn hình.

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

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

Để tự động huỷ bỏ các thành phần 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 không định nghĩa LifecycleOwner, đối tượng DisposeOnViewTreeLifecycleDestroyed sẽ được sử dụng .

Xem thêm các hoạt động của API này tại ComposeView trong Mảnh (Fragments).

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(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
        return view
    }

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

Hai phần tử văn bản văn có chút khác biệt, một phần tử nằm phía trên phần tử 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 đưa lớp ComposeView trực tiếp 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 sử dụng hoàn toàn tệp bố cục XML.

class ExampleFragment : 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(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
    }
}

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 ExampleFragment : Fragment() {

  override fun onCreateView(...): View = LinearLayout(...).apply {
      addView(ComposeView(...).apply {
          id = R.id.compose_view_x
          ...
      })
      addView(TextView(...))
      addView(ComposeView(...).apply {
          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>

Thành phần hiển thị trong Compose

Bạn có thể đưa một hệ phân cấp thành phần hiển thị Android vào Giao diện người dùng của 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 của 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 thành phần hiển thị tùy chỉnh mà bạn đã thiết kế trước đó.

Để 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 nào đó, chẳng hạn như để đặt vị trí của đối tượng này trong thành phần kết hợp mẹ.

@Composable
fun CustomView() {
    val selectedItem = remember { mutableStateOf(0) }

    // Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates custom view
            CustomView(context).apply {
                // Sets up listeners for View -> Compose communication
                myView.setOnClickListener {
                    selectedItem.value = 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.coordinator.selectedItem = selectedItem.value
        }
    )
}

@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 thành phần 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. 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 thành phần 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 ExampleActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // get data from savedInstanceState
        setContent {
            MaterialTheme {
                ExampleComposable(data, onButtonClick = {
                    startActivity(/*...*/)
                })
            }
        }
    }
}

@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 lề, hãy giả sử BroadcastReceiver cần được đăng ký qua một hàm có thể 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 */
}