Weitere Hinweise

Die Migration von Views zu Compose ist zwar rein UI-bezogen, aber es gibt viele Dinge, die Sie beachten müssen, 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 ansichtenbasierte Apps sind drei Versionen von Material Design verfügbar:

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

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

  • Material Design 2 mit der Bibliothek Compose Material (androidx.compose.material.MaterialTheme)
  • Material Design 3 mit der Bibliothek Compose Material 3 (z.B. 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.) 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-Designthemen 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 zum Testen finden Sie unter Compose-Layout 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 Architecture ComponentsViewModel verwenden, können Sie von jedem Compose-Element aus auf eine ViewModel zugreifen, indem Sie die Funktion viewModel() aufrufen, wie unter Compose 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 für den Status

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 freigegebenen Status nach Möglichkeit in einer anderen Klasse zu kapseln, die den von beiden Plattformen verwendeten Best Practices für UDFs entspricht. Beispielsweise in einer ViewModel, die einen Stream der freigegebenen Daten für die Ausgabe von Datenaktualisierungen bereitstellt.

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 die Quelle der Wahrheit sein und 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 den Befehl SideEffect composable, 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.

Mit Ihrer Analysebibliothek können Sie beispielsweise Ihre Nutzergruppe segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten (in diesem Beispiel Nutzereigenschaften) anhängen. 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 änderbaren Status und die Ansichten aktualisieren, die diesen Status verwenden.

Im folgenden Beispiel enthält ein CustomViewGroup ein TextView und ein ComposeView mit einem TextField-Element. 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 Benutzeroberfläche migrieren

Wenn Sie nach und nach zu Compose migrieren, müssen Sie möglicherweise sowohl in 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 das von Ihnen erstellte Compose-Element in Ihrem Compose-Design in dem überschriebenen Content-Element, 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 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.

Priorität des Splittstatus aus der Präsentation

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 die Eigenschaft visibility, die angibt, ob es 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 können Sie dagegen ganz einfach mithilfe bedingter Logik in Kotlin völlig unterschiedliche Composeables 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. Wenn Sie den Status bei Bedarf hochladen können, sind auch Komponenten wiederverwendbarer, da die Zuweisung von Status 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 benutzerdefinierter View kann beispielsweise davon ausgehen, dass er 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 verknüpft: 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 den aktuellen isEnabled-Status kennen. Es muss nicht wissen, dass ControlPanelWithToggle existiert oder wie es gesteuert wird.

  • ControlPanelWithToggle weiß nicht, dass ImageWithEnabledOverlay existiert. isEnabled kann auf null, eine oder mehrere Arten angezeigt werden und ControlPanelWithToggle muss 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

Unterschiedliche Ressourcen für unterschiedliche 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, 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 Standard-Einzüge überschreiben, wenn sich auf Ihrem Bildschirm sowohl Ansichten als auch Compose-Code in derselben Hierarchie befinden. In diesem Fall müssen Sie angeben, welche der beiden Seiten die Einleger verwenden und welche sie ignorieren soll.

Wenn Ihr äußerstes Layout beispielsweise ein Android-View-Layout ist, sollten Sie die Einzüge im View-System verwenden und für Compose 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 von jedem ComposeView alle Inset-Assets auf der Verbrauchsebene WindowInsetsCompat verbraucht. Wenn Sie dieses Standardverhalten ändern möchten, setzen Sie ComposeView.consumeWindowInsets auf false.

Weitere Informationen finden Sie in der Dokumentation WindowInsets in Compose.