Weitere Hinweise

Die Migration von Ansichten zu Compose ist zwar rein UI-bezogen, es gibt aber viele Dinge zu beachten, um eine sichere und schrittweise Migration durchzuführen. Auf dieser Seite finden Sie einige Überlegungen zur Migration Ihrer auf Ansichten basierenden App zu Compose.

Design Ihrer App migrieren

Material Design ist das empfohlene Designsystem für das Theming von Android-Apps.

Für auf Ansichten basierende Apps sind drei Versionen von Material verfügbar:

  • Material Design 1 mit der AppCompat-Bibliothek (z.B. Theme.AppCompat.*)
  • Material Design 2 mit der MDC-Android Bibliothek (z.B. Theme.MaterialComponents.*)
  • Material Design 3 mit der MDC-Android Bibliothek (z.B. Theme.Material3.*)

Für Compose-Apps sind zwei Versionen von Material verfügbar:

  • Material Design 2 mit der Compose Material Bibliothek (z.B. androidx.compose.material.MaterialTheme)
  • Material Design 3 mit der Compose Material 3-Bibliothek (z.B. androidx.compose.material3.MaterialTheme)

Wir empfehlen, die neueste Version (Material 3) zu verwenden, wenn das Designsystem Ihrer App dies zulässt. Es gibt Migrationsleitfäden für Ansichten und Compose:

Wenn Sie neue Bildschirme in Compose erstellen, müssen Sie unabhängig von der verwendeten Version von Material Design ein MaterialTheme auf alle zusammensetzbaren Funktionen anwenden, die UI aus den Compose Material-Bibliotheken ausgeben. Die Material-Komponenten (Button, Text usw.) sind von einem MaterialTheme abhängig und ihr Verhalten ist ohne dieses nicht definiert.

Alle Jetpack Compose-Beispiele verwenden ein benutzerdefiniertes Compose-Design, das auf MaterialTheme basiert.

Weitere Informationen finden Sie unter Designsysteme in Compose und XML-Designs zu Compose migrieren.

Wenn Sie die Navigationskomponente in Ihrer App verwenden, finden Sie weitere Informationen unter In einer auf Fragmenten basierenden App von Compose aus navigieren und Jetpack Navigation zu Navigation Compose migrieren.

Gemischte Compose/Ansichten-UI testen

Nachdem Sie Teile Ihrer App zu Compose migriert haben, sind Tests unerlässlich, um sicherzustellen, dass Sie nichts beschädigt haben.

Wenn eine Aktivität oder ein Fragment Compose verwendet, müssen Sie createAndroidComposeRule anstelle von ActivityScenarioRule verwenden. createAndroidComposeRule integriert ActivityScenarioRule in eine ComposeTestRule, mit der Sie Compose- und Ansichtscode gleichzeitig testen können.

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

Weitere Informationen zu Tests finden Sie unter Compose-Layout testen. Informationen zur Interoperabilität mit UI-Testframeworks finden Sie unter Interoperabilität mit Espresso und Interoperabilität mit UiAutomator.

Compose in Ihre bestehende App-Architektur einbinden

Architekturmuster mit unidirektionalem Datenfluss (Unidirectional Data Flow, UDF) funktionieren nahtlos mit Compose. Wenn die App stattdessen andere Arten von Architekturmustern wie Model View Presenter (MVP) verwendet, empfehlen wir, diesen Teil der UI zu UDF zu migrieren, bevor oder während Sie Compose einführen.

ViewModel in Compose verwenden

Wenn Sie die Architecture Components ViewModel-Bibliothek verwenden, können Sie auf ein ViewModel von jeder zusammensetzbaren Funktion aus zugreifen, indem Sie die viewModel() -Funktion aufrufen, wie unter Compose und andere Bibliotheken beschrieben.

Wenn Sie Compose einführen, sollten Sie denselben ViewModel-Typ nicht in verschiedenen zusammensetzbaren Funktionen verwenden, da ViewModel-Elemente den Lebenszyklusbereich von Ansichten verwenden. Der Bereich ist entweder die Hostaktivität, das Fragment oder der Navigationsgraph, wenn die Navigationsbibliothek verwendet wird.

