互通性 API

在您的App中導入 Compose,把 Compose和view-based UI 結合一起。以下清單中提供一些 API、建議與秘訣,可幫助您輕鬆轉換到 Compose。

在檢視畫面中的 Compose

你可以將Compose-based UI 加到採用view-based設計的既有App。

如要建立以 Compose 為基礎的新畫面,請讓活動呼叫 setContent() 方法,然後視需要傳遞任何可組合函式。

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

這段程式碼看起來就像 Compose-only App中會出現的寫法。

ComposeView 的 ViewCompositionStrategy

當檢視畫面從視窗卸離時,Compose 在預設情況下會處置Composition。Compose UI View 類型 (例如 ComposeViewAbstractComposeView) 是使用ViewCompositionStrategy來定義此行為。

Compose 在預設情況下是使用 DisposeOnDetachedFromWindow 策略。但在某些使用 Compose UI View 類型的情況下,這個預設值是不適合的

  • Fragment。Composition 必須遵循 Fragment 的檢視生命週期,使 Compose UI View 類型能夠儲存狀態。

  • 轉換。只要在轉換中使用 Compose UI View,當轉換開始 (而非轉換結束),系統就會從視窗卸離,使您的可編輯卸離其仍在螢幕上的狀態。

  • RecyclerView查看 holders,或由您自行管理的生命週期自訂 View

在某些情況下,除非您手動呼叫 AbstractComposeView.disposeComposition,否則應用程式將緩慢地無法從 Composition 執行個體釋放出記憶體。

請設定其他策略、或透過呼叫 setViewCompositionStrategy 方法,建立自己的策略,自動地處置不再需要的 Composition。例如,lifecycle 遭刪除時,DisposeOnLifecycleDestroyed 策略會處置Composition。此策略適用於與已知 LifecycleOwner 共用 1 對 1 關係的Compose UI View 類型。如果不知道 LifecycleOwner,可以使用 DisposeOnViewTreeLifecycleDestroyed

如需查看這個 API 的實際應用情形,請參閱 Fragment 區節中的 ComposeView

Fragment 中的 ComposeView

如要在片段或現有的檢視畫面版面配置中加入 Compose UI,請使用 ComposeView 並呼叫其 setContent() 方法。ComposeView 是 Android View

您可將 ComposeView 和其他的 View 一樣地放在 XML 版面配置中:

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

請在 Kotlin 原始碼中,從 XML 定義的版面配置資源加載版面配置。然後使用 XML ID 取得 ComposeView,並設定最適合代管 View 的 Composition 策略,並呼叫 setContent() 以使用 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
    }
}

兩個細微不同的文字元素,一個在另一個的上面

圖 1. 這個程式碼會顯示在 View UI 階層中新增 Compose 元素的程式碼輸出內容。「Hello Android!」文字會以 TextView 小工具顯示。「您好 Compose!」文字會顯示Compose 文字元素。

假如您使用 Compose 建立全螢幕畫面,可以直接在片段中加入 ComposeView,避免完全使用 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!")
                }
            }
        }
    }
}

如果在同一個版面配置中有多個 ComposeView 元素,則每個元素都必須有專屬 ID 供 savedInstanceState 運作。

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

res/values/ids.xml 檔案已定義 ComposeView ID:

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

在 Compose 中的檢視畫面

您可以在 Compose UI 中加入 Android 檢視區塊階層。如要使用 Compose 尚未提供的 UI 元素 (例如 AdView),這種做法就特別實用。您也可以透過這種做法重複使用自己設計的自訂檢視畫面。

如要加入檢視表元素或階層,請使用 AndroidView 可組合式AndroidView 會傳遞一個傳回 View 的 lambda。AndroidView 也提供了 update回呼,當 view 加載時呼叫的回呼。AndroidView 會在每次回呼變更時,讀取 State 時重組。舉例來說,如同其他內建可組合函式,AndroidView 會透過可用的 Modifier 參數設定其本身在上層可組合函式中的位置。

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

如要嵌入 XML 版面配置,請使用 androidx.compose.ui:ui-viewbinding 程式庫提供的 AndroidViewBinding API。如要這麼做,您的專案必須啟用 檢視畫面繫結

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

Compose 中的片段

使用 AndroidViewBinding 可組合函式在 Compose 中新增 Fragment。方法是:將包含 FragmentContainerView 的 XML 展開作為 Fragment 的持有者。

舉例來說,如果您定義了 my_fragment_layout.xml,則可以使用這樣的程式碼,並將 android:name XML 屬性替換為 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" />

在 Compose 中以下列方式展開這個片段:

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

如果您在同一個版面配置需要使用多個片段,請確定您已為每個 FragmentContainerView 定義專屬 ID。

從 Compose 呼叫 Android 架構

Compose 會在 Android 架構類別內運作。舉例來說,託管於 Android View 類別 (例如 ActivityFragment),而且可能需要使用 Context 的 Android 架構類別、系統資源、ServiceBroadcastReceiver

如要進一步瞭解系統資源,請參閱 Compose 中的資源說明文件。

Composition Locals

CompositionLocal 類別可讓使用者透過可編輯的函式以默示方式傳送資料。通常在 UI 樹狀結構的特定節點中提供值。該值可用在可撰寫的子系中,但不必將 CompositionLocal 宣告為可組合函式中的參數。

CompositionLocal用於在 Compose 中傳播 Android 架構類型的值,例如 ContextConfigurationView 其中,Compose 程式碼是由對應的 LocalContextLocalConfiguration,或 LocalView。請注意,CompositionLocal 類別前面會加上 Local,以在 IDE 中使用自動完成功能來進一步發現。

使用 current 屬性存取 CompositionLocal 目前的值。舉例來說,以下程式碼就是使用Context 在撰寫使用者介面樹狀結構的呼叫中,呼叫 LocalContext.current的 Google Ads 新帳戶重新申請驗證。

@Composable
fun rememberCustomView(): CustomView {
    val context = LocalContext.current
    return remember { CustomView(context).apply { /*...*/ } }
}

如需更完整的範例,請參閱本文結尾處的個案研究:BroadcastReceivers 部分。

其他互動

如果沒有根據所需互動定義的公用程式,最佳做法就是按照一般 Compose 指南,讓資料向下流動並讓活動向上流動。如要進一步瞭解如何運用 Compose,請參閱這篇文章。舉例來說,這個合成事件會啟動其他活動:

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

個案研究:BroadcastReceiver

如需實際範例,建議您在 Compose 中遷移或實作。如要展示 CompositionLocal副作用 (例如 BroadcastReceiver),您必須從可組合函式註冊該元件。

這項解決方案是使用 LocalContext,來使用目前結構定義,以及 rememberUpdatedStateDisposableEffect 副作用。

@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 */
}