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 Design 3 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:
- Material 1 bis Material 2 in Ansichten
- Material 2 zu Material 3 in Ansichten
- Material 2 zu Material 3 in Compose
Wenn Sie neue Bildschirme in Compose erstellen, müssen Sie unabhängig davon, welche Version von Material Design Sie verwenden, ein MaterialTheme vor allen Composables anwenden, die UI aus den Compose Material-Bibliotheken ausgeben. Die Material-Komponenten (Button, Text usw.) sind von einem MaterialTheme abhängig und ihr Verhalten ist ohne diesen nicht definiert.
Alle Jetpack Compose-Beispiele verwenden ein benutzerdefiniertes Compose-Theme, das auf MaterialTheme basiert.
Weitere Informationen finden Sie unter Designsysteme in Compose und XML-Themes zu Compose migrieren.
Navigation
Wenn Sie die Navigationskomponente in Ihrer App verwenden, finden Sie weitere Informationen unter Von Compose in einer fragmentbasierten App navigieren 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 in einer Aktivität oder einem Fragment Compose verwendet wird, 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 den 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.
Achten Sie bei der Einführung von Compose darauf, 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 Host-Aktivitä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") ?: "") } } }
Zentrale Datenquelle 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 freigegebenen Status in einer anderen Klasse zu kapseln, die den UDF-Best Practices beider Plattformen entspricht. Verwenden Sie beispielsweise ein 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. In der Regel sollte das Element, das sich näher an der Wurzel der UI-Hierarchie befindet, die „Source of Truth“ sein.
Compose als zentrale Informationsquelle
Verwenden Sie die SideEffect-Composable, um den Compose-Status in Nicht-Compose-Code zu veröffentlichen. In diesem Fall wird die Quelle der Wahrheit in einer zusammensetzbaren Funktion gespeichert, die Statusaktualisierungen sendet.
Mit Ihrer Analysenbibliothek 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. Auf dem 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 auf bildschirmbasierten Ansichten verwenden möchten, erstellen Sie einen benutzerdefinierten Ansichtsumbruch, der von AbstractComposeView abgeleitet wird. Platzieren Sie das von Ihnen erstellte Composable in seinem überschriebenen Content-Composable, das 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) } } }
Die zusammensetzbaren Parameter werden in der benutzerdefinierten Ansicht zu veränderlichen Variablen. Dadurch wird die benutzerdefinierte CallToActionViewButton-Ansicht aufgeblasen und kann wie eine herkömmliche Ansicht verwendet werden.
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, lesen Sie den Abschnitt State source of truth.
Status 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 ein visibility-Attribut, das beschreibt, ob es sichtbar, unsichtbar oder nicht vorhanden ist. Dies ist eine inhärente Eigenschaft von 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 dafür gesorgt 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 die Art und Weise, wie Sie Inhalte als Umwandlung von Status in die Benutzeroberfläche darstellen, freier ändern. Wenn Sie den Status bei Bedarf hochziehen 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 aufgebläht werden, ist die Gesamtstruktur eines View in der Regel 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 hat, und seine Eigenschaften direkt als Reaktion auf eine Aktion ändern. Dadurch werden die 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.
In Compose mit wiederverwendbaren Composables ist das weniger ein Problem. Eltern können ganz einfach Status und Rückrufe 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:
ImageWithEnabledOverlaymuss nur den aktuellenisEnabled-Status kennen. Es muss nicht wissen, dassControlPanelWithTogglevorhanden ist oder wie es gesteuert werden kann.ControlPanelWithToggleweiß nicht, dassImageWithEnabledOverlayexistiert.isEnabledkann auf null, eine oder mehrere Arten angezeigt werden undControlPanelWithTogglemuss sich nicht ändern.Für das übergeordnete Element spielt es keine Rolle, wie tief
ImageWithEnabledOverlayoderControlPanelWithToggleverschachtelt 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 Fenstergrößenklassen verwenden.
Weitere Informationen zu den Techniken, die Compose zum Erstellen adaptiver Benutzeroberflächen bietet, finden Sie unter Verschiedene Displaygrößen unterstützen.
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.
In RecyclerView verfassen
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 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 eine komponierbare Funktion ist, können Sie die Insets auch in Compose verwenden und die AndroidView-komponierbaren Funktionen 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.
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Emojis anzeigen
- Material Design 2 in Compose
- Fenstereinsätze in Compose