Wenn die zusammensetzbaren Funktionen beispielsweise in einer Aktivität gehostet werden, gibt viewModel() immer dieselbe Instanz zurück, die erst gelöscht wird, wenn die Aktivität beendet wird. Im folgenden Beispiel wird derselbe Nutzer („user1“) zweimal begrüßt, da dieselbe GreetingViewModel-Instanz in allen zusammensetzbaren Funktionen unter der Hostaktivität wiederverwendet wird. Die erste erstellte ViewModel-Instanz wird in anderen zusammensetzbaren Funktionen wiederverwendet.

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

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(  
        factory = GreetingViewModelFactory(userId)  
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

Da Navigationsgraphen auch den Gültigkeitsbereich von ViewModel-Elementen festlegen, haben zusammensetzbare Funktionen, die ein Ziel in einem Navigationsgraphen sind, eine andere Instanz von ViewModel. In diesem Fall ist der Gültigkeitsbereich von ViewModel auf den Lebenszyklus des Ziels beschränkt und wird gelöscht, wenn das Ziel aus dem Backstack entfernt wird. Im folgenden Beispiel wird eine neue Instanz von GreetingViewModel erstellt, wenn der Nutzer zum Bildschirm Profil navigiert.

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

Source of Truth für den Status

Wenn Sie Compose in einem Teil der UI verwenden, müssen sich Compose und der Ansichtssystemcode möglicherweise Daten teilen. Wenn möglich, empfehlen wir, diesen freigegebenen Zustand in einer anderen Klasse zu kapseln, die den UDF-Best Practices folgt, die von beiden Plattformen verwendet werden. Zum Beispiel in einem ViewModel, das einen Stream der weitergegebenen Daten bereitstellt, um Datenaktualisierungen auszugeben.

Das ist jedoch nicht immer möglich, wenn die freizugebenden Daten veränderlich sind oder eng an ein UI-Element gebunden sind. In diesem Fall muss ein System die Source of Truth sein und alle Datenaktualisierungen an das andere System weitergeben. Im Allgemeinen sollte die Source of Truth dem Element gehören, das sich näher an der Root der UI-Hierarchie befindet.

Compose als Source of Truth

Verwenden Sie die SideEffect zusammensetzbare Funktion, um den Compose-Status für Nicht-Compose-Code zu veröffentlichen. In diesem Fall wird die Source of Truth in einer zusammensetzbaren Funktion gespeichert, die Statusaktualisierungen sendet.

Mit Ihrer Analytikbibliothek können Sie beispielsweise Ihre Nutzer nach Segmenten unterteilen, indem Sie allen nachfolgenden Analytikereignissen benutzerdefinierte Metadaten (Nutzerattribute in diesem Beispiel) anhängen. Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analytikbibliothek weitergeben möchten, verwenden Sie SideEffect, um den Wert zu aktualisieren.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

Weitere Informationen finden Sie unter Nebeneffekte in Compose.

Ansichtssystem als Source of Truth

Wenn das Ansichtssystem den Status besitzt und ihn für Compose freigibt, empfehlen wir, den Status in mutableStateOf-Objekte einzuschließen, um ihn für Compose threadsicher zu machen. Wenn Sie diesen Ansatz verwenden, werden zusammensetzbare Funktionen vereinfacht, da sie nicht mehr die Source of Truth haben. Das Ansichtssystem muss jedoch den veränderlichen Status und die Ansichten aktualisieren, die diesen Status verwenden.

Im folgenden Beispiel enthält eine CustomViewGroup eine TextView und eine ComposeView mit einer zusammensetzbaren Funktion TextField. In der TextView muss der Inhalt angezeigt werden, den der Nutzer in das TextField eingibt.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

Freigegebene UI migrieren

Wenn Sie schrittweise zu Compose migrieren, müssen Sie möglicherweise freigegebene UI-Elemente sowohl in Compose als auch im Ansichtssystem verwenden. Wenn Ihre App beispielsweise eine benutzerdefinierte CallToActionButton-Komponente hat, müssen Sie sie möglicherweise sowohl in Compose- als auch in ansichtsbasierenden Bildschirmen verwenden.

In Compose werden freigegebene UI-Elemente zu zusammensetzbaren Funktionen, die in der gesamten App wiederverwendet werden können, unabhängig davon, ob das Element mit XML formatiert wurde oder eine benutzerdefinierte Ansicht ist. Sie erstellen beispielsweise eine zusammensetzbare Funktion CallToActionButton für Ihre benutzerdefinierte Komponente für Call-to-Action Button.

Wenn Sie die zusammensetzbare Funktion in ansichtsbasierenden Bildschirmen verwenden möchten, erstellen Sie einen benutzerdefinierten Ansichtswrapper, der von AbstractComposeView abgeleitet wird. Platzieren Sie in der überschriebenen zusammensetzbaren Funktion Content die von Ihnen erstellte zusammensetzbare Funktion, die in Ihr Compose-Design eingebettet ist, wie im folgenden Beispiel gezeigt:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf("")
    var onClick by mutableStateOf({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Die Parameter der zusammensetzbaren Funktion werden in der benutzerdefinierten Ansicht zu veränderlichen Variablen. Dadurch kann die benutzerdefinierte Ansicht CallToActionViewButton wie eine herkömmliche Ansicht aufgeblasen und verwendet werden. Ein Beispiel dafür mit der Ansichtsbindung finden Sie unten:

class ViewBindingActivity : ComponentActivity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.greeting)
            onClick = { /* Do something */ }
        }
    }
}

