Altre considerazioni

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:

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.

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 di isEnabled. Non deve necessariamente sapere che ControlPanelWithToggle esiste o persino come è controllabile.

  • ControlPanelWithToggle non sa dell'esistenza di ImageWithEnabledOverlay. Potrebbero esserci zero, uno o più modi in cui isEnabled viene visualizzato e ControlPanelWithToggle non dovrebbe cambiare.

  • Per l'elemento padre, non importa quanto siano nidificati ImageWithEnabledOverlay o ControlPanelWithToggle. 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.