Weitere Hinweise

Die Migration von Views zu Compose ist zwar rein UI-bezogen, es gibt aber viele Dinge, die bei einer sicheren und inkrementellen Migration berücksichtigt werden müssen. Auf dieser Seite finden Sie einige Überlegungen zur Migration Ihrer View-basierten App zu Compose.

App-Design migrieren

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

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

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

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

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

Wir empfehlen, die aktuelle Version (Material 3) zu verwenden, wenn das Designsystem Ihrer App dies zulässt. Es sind Migrationsleitfäden für Views und Compose verfügbar:

Wenn Sie neue Bildschirme in Compose erstellen, müssen Sie unabhängig davon, welche Version von Material Design Sie verwenden, vor allen Composables, die UI aus den Compose Material-Bibliotheken ausgeben, ein MaterialTheme anwenden. 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-Themes 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 Compose-/Views-UI testen

Nachdem Sie Teile Ihrer App zu Compose migriert haben, ist es wichtig, sie zu testen, um sicherzugehen, dass nichts beschädigt wurde.

Wenn eine Aktivität oder ein Fragment Compose verwendet, müssen Sie createAndroidComposeRule anstelle von ActivityScenarioRule verwenden. createAndroidComposeRule integriert ActivityScenarioRule mit einem ComposeTestRule, mit dem Sie Compose- und View-Code 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 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 (Unidirectional Data Flow, UDF) funktionieren nahtlos mit Compose. Wenn die App stattdessen andere Architekturmuster wie Model View Presenter (MVP) verwendet, empfehlen wir, diesen Teil der Benutzeroberfläche vor oder während der Einführung von Compose zu UDF zu migrieren.

ViewModel in Compose verwenden

Wenn Sie die Architecture Components-Bibliothek ViewModel verwenden, können Sie über die Funktion viewModel() auf ein ViewModel aus jedem Composable zugreifen, wie unter Compose und andere Bibliotheken beschrieben.

Wenn Sie Compose verwenden, sollten Sie darauf achten, nicht denselben ViewModel-Typ in verschiedenen Composables zu verwenden, da ViewModel-Elemente View-Lifecycle-Bereichen folgen. Der Bereich ist entweder die Host-Aktivität, das Fragment oder der Navigationsgraph, wenn die Navigationsbibliothek verwendet wird.

Wenn die Composables 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 Composables unter der Hostaktivität wiederverwendet wird. Die erste erstellte ViewModel-Instanz wird in anderen Composables 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 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. 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 sich Compose und der View-Systemcode möglicherweise Daten teilen. Wenn möglich, empfehlen wir, diesen gemeinsamen Status in einer anderen Klasse zu kapseln, die den UDF-Best Practices beider Plattformen entspricht, z. B. in einem ViewModel, das einen Stream der freigegebenen Daten bereitstellt, um Datenaktualisierungen auszugeben.

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

Compose als „Source of Truth“

Mit der SideEffect-Composable können Sie den Compose-Status in Nicht-Compose-Code veröffentlichen. In diesem Fall wird die Quelle der Wahrheit in einem zusammensetzbaren Element beibehalten, das Statusaktualisierungen sendet.

Mit Ihrer Analysebibliothek können Sie Ihre Nutzer beispielsweise segmentieren, indem Sie allen nachfolgenden Analyseereignissen benutzerdefinierte Metadaten (in diesem Beispiel Nutzereigenschaften) anhängen. Wenn Sie den Nutzertyp des aktuellen Nutzers an Ihre Analysenbibliothek übermitteln 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.

System als „Source of Truth“ ansehen

Wenn das View-System den Status besitzt und ihn mit Compose teilt, empfehlen wir, den Status in mutableStateOf-Objekte einzuschließen, damit er für Compose threadsicher ist. Wenn Sie diesen Ansatz verwenden, werden zusammensetzbare Funktionen vereinfacht, da sie nicht mehr die Quelle der Wahrheit sind. Das View-System muss jedoch den veränderlichen Status und die Views, die diesen Status verwenden, aktualisieren.

Im folgenden Beispiel enthält ein CustomViewGroup ein TextView und ein ComposeView mit einem TextField-Composable. Im TextView muss der Inhalt des Textes zu sehen sein, 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
    }
}

Gemeinsame Benutzeroberfläche migrieren

Wenn Sie nach und nach zu Compose migrieren, müssen Sie möglicherweise gemeinsame UI-Elemente sowohl in Compose als auch im View-System verwenden. Wenn Ihre App beispielsweise eine benutzerdefinierte CallToActionButton-Komponente hat, müssen Sie sie möglicherweise sowohl in Compose- als auch in View-basierten Bildschirmen verwenden.

In Compose werden freigegebene UI-Elemente zu Composables, 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 ein CallToActionButton-Composable für Ihre benutzerdefinierte Call-to-Action-Komponente Button.

