Weitere Aspekte

Die Migration von Views zu Compose ist rein UI-bezogen. Es gibt jedoch viele Dinge zu berücksichtigen, um eine sichere und inkrementelle Migration durchzuführen. Auf dieser Seite finden Sie einige Überlegungen zur Migration Ihrer View-basierten Anwendung zu Compose.

Design Ihrer App migrieren

Material Design ist das empfohlene Designsystem für das Erstellen 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 mithilfe der Bibliothek MDC-Android (z.B. 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 Compose Material-Bibliothek (z.B. androidx.compose.material.MaterialTheme)
  • Material Design 3 mit der Bibliothek Compose Material 3 (z.B. androidx.compose.material3.MaterialTheme)

Wir empfehlen, die neueste Version (Material 3) zu verwenden, wenn das Designsystem Ihrer App dazu in der Lage ist. Für „View“ und „Compose“ gibt es Migrationsleitfäden:

Wenn Sie neue Bildschirme in Compose erstellen, müssen Sie unabhängig von der verwendeten Version von Material Design vor allen zusammensetzbaren Funktionen, die Benutzeroberflächen aus den Compose Material-Bibliotheken ausgeben, ein MaterialTheme anwenden. Die Materialkomponenten (Button, Text usw.) hängen davon ab, dass eine MaterialTheme vorhanden ist, und ihr Verhalten ist ohne dieses nicht definiert.

Für alle Jetpack Compose-Beispiele wird ein benutzerdefiniertes Design für das Schreiben verwendet, das auf MaterialTheme aufbaut.

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 Mit Compose navigieren – Interoperabilität und Jetpack Navigation zu Navigation Compose migrieren.

Gemischte Benutzeroberfläche zum Schreiben und Ansehen testen

Nachdem Sie Teile Ihrer App zu Compose migriert haben, ist es wichtig, zu testen, ob alles beschädigt ist.

Wenn für eine Aktivität oder ein Fragment „Compose“ verwendet wird, müssen Sie createAndroidComposeRule anstelle von ActivityScenarioRule verwenden. createAndroidComposeRule bindet ActivityScenarioRule in eine ComposeTestRule ein, mit der Sie Code gleichzeitig verfassen und aufrufen 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 vorhandene Anwendungsarchitektur einbinden

UDF-Architekturmuster (Unidirektionale Datenfluss) funktionieren nahtlos mit Compose. Wenn die Anwendung stattdessen andere Arten von Architekturmustern verwendet, z. B. Model View Presenter (MVP), empfehlen wir, diesen Teil der UI vor oder während der Einführung von Compose zu UDFs zu migrieren.

ViewModel in „Compose“ 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.

Achten Sie bei der Einführung von „Compose“ darauf, denselben ViewModel-Typ in verschiedenen zusammensetzbaren Funktionen zu verwenden, da ViewModel-Elemente den Bereichen des Ansichtslebenszyklus folgen. Der Bereich ist entweder die Hostaktivität, das Fragment oder das Navigationsdiagramm, falls die Navigationsbibliothek verwendet wird.

Wenn die zusammensetzbaren Funktionen beispielsweise in einer Aktivität gehostet werden, gibt viewModel() immer dieselbe Instanz zurück, die erst nach Abschluss der Aktivität gelöscht wird. Im folgenden Beispiel wird derselbe Nutzer („user1“) zweimal begrüßt, weil dieselbe GreetingViewModel-Instanz in allen zusammensetzbaren Funktionen unter der Hostaktivität wiederverwendet wird. Die erste erstellte Instanz ViewModel 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 Navigationsdiagramme auch ViewModel-Elemente umfassen, haben zusammensetzbare Funktionen, die ein Ziel in einem Navigationsdiagramm sind, eine andere Instanz von ViewModel. In diesem Fall bezieht sich der ViewModel auf den Lebenszyklus des Ziels 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 den Bildschirm Profil aufruft.

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

„Source of Truth“

Wenn Sie die Funktion „Compose“ in einem Teil der Benutzeroberfläche verwenden, müssen Daten möglicherweise über Compose und den Systemcode für die Ansicht freigegeben werden. 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.

Dies ist jedoch nicht immer möglich, wenn die freizugebenden Daten änderbar oder eng an ein UI-Element gebunden sind. In diesem Fall muss ein System die "Source of Truth" sein und dieses System muss alle Datenaktualisierungen an das andere System weitergeben. Als Faustregel gilt: Die „Source of Truth“ sollte dem Element gehören, das sich näher am Stamm der UI-Hierarchie befindet.

Als „Source of Truth“ verfassen

Verwenden Sie die zusammensetzbare Funktion SideEffect, um den Erstellungsstatus in Nicht-Compose-Code zu veröffentlichen. In diesem Fall wird die zentrale Informationsquelle in einer zusammensetzbaren Funktion gespeichert, die 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. Verwenden Sie SideEffect, um den Wert des aktuellen Nutzers zu aktualisieren und der Analysebibliothek den Nutzertyp des aktuellen Nutzers mitzuteilen.

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

System als zentrale Datenquelle betrachten

Wenn das View-System Inhaber des Zustands ist und ihn für Compose freigeben, empfehlen wir, den Status in mutableStateOf-Objekte zu fassen, um ihn für Compose threadsicher zu machen. Bei diesem Ansatz werden zusammensetzbare Funktionen vereinfacht, da sie nicht mehr über die "Source of Truth" verfügen. Das View-System muss jedoch den änderbaren 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. TextView muss den Inhalt dessen anzeigen, was 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 Anwendung beispielsweise eine benutzerdefinierte CallToActionButton-Komponente hat, müssen Sie sie möglicherweise sowohl auf dem Bildschirm zum Schreiben als auch auf dem Bildschirm zum Erstellen einer Ansicht verwenden.

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

Wenn Sie die zusammensetzbare Funktion in ansichtsbasierten Bildschirmen verwenden möchten, müssen Sie einen benutzerdefinierten Ansichts-Wrapper erstellen, der von AbstractComposeView erweitert wird. 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 innerhalb der benutzerdefinierten Ansicht zu änderbaren Variablen. Dadurch ist die benutzerdefinierte Ansicht CallToActionViewButton wie eine herkömmliche Ansicht aufblasbar und nutzbar. Unten sehen Sie ein Beispiel dafür mit View Binding:

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, finden Sie weitere Informationen unter State Source of Truth.

Aufteilungsstatus gegenüber Präsentation priorisieren

Traditionell ist ein View zustandsorientiert. Ein View verwaltet neben der Anzeige auch Felder, die beschreiben, was angezeigt werden soll. Wenn Sie ein View in „Compose“ konvertieren, sollten Sie die gerenderten Daten trennen, um einen unidirektionalen Datenfluss zu erreichen. Dies wird unter State Hoisting näher erläutert.

Ein View hat beispielsweise eine visibility-Eigenschaft, die beschreibt, ob er sichtbar, unsichtbar oder verschwunden ist. Dies ist eine inhärente Eigenschaft von View. Während andere Codeteile die Sichtbarkeit einer View ändern können, kennt nur das View selbst die aktuelle Sichtbarkeit. Die Logik, die gewährleistet, dass eine View sichtbar ist, kann fehleranfällig sein und ist oft mit der 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 oder sich darum kümmern, warum es angezeigt wird, und es gibt auch kein Konzept von visibility: Entweder ist es in der Komposition enthalten oder nicht.

Wenn Sie die Statusverwaltung und die Präsentationslogik klar voneinander trennen, können Sie die Anzeige von Inhalten als Konvertierung des Zustands in die Benutzeroberfläche freier ändern. Die Möglichkeit, bei Bedarf zu winden, macht zusammensetzbare Funktionen auch besser wiederverwendbar, da die Statusinhaberschaft flexibler ist.

Gekapselte und wiederverwendbare Komponenten hochstufen

View-Elemente haben oft eine Vorstellung davon, wo sie sich befinden: innerhalb einer Activity-, Dialog-, Fragment- oder irgendwo innerhalb einer anderen View-Hierarchie. Da sie häufig von statischen Layoutdateien überhöht werden, ist die Gesamtstruktur einer View tendenziell sehr starr. Dies führt zu einer engeren Kopplung und erschwert die Änderung oder Wiederverwendung einer View.

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

Dies ist in der Funktion „Mit wiederverwendbaren zusammensetzbaren Funktionen verfassen“ weniger problematisch. Übergeordnete Elemente können ganz einfach Status und Callbacks angeben, sodass Sie wiederverwendbare zusammensetzbare Funktionen schreiben können, ohne wissen zu müssen, 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 benötigt nur Informationen zum aktuellen isEnabled-Status. Es muss nicht wissen, dass ControlPanelWithToggle existiert oder wie er steuerbar ist.

  • 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 verschachtelt ImageWithEnabledOverlay oder ControlPanelWithToggle sind. z. B. durch Animation, Austausch von Inhalten oder Weitergabe von Inhalten an andere Kinder.

Dieses Muster wird als Inversion der Kontrolle bezeichnet. Weitere Informationen dazu finden Sie in der CompositionLocal-Dokumentation.

Umgang mit Änderungen der Bildschirmgröße

Verschiedene Ressourcen für unterschiedliche Fenstergrößen sind eine der wichtigsten Methoden zum Erstellen responsiver View-Layouts. Während qualifizierte Ressourcen weiterhin eine Option für Layoutentscheidungen auf Bildschirmebene sind, vereinfacht „Compose“ das vollständige Ändern von Layouts im Code mit normaler bedingter Logik. Weitere Informationen finden Sie unter Fenstergrößenklassen.

Weitere Informationen zu den Techniken, die Compose zum Erstellen adaptiver UIs bietet, finden Sie unter Unterstützung verschiedener Bildschirmgrößen.

Verschachteltes Scrollen mit Ansichten

Weitere Informationen zum Aktivieren der verschachtelten Scrolling-Interoperabilität zwischen scrollbaren View-Elementen und scrollbaren zusammensetzbaren Funktionen, die in beide Richtungen verschachtelt sind, finden Sie unter Verschachtelte Scroll-Interoperabilität.

In RecyclerView schreiben

Zusammensetzbare Funktionen in RecyclerView sind seit RecyclerView-Version 1.3.0-alpha02 leistungsfähig. Sie benötigen mindestens Version 1.3.0-alpha02 von RecyclerView, um diese Vorteile zu sehen.

WindowInsets-Interoperabilität mit Ansichten

Möglicherweise müssen Sie die Standardeinfügungen überschreiben, wenn auf Ihrem Bildschirm in derselben Hierarchie Code für Ansichten und Schreiben vorhanden ist. In diesem Fall müssen Sie explizit angeben, in welchem Bereich die Insets aufgenommen und welche ignoriert werden sollen.

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 das äußerste Layout eine zusammensetzbare Funktion ist, sollten Sie alternativ die Insets in Compose verwenden und die zusammensetzbaren AndroidView-Elemente entsprechend auffüllen.

Standardmäßig verbraucht jeder ComposeView alle Einfügungen auf der Verbrauchsebene WindowInsetsCompat. Wenn Sie dieses Standardverhalten ändern möchten, setzen Sie ComposeView.consumeWindowInsets auf false.

Weitere Informationen finden Sie unter WindowInsets in Compose.