Sebbene la migrazione da Visualizzazioni a Compose sia puramente correlata all'interfaccia utente, ci sono molti aspetti da considerare per eseguire una migrazione sicura e incrementale. Questa pagina contiene alcune considerazioni relative alla migrazione dell'app basata su visualizzazioni a Compose.
Migrazione del tema dell'app
Material Design è il sistema di progettazione consigliato per la tematizzazione delle app per Android.
Per le app basate sulle visualizzazioni, sono disponibili tre versioni di Material:
- Material Design 1 utilizzando la libreria
AppCompat (ad es.
Theme.AppCompat.*
) - Material Design 2 con la libreria MDC-Android (ad es.
Theme.MaterialComponents.*
) - Material Design 3 con la libreria MDC-Android (ad es.
Theme.Material3.*
)
Per le app Compose, sono disponibili due versioni di Material:
- Material Design 2 utilizzando la libreria Compose Material (ad es.
androidx.compose.material.MaterialTheme
) - Material Design 3 utilizzando la libreria di Compose Material 3 (ad es.
androidx.compose.material3.MaterialTheme
).
Consigliamo di utilizzare la versione più recente (Material 3) se il sistema di progettazione dell'app è in grado di farlo. Sono disponibili guide alla migrazione sia per View che per Compose:
- Da Materiale 1 a Materiale 2 in Visualizzazioni
- Da Materiale 2 a Materiale 3 in Visualizzazioni
- Da Materiale 2 a Materiale 3 in Compose
Quando crei nuove schermate in Compose, indipendentemente dalla versione di Material
Design che stai utilizzando, assicurati di applicare un MaterialTheme
prima di qualsiasi
componibile che emette UI dalle librerie di Compose Material. I componenti Material (Button
, Text
e così via) dipendono dalla presenza di un MaterialTheme
e il loro comportamento è indefinito senza questo valore.
Tutti gli
esempi di Jetpack Compose
utilizzano un tema Compose personalizzato basato su MaterialTheme
.
Per saperne di più, vedi Progettare sistemi in Compose e Migrazione dei temi XML in Compose.
Navigazione
Se utilizzi il componente di navigazione nell'app, per saperne di più, consulta Navigazione con Compose - Interoperability e Eseguire la migrazione di Jetpack Navigation a Navigation Compose.
Testare l'interfaccia utente mista Compose/View
Dopo aver eseguito la migrazione di parti dell'app a Compose, i test sono fondamentali per assicurarsi di non aver subito problemi.
Quando un'attività o un frammento utilizza Compose, devi utilizzare createAndroidComposeRule
anziché ActivityScenarioRule
. createAndroidComposeRule
integra
ActivityScenarioRule
con un ComposeTestRule
che ti consente di testare il codice di Compose e
View contemporaneamente.
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() } }
Per scoprire di più sui test, consulta Test del layout di Compose. Per l'interoperabilità con i framework di test dell'UI, consulta Interoperabilità con Espresso e interoperabilità con UiAutomator.
Integrazione di Compose nell'architettura dell'app esistente
I pattern di architettura Unidirectional Data Flow (UDF) funzionano perfettamente con Compose. Se invece l'app utilizza altri tipi di pattern di architettura, ad esempio Model View Presenter (MVP), ti consigliamo di eseguire la migrazione di quella parte dell'interfaccia utente a UDF prima o durante l'adozione di Compose.
Uso di ViewModel
in Compose
Se utilizzi la libreria Componenti di architettura
ViewModel
, puoi accedere a un
ViewModel
da qualsiasi componibile
chiamando la funzione
viewModel()
come spiegato in Compose e altre librerie.
Quando adotti Compose, presta attenzione all'utilizzo dello stesso tipo ViewModel
in
diversi componibili, poiché gli elementi ViewModel
seguono gli ambiti del ciclo di vita delle visualizzazioni. L'ambito sarà l'attività host, il frammento o il grafico di navigazione, se viene utilizzata la libreria di navigazione.
Ad esempio, se gli elementi componibili sono ospitati in un'attività, viewModel()
restituisce sempre la stessa istanza che viene cancellata solo al termine dell'attività.
Nell'esempio seguente, lo stesso utente ("user1") viene accolto due volte perché
la stessa istanza GreetingViewModel
viene riutilizzata in tutti gli elementi componibili nell'attività
host. La prima istanza ViewModel
creata viene riutilizzata in altri
componibili.
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 } }
Poiché i grafici di navigazione hanno anche l'ambito degli elementi ViewModel
, i componibili che sono una destinazione in un grafico di navigazione hanno un'istanza diversa di ViewModel
.
In questo caso, ViewModel
ha come ambito il ciclo di vita della destinazione e viene cancellato quando la destinazione viene rimossa dal backstack. Nell'esempio seguente, quando l'utente passa alla schermata Profilo, viene creata una nuova istanza di GreetingViewModel
.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
Fonte di verità statale
Quando adotti Compose in una parte dell'interfaccia utente, è possibile che i codici di sistema di Compose e
View debbano condividere dati. Se possibile, ti consigliamo di
incapsulare lo stato condiviso in un'altra classe che segue le best practice delle funzioni definite dall'utente utilizzate da entrambe le piattaforme, ad esempio in un ViewModel
che espone un flusso di
dati condivisi per emettere aggiornamenti dei dati.
Tuttavia, ciò non è sempre possibile se i dati da condividere sono modificabili o sono strettamente associati a un elemento UI. In tal caso, un sistema deve essere la fonte attendibile e quel sistema deve condividere gli eventuali aggiornamenti dei dati con l'altro. Come regola generale, la fonte attendibile dovrebbe essere di proprietà dell'elemento più vicino alla radice della gerarchia dell'interfaccia utente.
Componi come fonte attendibile
Utilizza l'elemento componibile SideEffect
per pubblicare lo stato della Compose in un codice non composto. In questo caso, la fonte attendibile è mantenuta in un componibile, che invia aggiornamenti di stato.
Ad esempio, la libreria di analisi potrebbe consentirti di segmentare la popolazione di utenti collegando i metadati personalizzati (in questo esempio le proprietà utente) a tutti gli eventi di analisi successivi. Per comunicare il tipo di utente dell'utente corrente alla libreria di analisi, utilizza SideEffect
per aggiornarne il valore.
@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 }
Per ulteriori informazioni, vedi Effetti collaterali in Compose.
Vedi il sistema come fonte attendibile
Se il sistema View è proprietario dello stato e lo condivide con Compose, ti consigliamo di
aggregare lo stato negli oggetti mutableStateOf
per renderlo sicuro per i thread per
Compose. Se utilizzi questo approccio, le funzioni componibili vengono semplificate perché non hanno più la fonte attendibile, ma il sistema delle viste deve aggiornare lo stato modificabile e le viste che lo utilizzano.
Nell'esempio seguente, un elemento CustomViewGroup
contiene un TextView
e un
ComposeView
con un elemento componibile TextField
all'interno. L'elemento TextView
deve mostrare
i contenuti dei tipi di testo digitati dall'utente in TextField
.
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 } }
Migrazione della UI condivisa
Se esegui la migrazione graduale a Compose, potresti dover utilizzare elementi UI
condivisi sia nel sistema di Compose sia nel sistema View. Ad esempio, se la tua app ha un componente CallToActionButton
personalizzato, potresti doverlo utilizzare sia nella schermata Scrivi sia in quella basata sulle visualizzazioni.
In Compose, gli elementi dell'interfaccia utente condivisi diventano componibili che possono essere riutilizzati nell'app, indipendentemente dal fatto che l'elemento abbia uno stile utilizzando XML o che sia una visualizzazione personalizzata. Ad
esempio, devi creare un componibile CallToActionButton
per il componente Button
di invito all'azione personalizzato.
Per utilizzare il componibile nelle schermate basate sulle visualizzazioni, crea un wrapper visualizzazione personalizzata che si estende da AbstractComposeView
. Nel relativo componibile Content
sostituito,
posiziona l'elemento componibile che hai creato a capo del tema Scrivi, come mostrato
nell'esempio seguente:
@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) } } }
Tieni presente che i parametri componibili diventano variabili mutabili all'interno della vista personalizzata. Ciò rende la vista CallToActionViewButton
personalizzata gonfiabile e utilizzabile,
come una vista tradizionale. Guarda un esempio con Visualizza associazione di seguito:
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 */ } } } }
Se il componente personalizzato contiene uno stato modificabile, consulta Stato fonte attendibile.
Dai la priorità allo stato di suddivisione dalla presentazione
Tradizionalmente, un View
è stateful. Un View
gestisce i campi che descrivono cosa visualizzare, oltre alla modalità di visualizzazione. Quando
converti un View
in Compose, cerca di separare i dati di cui viene eseguito il rendering per
ottenere un flusso di dati unidirezionale, come spiegato più dettagliatamente nell'articolo sull'attacco dello stato.
Ad esempio, View
ha una proprietà visibility
che indica se è visibile, invisibile o non disponibile. Questa è una proprietà intrinseca di View
. Anche se
altre parti di codice possono modificare la visibilità di un oggetto View
, solo l'elemento View
in sé sa davvero qual è la sua visibilità attuale. La logica per garantire che
un elemento View
sia visibile può essere soggetta a errori ed è spesso legata all'oggetto View
stesso.
Al contrario, Compose semplifica la visualizzazione di componibili completamente diversi utilizzando la logica condizionale in Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
In base alla sua progettazione, CautionIcon
non ha bisogno di sapere o di preoccuparsi del motivo per cui viene visualizzato. Inoltre, non esiste il concetto di visibility
: o è nella composizione o non lo è.
Separando in modo netto la logica di gestione dello stato e di presentazione, puoi modificare più liberamente il modo in cui visualizzi i contenuti come conversione dello stato in UI. La possibilità di istruire lo stato quando è necessario rende i componibili più riutilizzabili, dal momento che la proprietà dello stato è più flessibile.
Promuovere componenti incapsulati e riutilizzabili
Gli elementi View
hanno spesso un'idea di dove si trovano: all'interno di una gerarchia Activity
, Dialog
, Fragment
o in qualche modo all'interno di un'altra gerarchia View
. Poiché vengono spesso gonfiati da file di layout statici, la struttura complessiva di un elemento View
tende a essere molto rigida. Ciò determina un accoppiamento più stretto e rende
più difficile la modifica o il riutilizzo di un View
.
Ad esempio, un elemento View
personalizzato potrebbe presumere di avere una vista secondaria di un certo tipo con un determinato ID e modificarne le proprietà direttamente in risposta a un'azione. Questo accoppia strettamente gli elementi View
: l'elemento View
personalizzato potrebbe arrestarsi in modo anomalo se non riesce a trovare l'elemento secondario e probabilmente l'elemento secondario non può essere riutilizzato senza l'elemento principale View
personalizzato.
Questo problema non rappresenta un problema in Compose con elementi componibili riutilizzabili. I genitori possono specificare facilmente lo stato e i callback, in modo da poter scrivere componibili riutilizzabili senza dover conoscere la posizione esatta in cui verranno utilizzati.
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
Nell'esempio precedente, tutte e tre le parti sono più incapsulate e meno accoppiate:
ImageWithEnabledOverlay
deve solo sapere qual è lo stato corrente diisEnabled
. Non deve necessariamente sapere cheControlPanelWithToggle
esiste o persino come è controllabile.ControlPanelWithToggle
non sa dell'esistenza diImageWithEnabledOverlay
. Potrebbero esserci zero, uno o più modi in cuiisEnabled
viene visualizzato eControlPanelWithToggle
non dovrebbe cambiare.Per l'elemento padre, non importa quanto siano nidificati
ImageWithEnabledOverlay
oControlPanelWithToggle
. come animare cambiamenti, sostituire contenuti o passarli ad altri.
Questo pattern è noto come inversione del controllo; puoi scoprire di più
nella documentazione di CompositionLocal
.
Gestione delle modifiche alle dimensioni dello schermo
Avere risorse diverse per dimensioni di finestre diverse è uno dei modi principali per creare layout View
adattabili. Sebbene le risorse qualificate siano ancora un'opzione per prendere decisioni in merito ai layout a livello di schermo, Compose semplifica la modifica dei layout interamente nel codice con la normale logica condizionale. Per ulteriori informazioni, consulta Supporto di schermi di dimensioni diverse.
Inoltre, consulta la pagina Creare layout adattivi per scoprire le tecniche offerte da Compose per creare UI adattive.
Scorrimento nidificato con Visualizzazioni
Per ulteriori informazioni su come abilitare l'interoperabilità dello scorrimento nidificato tra gli elementi di visualizzazione a scorrimento e i componibili scorrevoli, nidificati in entrambe le direzioni, leggi l'articolo sull'interoperabilità dello scorrimento nidificato.
Scrivi in RecyclerView
Gli elementi componibili in RecyclerView
funzionano a partire dalla versione 1.3.0-alpha02 di RecyclerView
. Assicurati di utilizzare almeno la versione 1.3.0-alpha02 di
RecyclerView
per vedere questi vantaggi.
Interoperabilità di WindowInsets
con Visualizzazioni
Potrebbe essere necessario eseguire l'override dei riquadri predefiniti quando lo schermo ha sia il codice Visualizzazioni sia Scrivi nella stessa gerarchia. In questo caso, devi indicare esplicitamente in quale quello utilizzare gli inset e in quale ignorarli.
Ad esempio, se il layout più esterno è un layout Android View, devi utilizzare gli insiemi nel sistema di visualizzazione e ignorarli per Compose.
In alternativa, se il layout più esterno è un componibile, dovresti utilizzare gli
inserti di Compose e incollare i componibili AndroidView
di conseguenza.
Per impostazione predefinita, ogni elemento ComposeView
consuma tutti gli insiemi al livello di consumo WindowInsetsCompat
. Per modificare questo comportamento predefinito, imposta ComposeView.consumeWindowInsets
su false
.
Per maggiori informazioni, leggi la documentazione di WindowInsets
in Compose.
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Mostra emoji
- Material Design 2 in Compose
- Inset di finestre in Compose