Weitere Aspekte

Auch wenn die Migration von Views zu Compose nur die UI betrifft, müssen bei einer sicheren und inkrementellen Migration viele Aspekte berücksichtigt werden. Auf dieser Seite finden Sie einige Überlegungen zur Migration Ihrer View-basierten Anwendung zu Compose.

Anwendungsdesign migrieren

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

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

  • Material Design 1 mithilfe 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:

Wir empfehlen, die neueste Version (Material 3) zu verwenden, wenn das Designsystem Ihrer App dies in der Lage ist. Für Ansichten und für die Funktion „Compose“ sind Migrationsleitfäden verfügbar:

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

In allen Jetpack Compose-Beispielen wird ein benutzerdefiniertes Compose-Design verwendet, das auf MaterialTheme basiert.

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

Wenn Sie die Navigationskomponente in Ihrer Anwendung verwenden, finden Sie weitere Informationen unter Navigation mit Compose – Interoperability und Migration von Jetpack-Navigation zu Navigation Compose.

UI für gemischte Erstellungen/Ansichten testen

Nachdem Sie Teile Ihrer Anwendung zu Compose migriert haben, sollten Sie unbedingt Tests durchführen, um sicherzustellen, dass nichts fehlerhaft ist.

Wenn für eine Aktivität oder ein Fragment die Funktion „Compose“ verwendet wird, müssen Sie createAndroidComposeRule anstelle von ActivityScenarioRule verwenden. createAndroidComposeRule bindet ActivityScenarioRule in eine ComposeTestRule ein, mit der Sie Code zum Schreiben und Ansehen 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 finden Sie unter Layout testen. Informationen zur Interoperabilität mit UI-Test-Frameworks finden Sie unter Interoperabilität mit Espresso und Interoperabilität mit UiAutomator.

Compose in eine vorhandene Anwendungsarchitektur einbinden

Architekturmuster für den unidirektionalen Datenfluss (UDF) funktionieren nahtlos mit Compose. Wenn die Anwendung stattdessen andere Typen von Architekturmustern wie Model View Presenter (MVP) verwendet, empfehlen wir, diesen Teil der UI vor oder während der Einführung von Compose zu UDF zu migrieren.

ViewModel in „Compose“ verwenden

Wenn Sie die ViewModel-Bibliothek für Architekturkomponenten verwenden, können Sie von jeder zusammensetzbaren Funktion auf ein ViewModel zugreifen. Dazu rufen Sie die Funktion viewModel() auf, wie unter Compose und andere Bibliotheken beschrieben.

Achten Sie beim Verwenden von „Compose“ darauf, denselben ViewModel-Typ in verschiedenen zusammensetzbaren Funktionen zu verwenden, da ViewModel-Elemente den Bereichen des View-Lebenszyklus folgen. Der Bereich ist entweder die Hostaktivität, das Fragment oder das Navigationsdiagramm, 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 abgeschlossen ist. 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 Navigationsdiagramme auch ViewModel-Elemente umfassen, haben zusammensetzbare Funktionen, die ein Ziel in einem Navigationsdiagramm sind, eine andere Instanz von 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. Wenn der Nutzer im folgenden Beispiel den Bildschirm Profil aufruft, wird eine neue Instanz von GreetingViewModel erstellt.

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

Staatliche Informationsquelle

Wenn Sie Compose in einem Teil der Benutzeroberfläche verwenden, ist es möglich, dass Compose und der Systemcode „View“ Daten freigeben müssen. Kapseln Sie diesen gemeinsamen Status nach Möglichkeit in einer anderen Klasse, die den Best Practices für UDFs entspricht, die von beiden Plattformen verwendet werden. Verwenden Sie beispielsweise eine ViewModel, die einen Stream der freigegebenen Daten zur Ausgabe von Datenaktualisierungen zur Verfügung stellt.

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: Als Datenquelle sollte das Element dienen, das sich näher am Stamm der UI-Hierarchie befindet.

Das Verfassen als Quelle der Wahrheit

Verwenden Sie die zusammensetzbare Funktion SideEffect, um den Erstellungsstatus in Nicht-Compose-Code zu veröffentlichen. In diesem Fall wird die Datenquelle in einer zusammensetzbaren Funktion gespeichert, die Statusaktualisierungen sendet.

In der Analysebibliothek können Sie beispielsweise die Nutzerpopulation segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten (in diesem Beispiel Nutzereigenschaften) hinzufügen. Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analysebibliothek kommunizieren möchten, verwenden Sie SideEffect, um seinen 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.

Das System als zentrale Informationsquelle ansehen

Wenn das Ansichtssystem Inhaber des Status ist und ihn für „Compose“ freigibt, empfehlen wir, den Status in mutableStateOf-Objekte zu verpacken, damit er Thread-sicher für Composer ist. Wenn Sie diesen Ansatz verwenden, werden zusammensetzbare Funktionen vereinfacht, da sie nicht mehr die „Source of Truth“ haben. Das View-System muss jedoch den änderbaren Status und die Ansichten, die diesen Status verwenden, aktualisieren.

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

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

