互通性 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 在預設情況下是使用 DisposeOnDetachedFromWindowOrReleasedFromPool 策略。但在某些使用 Compose UI View 類型的情況下,這個預設值是不適合的

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

  • 轉換。只要在轉換中使用 Compose UI View,View 就會在轉換開始 (而非轉換結束) 時從視窗卸離,因此導致您的可組合項仍顯示在畫面上時就處置其狀態。

  • 您自己的生命週期管理自訂 View

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

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

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

Fragment 中的 ComposeView

如要在片段或現有的 View 版面配置中加入 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 中傳遞 ContextConfigurationView 等 Android 架構類型的值,其中的 Compose 程式碼是由對應的 LocalContextLocalConfiguration,或 LocalView 所代管。請注意,CompositionLocal 類別前面會加上 Local,以在 IDE 中使用自動完成功能來進一步發現。

使用 current 屬性存取 CompositionLocal 目前的值。舉例來說,以下程式碼會在 Toast.makeToast 方法中提供 LocalContext.current,以顯示浮動式訊息。

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

如需更完整的範例,請參閱本文結尾處的個案研究: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 */
}