Wenn Sie das Composable in bildschirmbasierten Ansichten verwenden möchten, erstellen Sie einen benutzerdefinierten Ansichtsumbruch, der von AbstractComposeView abgeleitet wird. Platzieren Sie die erstellte Composable in der überschriebenen Content-Composable, die in Ihrem Compose-Theme umschlossen ist, wie im Beispiel unten 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)
        }
    }
}

Beachten Sie, dass die zusammensetzbaren Parameter innerhalb der benutzerdefinierten Ansicht zu veränderlichen Variablen werden. Dadurch wird die benutzerdefinierte CallToActionViewButton-Ansicht aufgeblasen und kann wie eine herkömmliche Ansicht verwendet werden. Hier finden Sie ein Beispiel für 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, lesen Sie den Abschnitt State source of truth.

Zustand und Darstellung trennen

Traditionell ist ein View zustandsorientiert. Ein View verwaltet Felder, die beschreiben, was angezeigt werden soll, zusätzlich dazu, wie es angezeigt werden soll. Wenn Sie ein View in Compose umwandeln, sollten Sie die gerenderten Daten trennen, um einen unidirektionalen Datenfluss zu erreichen. Weitere Informationen finden Sie unter State Hoisting.

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

Mit Compose ist es dagegen ganz einfach, mit bedingter Logik in Kotlin völlig unterschiedliche Composables anzuzeigen:

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

CautionIcon muss nicht wissen oder sich darum kümmern, warum es angezeigt wird. Es gibt kein Konzept von visibility: Es ist entweder in der Komposition enthalten oder nicht.

Durch die saubere Trennung von Statusverwaltung und Darstellungslogik können Sie freier ändern, wie Inhalte als Umwandlung von Status in die Benutzeroberfläche angezeigt werden. Wenn Sie den Status bei Bedarf nach oben verschieben können, sind Composables auch besser wiederverwendbar, da die Statusinhaberschaft flexibler ist.

Gekapselte und wiederverwendbare Komponenten fördern

View-Elemente haben oft eine Vorstellung davon, wo sie sich befinden: in einem Activity, einem Dialog, einem Fragment oder irgendwo in einer anderen View-Hierarchie. Da sie häufig aus statischen Layoutdateien erstellt werden, ist die Gesamtstruktur eines 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 werden diese View-Elemente eng miteinander verknüpft: Das benutzerdefinierte View kann abstürzen oder beschädigt werden, wenn das untergeordnete Element nicht gefunden wird, und das untergeordnete Element kann wahrscheinlich nicht ohne das benutzerdefinierte View-Übergeordnete Element wiederverwendet werden.

Bei der Verwendung von wiederverwendbaren Composables in Compose ist das weniger ein Problem. Eltern können ganz einfach Status und Callbacks angeben, sodass Sie wiederverwendbare Composables schreiben können, ohne genau 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 muss nur den aktuellen isEnabled-Status kennen. Es muss nicht wissen, dass ControlPanelWithToggle vorhanden ist oder wie es gesteuert werden kann.

  • ControlPanelWithToggle weiß nicht, dass ImageWithEnabledOverlay existiert. isEnabled kann auf null, eine oder mehrere Arten dargestellt 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 dazu finden Sie in der CompositionLocal-Dokumentation.

Änderungen der Displaygröß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, mit Compose ist es jedoch viel einfacher, Layouts vollständig im Code mit normaler bedingter Logik zu ändern. Weitere Informationen finden Sie unter Klassen für Fenstergrößen verwenden.

Weitere Informationen zu den Techniken, die Compose zum Erstellen adaptiver UIs bietet, findest du hier.

Verschachteltes Scrollen mit Views

Weitere Informationen zum Aktivieren der Interoperabilität von verschachteltem Scrollen zwischen scrollbaren View-Elementen und scrollbaren Composables, die in beide Richtungen verschachtelt sind, finden Sie unter Interoperabilität von verschachteltem Scrollen.

E‑Mail in RecyclerView schreiben

Composables in RecyclerView sind seit RecyclerView-Version 1.3.0-alpha02 leistungsstark. Sie müssen mindestens Version 1.3.0-alpha02 von RecyclerView verwenden, um diese Vorteile nutzen zu können.

WindowInsets Interop mit Views

Möglicherweise müssen Sie die Standard-Insets überschreiben, wenn Ihr Bildschirm sowohl Views als auch Compose-Code in derselben Hierarchie enthält. In diesem Fall müssen Sie explizit angeben, welche Ansicht die Insets verwenden und welche sie ignorieren soll.

Wenn Ihr äußerstes Layout beispielsweise ein Android-View-Layout ist, sollten Sie die Insets im View-System verwenden und sie für Compose ignorieren. Wenn Ihr äußerstes Layout ein Composable ist, sollten Sie die Insets in Compose verwenden und die AndroidView-Composables entsprechend auffüllen.

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

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