Wenn Sie die zusammensetzbare Funktion in ansichtsbasierten Bildschirmen verwenden möchten, müssen Sie einen benutzerdefinierten Ansichts-Wrapper erstellen, der von AbstractComposeView erweitert wird. Legen Sie in der überschriebenen zusammensetzbaren Funktion Content die erstellte zusammensetzbare Funktion in Ihr Design „Compose“ ein, 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 zusammensetzbaren Parameter werden in der benutzerdefinierten Ansicht zu änderbaren Variablen. Dadurch wird die benutzerdefinierte Ansicht CallToActionViewButton aufblasbar und nutzbar, ähnlich wie eine herkömmliche Ansicht. Im Folgenden finden Sie ein Beispiel hierfür mit der Ansichtsbindung:

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

Enthält die benutzerdefinierte Komponente einen änderbaren Status, finden Sie weitere Informationen unter State Source of Truth.

Aufteilungsstatus der Präsentation priorisieren

Traditionell ist ein View zustandsorientiert. Ein View verwaltet neben der Anzeige auch Felder, die beschreiben, was angezeigt werden soll. Wenn Sie View in Compose konvertieren, müssen die gerenderten Daten getrennt werden, um einen unidirektionalen Datenfluss zu erzielen, wie unter Zustandswinding näher erläutert.

Eine View hat beispielsweise eine visibility-Eigenschaft, die beschreibt, ob sie sichtbar, unsichtbar oder nicht mehr ist. Dies ist eine inhärente Eigenschaft von View. Während andere Codeelemente die Sichtbarkeit einer View ändern können, weiß nur die View selbst wirklich, welche aktuelle Sichtbarkeit vorliegt. Die Logik, um dafür zu sorgen, dass eine View sichtbar ist, kann fehleranfällig sein und ist häufig an das View selbst gebunden.

Im Gegensatz dazu vereinfacht Compose mithilfe von bedingter Logik komplett verschiedene zusammensetzbare Funktionen in Kotlin:

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

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

Durch eine saubere Trennung von Statusverwaltung und Darstellungslogik können Sie die Anzeige von Inhalten als Konvertierung des Zustands in die UI freier anpassen. Die Möglichkeit, bei Bedarf Winden zu steuern, macht zusammensetzbare Funktionen auch wiederverwendbar, da die Zuständigkeit für den Zustand 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 aus statischen Layoutdateien überlastet werden, ist die Gesamtstruktur einer View in der Regel sehr starr. Dies führt zu einer engeren Kopplung und erschwert die Änderung oder Wiederverwendung einer View.

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

Dies ist bei wiederverwendbaren zusammensetzbaren Funktionen in der Funktion „Schreiben“ weniger ein Problem. Übergeordnete Elemente können einfach Status und Callbacks 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 stärker gekapselt und weniger gekoppelt:

  • ImageWithEnabledOverlay braucht nur den aktuellen isEnabled-Status zu kennen. Es ist nicht nötig zu wissen, ob ControlPanelWithToggle existiert oder ob er steuerbar ist.

  • ControlPanelWithToggle weiß nicht, dass ImageWithEnabledOverlay existiert. Es kann null, eine oder mehrere Möglichkeiten geben, wie isEnabled angezeigt wird und ControlPanelWithToggle muss sich nicht ändern.

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

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

Änderungen der Bildschirmgröße verarbeiten

Unterschiedliche Ressourcen für unterschiedliche Fenstergrößen sind eine der wichtigsten Möglichkeiten zum Erstellen responsiver View-Layouts. Qualifizierte Ressourcen sind zwar weiterhin eine Option für Layoutentscheidungen auf Bildschirmebene, mit der Funktion „Compose“ ist es jedoch viel einfacher, Layouts vollständig im Code mit normaler bedingter Logik zu ändern. Weitere Informationen finden Sie unter Unterstützung verschiedener Bildschirmgrößen.

Unter Adaptive Layouts erstellen erfahren Sie außerdem, wie Sie in der Funktion „Compose“ adaptive UIs erstellen.

Verschachteltes Scrollen mit Ansichten

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

In RecyclerView verfassen

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

WindowInsets-Interoperabilität mit Datenansichten

Möglicherweise müssen Sie Standardeinfügungen überschreiben, wenn sich auf Ihrem Bildschirm sowohl Ansichten als auch Code in einer Hierarchie befinden. In diesem Fall müssen Sie genau angeben, in welcher Version die Einfügungen aufgenommen und in welchen ignoriert werden sollen.

Wenn Ihr äußerstes Layout beispielsweise ein Android View-Layout ist, sollten Sie die Einfügungen im View-System verwenden und für Compose ignorieren. Wenn Ihr äußerstes Layout eine zusammensetzbare Funktion ist, sollten Sie die Einfügungen in Compose verwenden und die AndroidView-zusammensetzbaren Funktionen entsprechend auffüllen.

Standardmäßig nutzt jeder ComposeView alle Einsätze auf der Verbrauchsebene WindowInsetsCompat. Wenn Sie dieses Standardverhalten ändern möchten, setzen Sie ComposeView.consumeWindowInsets auf false.

Weitere Informationen finden Sie in der Dokumentation zu WindowInsets in Compose.