Wenn die benutzerdefinierte Komponente einen veränderlichen Status enthält, lesen Sie den Abschnitt Source of Truth für den Status.

Status von der Darstellung trennen

Traditionell ist eine View zustandsbehaftet. Eine View verwaltet Felder, die beschreiben, was angezeigt werden soll, und wie es angezeigt werden soll. Wenn Sie eine View in Compose konvertieren, sollten Sie die gerenderten Daten trennen, um einen unidirektionalen Datenfluss zu erreichen, wie unter Status-Hoisting erläutert.

Eine View hat beispielsweise eine visibility-Eigenschaft, die beschreibt, ob sie sichtbar, unsichtbar oder nicht vorhanden ist. Dies ist eine inhärente Eigenschaft der View. Andere Codeabschnitte können zwar die Sichtbarkeit einer View ändern, aber nur die View selbst weiß, wie ihre aktuelle Sichtbarkeit ist. Die Logik, mit der sichergestellt wird, dass eine View sichtbar ist, kann fehleranfällig sein und ist oft an die View selbst gebunden.

Im Gegensatz dazu können Sie mit Compose mithilfe der bedingten Logik in Kotlin ganz andere zusammensetzbare Funktionen anzeigen:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

Die zusammensetzbare Funktion CautionIcon muss nicht wissen oder sich darum kümmern, warum sie angezeigt wird. Es gibt kein Konzept von visibility. Sie ist entweder in der Komposition oder nicht.

Durch die saubere Trennung von Statusverwaltung und Darstellungslogik können Sie freier ändern, wie Inhalte als Konvertierung des Status in die UI angezeigt werden. Wenn Sie den Status bei Bedarf hoisten können, werden zusammensetzbare Funktionen auch wiederverwendbarer, da die Statusinhaberschaft flexibler ist.

Gekapselte und wiederverwendbare Komponenten fördern

View -Elemente haben oft eine Vorstellung davon, wo sie sich befinden: in einer Activity, einem Dialog, einem Fragment oder irgendwo in einer anderen View-Hierarchie. Da sie oft aus statischen Layoutdateien aufgeblasen werden, ist die Gesamtstruktur einer View in der Regel sehr starr. Das führt zu einer engeren Kopplung und erschwert das Ändern oder Wiederverwenden einer View.

Eine benutzerdefinierte View kann beispielsweise davon ausgehen, dass sie eine Kinderansicht eines bestimmten Typs mit einer bestimmten ID hat, und ihre Eigenschaften direkt als Reaktion auf eine Aktion ändern. Dadurch werden diese View-Elemente eng miteinander gekoppelt: Die benutzerdefinierte View kann abstürzen oder beschädigt werden, wenn sie das untergeordnete Element nicht findet, und das untergeordnete Element kann wahrscheinlich nicht ohne das übergeordnete Element der benutzerdefinierten View wiederverwendet werden.

