Weitere Hinweise

Die Migration von Views zu Compose ist zwar rein UI-bezogen, aber es gibt viele Dinge zu beachten, um eine sichere und inkrementelle Migration durchzuführen. Auf dieser Seite finden Sie einige Hinweise zur Migration Ihrer datenbankbasierten App zu Compose.

Design Ihrer App migrieren

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

Für ansichtsbasierte Apps sind drei Material-Versionen verfügbar:

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

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

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

Wir empfehlen die Verwendung der neuesten Version (Material 3), wenn das Designsystem Ihrer App dies zulässt. Es gibt Migrationsleitfäden sowohl für Views als auch für Compose:

Wenn Sie neue Bildschirme in Compose erstellen, müssen Sie unabhängig von der verwendeten Version von Material Design vor allen Compose-Elementen, die UI aus den Materialbibliotheken von Compose ausgeben, eine MaterialTheme anwenden. Die Materialkomponenten (Button, Text usw.) hängen davon ab, dass eine MaterialTheme vorhanden ist, 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 Systeme in Compose und XML-Designs zu Compose migrieren.

Wenn Sie die Navigationskomponente in Ihrer App verwenden, finden Sie weitere Informationen unter Navigation mit Compose – Interoperabilität und Jetpack Navigation zu Navigation Compose migrieren.

Benutzeroberfläche für die kombinierte Ansicht „Compose“ und „Views“ testen

Nachdem Sie Teile Ihrer App zu Compose migriert haben, ist es wichtig, sie zu testen, um sicherzustellen, dass keine Fehler aufgetreten sind.

Wenn eine Aktivität oder ein Fragment Compose verwendet, müssen Sie anstelle von ActivityScenarioRule createAndroidComposeRule verwenden. createAndroidComposeRule integriert ActivityScenarioRule in eine ComposeTestRule, mit der Sie Code gleichzeitig schreiben und ansehen 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 finden Sie unter Layout von „Compose“ testen. Informationen zur Interoperabilität mit UI-Test-Frameworks finden Sie unter Interoperabilität mit Espresso und Interoperabilität mit UiAutomator.

Compose in Ihre bestehende App-Architektur einbinden

Architekturmuster für unidirektionalen Datenfluss (UDF) funktionieren nahtlos mit Compose. Wenn in der App stattdessen andere Arten von Architekturmustern wie Model View Presenter (MVP) verwendet werden, empfehlen wir, diesen Teil der Benutzeroberfläche vor oder während der Einführung von Compose zu UDF zu migrieren.

ViewModel in der Eingabeleiste verwenden

Wenn Sie die Bibliothek der Architekturkomponenten ViewModel verwenden, können Sie über jede zusammensetzbare Funktion auf eine ViewModel zugreifen. Dazu rufen Sie die Funktion viewModel() auf, wie unter Erstellung und andere Bibliotheken erläutert.

Wenn Sie Compose verwenden, sollten Sie denselben ViewModel-Typ nicht in verschiedenen Composeables verwenden, da ViewModel-Elemente dem Gültigkeitsbereich des Ansichtslebenszyklus folgen. Der Umfang ist entweder die Hostaktivität, das Fragment oder das Navigationsdiagramm, wenn die Navigationsbibliothek verwendet wird.

Wenn die Composeables beispielsweise in einer Aktivität gehostet werden, gibt viewModel() immer dieselbe Instanz zurück, die erst gelöscht wird, wenn die Aktivität beendet ist. Im folgenden Beispiel wird derselbe Nutzer („user1“) zweimal begrüßt, da dieselbe GreetingViewModel-Instanz in allen Composeables unter der Hostaktivität wiederverwendet wird. Die erste erstellte ViewModel-Instanz wird in anderen Composeables 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 ViewModel-Elemente umfassen, haben Composables, die ein Ziel in einem Navigationsgraphen sind, eine andere Instanz der ViewModel. In diesem Fall ist ViewModel auf den Lebenszyklus des Ziels beschränkt und wird gelöscht, wenn das Ziel aus dem Backstack entfernt wird. Im folgenden Beispiel wird beim Aufrufen des Bildschirms Profil eine neue Instanz von GreetingViewModel erstellt.

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

„Source of Truth“

Wenn Sie Compose in einem Teil der Benutzeroberfläche verwenden, müssen Compose und der Systemcode der Ansicht möglicherweise Daten teilen. Wir empfehlen, diesen gemeinsamen Status nach Möglichkeit in einer anderen Klasse zu kapseln, die den von beiden Plattformen verwendeten Best Practices für UDFs entspricht, z. B. in einer ViewModel, die einen Stream der freigegebenen Daten zur Ausgabe von Datenaktualisierungen freigibt.

