Altre considerazioni

La migrazione dalle viste a Compose è puramente correlata alla UI, ma ci sono molti aspetti da tenere in considerazione per eseguire una migrazione sicura e incrementale. Questa pagina contiene alcune considerazioni durante la migrazione dell'app basata su View a Compose.

Eseguire la migrazione del tema dell'app

Material Design è il sistema di progettazione consigliato per la gestione dei temi delle app per Android.

Per le app basate sulla visualizzazione 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 che utilizza la libreria MDC-Android (ovvero 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 libreria 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:

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 gli esempi di Jetpack Compose utilizzano un tema Compose personalizzato basato su MaterialTheme.

Per saperne di più, consulta Design system in Compose e Eseguire la migrazione dei temi XML in Compose.

Se nella tua app utilizzi il componente Navigation, consulta la sezione Navigazione con Compose - Interoperabilità e Eseguire la migrazione di Jetpack Navigation 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 ComposeTestRule che ti consente di testare contemporaneamente il codice Compose e View.

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 l'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 grafico di navigazione, se viene utilizzata la Libreria di navigazione.

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 gli elementi componibili nell'attività 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 adotti Compose in una parte della UI, è possibile che Compose e il codice di sistema di visualizzazione 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 elemento ViewModel che espone un flusso 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, l'origine dati attendibile dovrebbe essere posseduta da qualsiasi 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 viene conservata in un composable, che invia aggiornamenti dello 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 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 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 componibile nelle schermate basate sulle visualizzazioni, crea un wrapper vista 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. In questo modo, la visualizzazione personalizzata CallToActionViewButton può essere gonfiata e utilizzata, 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

In genere, un View è stateful. Un View gestisce i campi che descrivono cosa visualizzare e come visualizzarlo. Quando converti un View in Compose, cerca di separare i dati da visualizzare per ottenere un flusso di dati unidirezionale, come spiegato in maggiore dettaglio nell'articolo sulla sostituzione 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 impostazione predefinita, CautionIcon non ha bisogno di sapere o di preoccuparsi del motivo per cui viene visualizzato, e non esiste il concetto di visibility: può essere nella composizione o no.

Se separi in modo chiaro la gestione dello stato e la logica di presentazione, puoi modificare più liberamente la modalità di visualizzazione dei contenuti come conversione dello stato in interfaccia utente. La possibilità di aumentare lo stato quando necessario rende gli elementi componibili più riutilizzabili, dato che la proprietà statale è 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 sono aumentati in modo artificioso a causa di file di layout statici, la struttura complessiva di un View tende a 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 di isEnabled. Non deve sapere che ControlPanelWithToggle esiste o persino come è controllabile.

  • ControlPanelWithToggle non sa che ImageWithEnabledOverlay esiste. isEnabled potrebbe essere visualizzato in uno o più modi e ControlPanelWithToggle non dovrebbe cambiare.

  • Per l'elemento principale, non importa la profondità di ImageWithEnabledOverlay o ControlPanelWithToggle. 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 alle dimensioni dello schermo

Disporre di risorse diverse per finestre di dimensioni diverse è uno dei modi principali per creare layout View adattabili. Anche se le risorse qualificate sono ancora un'opzione per le decisioni relative al layout a livello di schermata, Compose semplifica notevolmente la modifica dei layout interamente in codice con la normale logica condizionale. Per ulteriori informazioni, consulta Utilizzare le classi di dimensione delle finestre.

Inoltre, consulta la sezione Supportare schermi di dimensioni diverse per scoprire le tecniche offerte da Compose per creare UI adattabili.

Scorrimento nidificato con 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 componenti componibili in RecyclerView funzionano a partire dalla versione 1.3.0-alpha02 di RecyclerView. Per usufruire di questi vantaggi, assicurati di utilizzare almeno la versione 1.3.0-alpha02 diRecyclerView.

Interoperabilità di WindowInsets con Views

Potrebbe essere necessario eseguire l'override degli insiemi predefiniti quando lo schermo ha sia il codice Visualizzazioni sia il codice Compose 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 componibile, devi utilizzare gli insiemi in Compose e comporre di conseguenza i componibili AndroidView.

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.