Das ist in Compose mit wiederverwendbaren zusammensetzbaren Funktionen weniger ein Problem. Übergeordnete Elemente können Status und Callbacks einfach angeben, sodass Sie wiederverwendbare zusammensetzbare Funktionen schreiben können, ohne genau zu wissen, wo sie verwendet werden.

@Composable
fun AScreen() {
    var isEnabled by rememberSaveable { mutableStateOf(false) }

    Column {
        ImageWithEnabledOverlay(isEnabled)
        ControlPanelWithToggle(
            isEnabled = isEnabled,
            onEnabledChanged = { isEnabled = it }
        )
    }
}

Im obigen Beispiel sind alle drei Teile besser gekapselt und weniger gekoppelt:

  • ImageWithEnabledOverlay muss nur wissen, wie der aktuelle isEnabled-Status ist. Es muss nicht wissen, dass ControlPanelWithToggle vorhanden ist oder wie es gesteuert werden kann.

  • ControlPanelWithToggle weiß nicht, dass ImageWithEnabledOverlay vorhanden ist. Es kann null, eine oder mehrere Möglichkeiten geben, wie isEnabled angezeigt wird, und ControlPanelWithToggle muss nicht geändert werden.

  • Für das übergeordnete Element spielt es keine Rolle, wie tief ImageWithEnabledOverlay oder ControlPanelWithToggle verschachtelt sind. Diese untergeordneten Elemente können Änderungen animieren, Inhalte austauschen oder Inhalte an andere untergeordnete Elemente weitergeben.

Dieses Muster wird als Inversion of Control bezeichnet. Weitere Informationen finden Sie in der CompositionLocal Dokumentation.

Änderungen der Bildschirmgröße verarbeiten

Verschiedene Ressourcen für verschiedene Fenstergrößen sind eine der wichtigsten Möglichkeiten, responsive View-Layouts zu erstellen. Qualifizierte Ressourcen sind zwar weiterhin eine Option für Layoutentscheidungen auf Bildschirmebene, aber mit Compose ist es viel einfacher, Layouts vollständig im Code mit normaler bedingter Logik zu ändern. Weitere Informationen finden Sie unter Fenstergrößen klassen verwenden.

Informationen zu den Techniken, die Compose zum Erstellen adaptiver UIs bietet, finden Sie unter Verschiedene Displaygrößen unterstützen.

Verschachteltes Scrollen mit Ansichten

Weitere Informationen zum Aktivieren der Interoperabilität für verschachteltes Scrollen zwischen scrollbaren Ansichtselementen und scrollbaren zusammensetzbaren Funktionen in beiden Richtungen finden Sie unter Interoperabilität für verschachteltes Scrollen.

Compose in RecyclerView

Zusammensetzbare Funktionen in RecyclerView sind seit Version 1.3.0-alpha02 von RecyclerView leistungsstark. Achten Sie darauf, dass Sie mindestens Version 1.3.0-alpha02 von RecyclerView verwenden, um diese Vorteile zu nutzen.

WindowInsets-Interoperabilität mit Ansichten

Möglicherweise müssen Sie die Standardeinsätze überschreiben, wenn Ihr Bildschirm sowohl Ansichten als auch Compose-Code in derselben Hierarchie enthält. In diesem Fall müssen Sie explizit angeben, welches Element die Einsätze verwenden und welches sie ignorieren soll.

Wenn Ihr äußerstes Layout beispielsweise ein Android-Ansichtslayout ist, sollten Sie die Einsätze im Ansichtssystem verwenden und sie für Compose ignorieren. Wenn Ihr äußerstes Layout stattdessen eine zusammensetzbare Funktion ist, sollten Sie die Einsätze in Compose verwenden und die zusammensetzbaren Funktionen AndroidView entsprechend auffüllen.

Standardmäßig verwendet jede ComposeView alle Einsätze auf der Verbrauchsebene WindowInsetsCompat. Wenn Sie dieses Standardverhalten ändern möchten, legen Sie ComposeView.consumeWindowInsets auf false fest.

Weitere Informationen finden Sie in der WindowInsets in Compose Dokumentation.