Das ist jedoch nicht immer möglich, wenn die freigegebenen Daten veränderbar sind oder eng mit einem UI-Element verknüpft sind. In diesem Fall muss ein System als "Source of Truth" dienen und dieses System muss alle Datenaktualisierungen an das andere System weitergeben. Als Faustregel gilt: Die „Source of Truth“ sollte dem Element gehören, das der Wurzel der UI-Hierarchie am nächsten ist.

Als „Source of Truth“ zusammenstellen

Verwenden Sie das Attribut SideEffectcomposable, um den Compose-Status in nicht Compose-Code zu veröffentlichen. In diesem Fall wird die maßgebliche Quelle in einem Composeable gespeichert, das Statusaktualisierungen sendet.

Ihre Analysebibliothek bietet Ihnen beispielsweise die Möglichkeit, die Nutzerpopulation zu segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten (in diesem Beispiel Nutzereigenschaften) hinzufügen. Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analysebibliothek senden möchten, aktualisieren Sie den Wert mit SideEffect.

@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.

System als „Source of Truth“ ansehen

Wenn das Ansichtssystem den Status besitzt und ihn für Compose freigibt, empfehlen wir, den Status in mutableStateOf-Objekten zu verpacken, um ihn für Compose threadsicher zu machen. Bei diesem Ansatz werden zusammensetzbare Funktionen vereinfacht, da sie nicht mehr die Referenzquelle haben. Das Ansichtssystem muss jedoch den mutable-Status und die Ansichten aktualisieren, die diesen Status verwenden.

Im folgenden Beispiel enthält ein CustomViewGroup einen TextView und einen ComposeView mit einer zusammensetzbaren TextField-Funktion. Die TextView muss den Inhalt anzeigen, den der Nutzer in die 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 sowohl im Compose- als auch im View-System gemeinsame UI-Elemente verwenden. Wenn Ihre App beispielsweise eine benutzerdefinierte CallToActionButton-Komponente hat, müssen Sie sie möglicherweise sowohl auf Compose- als auch auf datenbankbasierten Bildschirmen verwenden.

In Compose werden freigegebene UI-Elemente zu Composeables, die in der gesamten App wiederverwendet werden können, unabhängig davon, ob das Element mit XML gestylt wird oder eine benutzerdefinierte Ansicht ist. Sie können beispielsweise ein CallToActionButton-Element für Ihre benutzerdefinierte Button-Aufrufkomponente erstellen.

Wenn Sie das Composeable in ansichtsbasierten Bildschirmen verwenden möchten, erstellen Sie einen benutzerdefinierten Ansichts-Wrapper, der von AbstractComposeView ausgeht. Platzieren Sie die von Ihnen erstellte zusammensetzbare Funktion in der überschriebenen zusammensetzbaren Funktion Content, wie im folgenden Beispiel gezeigt, in Ihrem Entwurfsdesign:

@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 zusammensetzbaren Parameter werden in der benutzerdefinierten Ansicht zu veränderbaren Variablen. Dadurch ist die benutzerdefinierte CallToActionViewButton-Ansicht wie eine herkömmliche Ansicht skalierbar und nutzbar. Unten sehen Sie ein Beispiel für die Bindung an die Ansicht:

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änderbaren Status enthält, lesen Sie den Hilfeartikel Wahrheitsquelle für den Status.

Aufteilungsstatus gegenüber Präsentation priorisieren

Traditionell ist ein View zustandsorientiert. Mit einem View werden Felder verwaltet, die nicht nur beschreiben, was angezeigt werden soll, sondern auch 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 im Abschnitt State Hoisting erläutert.

Ein View hat beispielsweise eine visibility-Eigenschaft, die beschreibt, ob er sichtbar, unsichtbar oder verschwunden ist. Das ist eine inhärente Eigenschaft der View. Auch wenn andere Codeteile die Sichtbarkeit eines View ändern können, ist nur das View selbst wirklich darüber informiert, wie es aktuell sichtbar ist. Die Logik, die dafür sorgt, dass ein View sichtbar ist, kann fehleranfällig sein und ist oft mit dem View selbst verknüpft.

Mit „Compose“ lassen sich hingegen ganz einfach völlig unterschiedliche zusammensetzbare Funktionen mit bedingter Logik in Kotlin anzeigen:

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

CautionIcon muss nicht wissen, warum es angezeigt wird, und es gibt kein Konzept für visibility: Es ist entweder in der Komposition oder nicht.

