Sebbene la migrazione da Visualizzazioni a Componi sia puramente correlata all'interfaccia utente, ci sono molte cose da tenere in considerazione per eseguire una migrazione sicura e incrementale. Questa pagina contiene alcune considerazioni durante la migrazione della tua app basata su visualizzazioni a Compose.
Eseguire la migrazione del tema dell'app
Material Design è il sistema di design consigliato per la creazione di temi per le app per Android.
Per le app basate su visualizzazioni, sono disponibili tre versioni di Material:
- Material Design 1 utilizzando la libreria AppCompat (ad es.
Theme.AppCompat.*
) - Material Design 2 che utilizza la libreria MDC-Android (ad es.
Theme.MaterialComponents.*
) - Material Design 3 utilizzando la libreria MDC-Android (ad es.
Theme.Material3.*
)
Per le app Compose sono disponibili due versioni di Material:
- Material Design 2 utilizzando la raccolta Compose Material (ad es.
androidx.compose.material.MaterialTheme
) - Material Design 3 utilizzando la raccolta
Compose Material 3
(ad es.
androidx.compose.material3.MaterialTheme
)
Ti consigliamo di utilizzare la versione più recente (Material 3) se il sistema di progettazione della tua app è in grado di farlo. Sono disponibili guide alla migrazione sia per le viste che per Compose:
- Da Material 1 a Material 2 nelle viste
- Materiale 2 a Materiale 3 nelle visualizzazioni
- Materiale 2 a Materiale 3 in Scrivi
Quando crei nuove schermate in Compose, indipendentemente dalla versione di Material Design in uso, assicurati di applicare un MaterialTheme
prima di qualsiasi composable che emette l'interfaccia utente dalle librerie Material di Compose. I componenti di Material (Button
, Text
e così via) dipendono dall'esistenza di un MaterialTheme
e il loro comportamento non è definito in sua assenza.
Tutti
i Sample di Jetpack Compose
utilizzano un tema Compose personalizzato basato su MaterialTheme
.
Per saperne di più, vedi Progettare sistemi in Compose e Eseguire la migrazione di temi XML in Compose.
Navigazione
Se utilizzi il componente Navigazione nella tua app, consulta gli articoli Navigazione con Compose - Interoperabilità e Eseguire la migrazione di Jetpack Navigazione a Navigation Compose per ulteriori informazioni.
Testare la UI mista di Scrittura/Visualizzazioni
Dopo aver eseguito la migrazione di parti dell'app a Compose, i test sono fondamentali per assicurarti di non aver danneggiato nulla.
Quando un'attività o un frammento utilizza Compose, devi utilizzare
createAndroidComposeRule
instead of using ActivityScenarioRule
. createAndroidComposeRule
integra
ActivityScenarioRule
con un ComposeTestRule
che ti consente di testare contemporaneamente il codice di Compose e di Visualizza.
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 Testare il layout di Componi. Per informazioni sull'interoperabilità con i framework di test dell'interfaccia utente, consulta Interoperabilità con Espresso e Interoperabilità con UiAutomator.
Integrare Compose con l'architettura dell'app esistente
I pattern di architettura di flusso di dati unidirezionale (UDF) funzionano perfettamente con Compose. Se invece l'app utilizza altri tipi di pattern di architettura, come Model View Presenter (MVP), ti consigliamo di eseguire la migrazione di quella parte della UI alla funzione definita dall'utente prima o durante l'adozione di Compose.
Utilizzare un ViewModel
in Scrittura
Se utilizzi la libreria Componenti di architettura
ViewModel
, puoi accedere a
ViewModel
da qualsiasi componibile
chiamando la funzione
viewModel()
, come spiegato in Compose e altre librerie.
Quando adotti Compose, fai attenzione a utilizzare lo stesso tipo ViewModel
in
diversi componibili degli elementi ViewModel
che seguono l'ambito del ciclo di vita delle visualizzazioni. L'ambito sarà l'attività host, il frammento o il grafo di navigazione se viene utilizzata la libreria Navigation.
Ad esempio, se i composabili sono ospitati in un'attività, viewModel()
sempre
restituisce la stessa istanza che viene cancellata solo al termine dell'attività.
Nell'esempio seguente, lo stesso utente ("user1") viene salutato due volte perché
la stessa istanza GreetingViewModel
viene riutilizzata in tutti i composabili nell'attività
dell'host. La prima istanza di ViewModel
creata viene riutilizzata in altri composabili.
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é anche i grafici di navigazione influiscono sugli elementi ViewModel
, i componibili che rappresentano
una destinazione in un grafico di navigazione hanno un'istanza diversa di ViewModel
.
In questo caso, l'ambito ViewModel
è limitato al 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") ?: "") } } }
Stato fonte di riferimento
Quando utilizzi Compose in una parte dell'interfaccia utente, è possibile che Compose e il codice di sistema della visualizzazione debbano condividere dati. Se possibile, ti consigliamo dicapsulare questo stato condiviso in un'altra classe che segua le best practice per le funzioni UDF utilizzate da entrambe le piattaforme, ad esempio in un ViewModel
che esponga uno stream di dati condivisi per emettere aggiornamenti dei dati.
Tuttavia, non è sempre possibile se i dati da condividere sono mutabili o fortemente legati a un elemento dell'interfaccia utente. In questo caso, un sistema deve essere l'origine attendibile e deve condividere tutti gli aggiornamenti dei dati con l'altro. Come regola generale, la fonte attendibile deve essere di proprietà dell'elemento più vicino alla radice della gerarchia dell'interfaccia utente.
Componi come fonte attendibile
Utilizza il composable
SideEffect
per pubblicare lo stato di Compose in codice non Compose. In questo caso, la fonte attendibile è conservata in un componibile, che invia aggiornamenti di stato.
Ad esempio, la tua libreria di analisi potrebbe consentirti di segmentare la popolazione di utenti collegando metadati personalizzati (proprietà utente in questo esempio) a tutti gli eventi di analisi successivi. Per comunicare il tipo di utente
dell'utente corrente alla tua 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, consulta Effetti collaterali in Compose.
Considera il sistema come la fonte attendibile
Se il sistema di visualizzazione è proprietario dello stato e lo condivide con Compose, ti consigliamo di avvolgere lo stato in oggetti mutableStateOf
per renderlo sicuro per i thread per Compose. Se utilizzi questo approccio, le funzioni composable vengono semplificate perché
non hanno più l'origine della verità, ma il sistema di visualizzazione deve aggiornare
lo stato mutabile e le visualizzazioni che lo utilizzano.
Nell'esempio seguente, un CustomViewGroup
contiene un TextView
e un
ComposeView
con un composable TextField
al suo interno. Il TextView
deve mostrare
i contenuti digitati dall'utente nel 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 dell'interfaccia utente condivisa
Se esegui la migrazione graduale a Scrivi, potresti dover utilizzare elementi di UI condivisi sia in Scrivi sia nel sistema di visualizzazione. Ad esempio, se la tua app ha un
componente CallToActionButton
personalizzato, potresti doverlo utilizzare sia nelle schermate di scrittura
che in quelle basate sulla visualizzazione.
In Compose, gli elementi dell'interfaccia utente condivisi diventano composabili che possono essere riutilizzati nell'app, indipendentemente dal fatto che l'elemento sia stilizzato utilizzando XML o sia una visualizzazione personalizzata. Ad esempio, creerai un composable CallToActionButton
per il componente Button
chiamata all'azione personalizzata.
Per utilizzare il composable nelle schermate basate su visualizzazioni, crea un wrapper della visualizzazione personalizzata che si estende da AbstractComposeView
. Nel composable Content
sostituito, colloca il composable che hai creato racchiuso nel tema Compose, 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 composable diventano variabili mutabili all'interno della vista personalizzata. Questo rende la visualizzazione personalizzata CallToActionViewButton
gonfiabile e utilizzabile,
come una visualizzazione tradizionale. Di seguito è riportato un esempio con 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 */ } } } }
Se il componente personalizzato contiene uno stato mutabile, consulta Origine di verità dello stato.
Dare la priorità allo stato di suddivisione della presentazione
Tradizionalmente, un View
è stateful. Un View
gestisce i campi che descrivono cosa visualizzare e come visualizzarlo. Quando
converti un'istruzione View
in Compose, cerca di separare i dati di cui viene eseguito il rendering per
ottenere un flusso di dati unidirezionale, come spiegato ulteriormente nella sezione relativa al passaggio dello stato.
Ad esempio, un View
ha una proprietà visibility
che descrive se è visibile, invisibile o non presente. Questa è una proprietà intrinseca di View
. Anche se
altri frammenti di codice possono modificare la visibilità di un View
, solo il View
in sé sa quale sia la sua visibilità attuale. La logica per garantire che
un View
sia visibile può essere soggetta a errori ed è spesso legata al View
in sé.
Al contrario, Compose consente di visualizzare facilmente composabili completamente diversi utilizzando la logica condizionale in Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
Per sua natura, CautionIcon
non deve sapere o preoccuparsi del motivo per cui viene visualizzato e non esiste il concetto di visibility
: o è nella composizione o non lo è.
Separando in modo chiaro la logica di gestione dello stato e di presentazione, puoi modificare più liberamente il modo in cui vengono visualizzati i contenuti sotto forma di conversione dello stato in UI. La possibilità di eseguire l'elevazione dello stato in caso di necessità rende inoltre i composabili più riutilizzabili, poiché la proprietà dello stato è più flessibile.
Promuovi componenti incapsulati e riutilizzabili
Gli elementi View
hanno spesso un'idea di dove si trovano: all'interno di un Activity
, di un
Dialog
, di un Fragment
o da qualche parte all'interno di un'altra gerarchia View
. Poiché spesso vengono gonfiati da file di layout statici, la struttura complessiva di un View
tende ad essere molto rigida. Ciò comporta un accoppiamento più stretto e rende più difficile la modifica o il riutilizzo di un View
.
Ad esempio, un View
personalizzato potrebbe presupporre di avere una visualizzazione secondaria di un determinato tipo con un determinato ID e modificare le sue proprietà direttamente in risposta a un'azione. Ciò accoppia strettamente questi elementi View
: il View
personalizzato potrebbe arrestarsi in modo anomalo o essere danneggiato se non riesce a trovare l'elemento secondario e quest'ultimo probabilmente non può essere riutilizzato senza l'elemento principale View
personalizzato.
Questo problema è meno grave in Compose con i composabili riutilizzabili. I componenti principali possono specificare facilmente lo stato e i callback, in modo da poter scrivere composabili riutilizzabili senza dover conoscere il luogo esatto 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 sapere cheControlPanelWithToggle
esiste o persino come è controllabile.ControlPanelWithToggle
non sa cheImageWithEnabledOverlay
esiste. Potrebbero esserci zero, uno o più modi in cui viene visualizzatoisEnabled
eControlPanelWithToggle
non dovrebbe cambiare.Per l'elemento principale, non importa la profondità di
ImageWithEnabledOverlay
oControlPanelWithToggle
. Questi bambini potrebbero animare le modifiche, scambiare contenuti o trasmetterli ad altri bambini.
Questo pattern è noto come inversione del controllo, di cui puoi scoprire di più
nella documentazione di CompositionLocal
.
Gestione delle modifiche delle dimensioni dello schermo
Avere risorse diverse per dimensioni diverse della finestra è uno dei modi principali per creare layout View
adattabili. Sebbene le risorse qualificate siano ancora un'opzione
per le decisioni relative al layout a livello di schermo, Compose semplifica la modifica
dei layout interamente all'interno del codice con la normale logica condizionale. Per scoprire di più, consulta la sezione Utilizzare le classi di dimensione della finestra.
Inoltre, consulta l'articolo Supportare schermi di dimensioni diverse per scoprire le tecniche offerte da Compose per creare UI adattive.
Scorri nidificato con le visualizzazioni
Per ulteriori informazioni su come abilitare l'interoperabilità con lo scorrimento nidificato tra gli elementi View a scorrimento e gli elementi componibili scorrevoli, nidificati in entrambe le direzioni, leggi attentamente l'articolo Interoperabilità a scorrimento nidificato.
Scrivi in RecyclerView
I composabili in RecyclerView
sono performanti dalla versione RecyclerView
1.3.0-alpha02. Per usufruire di questi vantaggi, assicurati di utilizzare almeno la versione 1.3.0-alpha02 diRecyclerView
.
Interoperabilità di WindowInsets
con Views
Potresti dover ignorare gli inserimenti predefiniti quando nella schermata sono presenti sia le visualizzazioni sia il codice di composizione nella stessa gerarchia. In questo caso, devi indicare esplicitamente quale deve utilizzare gli insert e quale ignorarli.
Ad esempio, se il layout più esterno è un layout di vista Android, devi utilizzare gli inserti nel sistema di visualizzazione e ignorarli per Compose.
In alternativa, se il layout più esterno è un composable, devi utilizzare gli inserti in Compose e aggiungere spaziatura ai composable AndroidView
di conseguenza.
Per impostazione predefinita, ogni ComposeView
utilizza tutti i riquadri al
livello di consumo WindowInsetsCompat
. Per modificare questo comportamento predefinito, imposta
ComposeView.consumeWindowInsets
su false
.
Per ulteriori informazioni, leggi la documentazione relativa a WindowInsets
in Compose.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Visualizzare le emoji
- Material Design 2 in Compose
- Insegni della finestra in Scrivi