Wenn Sie die Zustandsverwaltung und die Darstellungslogik klar voneinander trennen, können Sie die Darstellung von Inhalten als Umwandlung des Zustands in die Benutzeroberfläche flexibler ändern. Die Möglichkeit, bei Bedarf zu winden, macht zusammensetzbare Funktionen auch besser wiederverwendbar, da die Statusinhaberschaft flexibler ist.

Für gekapselte und wiederverwendbare Komponenten werben

View-Elemente wissen oft, wo sie sich befinden: in einer Activity, einer Dialog, einer Fragment oder irgendwo in einer anderen View-Hierarchie. Da sie oft aus statischen Layoutdateien erstellt werden, ist die Gesamtstruktur einer View in der Regel sehr starr. Dies führt zu einer engeren Kopplung und erschwert die Änderung oder Wiederverwendung eines View.

Ein benutzerdefiniertes View kann beispielsweise davon ausgehen, dass es eine untergeordnete Ansicht eines bestimmten Typs mit einer bestimmten ID hat, und seine Eigenschaften direkt als Reaktion auf eine Aktion ändern. Dadurch sind diese View-Elemente eng miteinander verbunden: Das benutzerdefinierte View kann abstürzen oder beschädigt werden, wenn es das untergeordnete Element nicht findet, und das untergeordnete Element kann wahrscheinlich nicht ohne das übergeordnete benutzerdefinierte View wiederverwendet werden.

Mit wiederverwendbaren Compose-Elementen ist das in Compose weniger ein Problem. Übergeordnete Elemente können Status und Rückrufe ganz einfach angeben. So können Sie wiederverwendbare Composeables schreiben, 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 stärker gekapselt und weniger gekoppelt:

  • ImageWithEnabledOverlay muss nur wissen, was der aktuelle isEnabled-Status ist. Es muss nicht wissen, dass ControlPanelWithToggle existiert oder wie es gesteuert wird.

  • ControlPanelWithToggle weiß nicht, dass ImageWithEnabledOverlay existiert. Es kann null, eine oder mehrere Möglichkeiten für die Anzeige von isEnabled geben und ControlPanelWithToggle müsste sich nicht ändern.

  • Für das übergeordnete Element spielt es keine Rolle, wie tief ImageWithEnabledOverlay oder ControlPanelWithToggle verschachtelt sind. Diese Kinder könnten Änderungen animieren, Inhalte austauschen oder Inhalte an andere Kinder 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 unterschiedliche Fenstergrößen sind eine der wichtigsten Methoden zum Erstellen responsiver View-Layouts. Qualifizierte Ressourcen sind zwar weiterhin eine Option für Layoutentscheidungen auf Bildschirmebene, mit Compose ist es jedoch viel einfacher, Layouts vollständig in Code mit normaler bedingter Logik zu ändern. Weitere Informationen finden Sie unter Fenstergrößenklassen verwenden.

Weitere Informationen zu den Techniken, die Compose für die Erstellung adaptiver Benutzeroberflächen bietet, finden Sie unter Unterstützung verschiedener Bildschirmgrößen.

Verschachteltes Scrollen mit Ansichten

Weitere Informationen zum Aktivieren der Interoperabilität für verschachtelte Scrollfunktionen zwischen scrollbaren Ansichtselementen und scrollbaren Composeables, die in beide Richtungen verschachtelt sind, finden Sie unter Interoperabilität für verschachtelte Scrollfunktionen.

In RecyclerView schreiben

Composables in RecyclerView sind seit RecyclerView Version 1.3.0-alpha02 leistungsstark. Sie benötigen mindestens Version 1.3.0-alpha02 von RecyclerView, um diese Vorteile nutzen zu können.

WindowInsets Interoperabilität mit Ansichten

Möglicherweise müssen Sie die Standardeinfügungen überschreiben, wenn auf Ihrem Bildschirm sowohl Aufruf- als auch Editor-Code in derselben Hierarchie vorhanden sind. In diesem Fall müssen Sie angeben, welche der beiden Seiten die Einleger verwenden und welche sie ignorieren soll.

Wenn das äußerste Layout beispielsweise ein Android View-Layout ist, sollten Sie die Einfügungen im View-System übernehmen und beim Schreiben ignorieren. Wenn Ihr äußerstes Layout ein Composeable ist, sollten Sie die Einzüge in Compose verwenden und die AndroidView-Composeables entsprechend ausrichten.

Standardmäßig werden für jede ComposeView alle Inset-Assets auf der Verbrauchsebene WindowInsetsCompat verwendet. Wenn Sie dieses Standardverhalten ändern möchten, setzen Sie ComposeView.consumeWindowInsets auf false.

Weitere Informationen finden Sie in der Dokumentation WindowInsets in Compose.