Altre considerazioni

La migrazione da Visualizzazioni a Crea è puramente correlata all'interfaccia utente, ma ci sono molte cose da prendere in considerazione per eseguire una migrazione sicura e incrementale. Questa pagina contiene alcune considerazioni da tenere presenti durante la migrazione dell'app basata su View a Compose.

Migrazione del tema dell'app

Material Design è il sistema di progettazione consigliato per la creazione di temi per le app per Android.

Per le app basate su View, sono disponibili tre versioni di Material:

  • Material Design 1 che utilizza la libreria AppCompat (ad es. Theme.AppCompat.*)
  • Material Design 2 utilizzando 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 libreria 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 l'ultima versione (Material 3) se il sistema di progettazione della tua app è in grado di farlo. Sono disponibili guide alla migrazione sia per Views che per Compose:

Quando crei nuove schermate in Compose, indipendentemente dalla versione di Material Design che utilizzi, assicurati di applicare un MaterialTheme prima di qualsiasi composizione 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.

Tutti gli esempi di Jetpack Compose utilizzano un tema Compose personalizzato basato su MaterialTheme.

Per saperne di più, consulta Sistemi di progettazione in Compose e Migrazione dei temi XML a Compose.

Se utilizzi il componente di navigazione nella tua app, consulta Navigazione con Compose - Interoperabilità e Migrazione di Jetpack Navigation a Navigation Compose per saperne di più.

Testare la UI di composizione/visualizzazioni mista

Dopo aver eseguito la migrazione di parti dell'app a Compose, i test sono fondamentali per assicurarsi di non aver danneggiato nulla.

Quando un'attività o un fragment 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 saperne di più sui test, consulta la sezione Testare il layout di Composizione. Per l'interoperabilità con i framework di test dell'interfaccia utente, consulta Interoperabilità con Espresso e Interoperabilità con UiAutomator.

Integrazione di Compose con l'architettura dell'app esistente

I pattern di architettura Unidirectional Data Flow (UDF) funzionano perfettamente con Compose. Se l'app utilizza altri tipi di pattern di architettura, come Model View Presenter (MVP), ti consigliamo di migrare quella parte della UI a UDF prima o durante l'adozione di Compose.

Utilizzare un ViewModel in Scrivi

Se utilizzi la libreria Architecture Components ViewModel, puoi accedere a un ViewModel da qualsiasi composable chiamando la funzione viewModel(), come spiegato in Compose e altre librerie.

Quando adotti Compose, fai attenzione a utilizzare lo stesso tipo ViewModel in composable diversi, poiché gli elementi ViewModel seguono gli ambiti del ciclo di vita della visualizzazione. L'ambito sarà l'attività host, il fragment o il grafico di navigazione se viene utilizzata la libreria Navigation.

Ad esempio, se i composable 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 salutato due volte perché la stessa istanza di GreetingViewModel viene riutilizzata in tutti i composable nell'attività host. La prima istanza ViewModel creata viene riutilizzata in altri componenti 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 definiscono anche l'ambito degli elementi ViewModel, i composable che sono una destinazione in un grafico di navigazione hanno un'istanza diversa di ViewModel. In questo caso, ViewModel è limitato al ciclo di vita della destinazione e viene cancellato quando la destinazione viene rimossa dallo stack indietro. 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 attendibile dello stato

Quando adotti Compose in una parte della UI, è possibile che Compose e il codice del sistema View debbano condividere i dati. Se possibile, ti consigliamo di incapsulare lo stato condiviso in un'altra classe che segue le best practice per le UDF utilizzate da entrambe le piattaforme, ad esempio in un ViewModel che espone un flusso di dati condivisi per emettere aggiornamenti dei dati.

Tuttavia, non è sempre possibile se i dati da condividere sono modificabili o sono strettamente legati a un elemento dell'interfaccia utente. In questo caso, un sistema deve essere la fonte di verità e deve condividere gli aggiornamenti dei dati con l'altro sistema. 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 SideEffect componente componibile per pubblicare lo stato di Compose nel codice non Compose. In questo caso, la fonte attendibile viene mantenuta in un composable, che invia aggiornamenti dello stato.

Ad esempio, la tua libreria di analisi potrebbe consentirti di segmentare la popolazione di utenti allegando metadati personalizzati (proprietà utente in questo esempio) a tutti gli eventi di analisi successivi. Per comunicare il tipo di utente dell'utente attuale 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 saperne di più, consulta Effetti collaterali in Compose.

Visualizzare il sistema come fonte attendibile

Se la visualizzazione di sistema possiede lo stato e lo condivide con Compose, ti consigliamo di racchiudere lo stato in oggetti mutableStateOf per renderlo thread-safe per Compose. Se utilizzi questo approccio, le funzioni componibili vengono semplificate perché non hanno più l'origine attendibile, ma il sistema View deve aggiornare lo stato modificabile e le View che utilizzano questo stato.

Nell'esempio seguente, un CustomViewGroup contiene un TextView e un ComposeView con un TextField componibile all'interno. Il TextView deve mostrare il contenuto di ciò che l'utente digita 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 dell'interfaccia utente condivisa

Se esegui la migrazione gradualmente a Compose, potresti dover utilizzare elementi dell'interfaccia utente condivisi sia in Compose che nel sistema View. Ad esempio, se la tua app ha un componente CallToActionButton personalizzato, potresti doverlo utilizzare sia nelle schermate basate su Compose sia in quelle basate su View.

In Compose, gli elementi UI condivisi diventano composable riutilizzabili nell'app, indipendentemente dal fatto che l'elemento sia stilizzato utilizzando XML o sia una visualizzazione personalizzata. Ad esempio, creeresti un CallToActionButton componibile per il componente di invito all'azione Button personalizzato.

Per utilizzare il composable nelle schermate basate su View, crea un wrapper di visualizzazione personalizzato che estenda AbstractComposeView. Nel composable Content sottoposto a override, inserisci 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 componibili diventano variabili modificabili all'interno della visualizzazione personalizzata. In questo modo, la visualizzazione personalizzata CallToActionViewButton diventa 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 modificabile, consulta Origine attendibile dello stato.

Dai la priorità alla separazione dello stato dalla presentazione

Tradizionalmente, un View è stateful. Un View gestisce i campi che descrivono cosa visualizzare, oltre a come visualizzarlo. Quando converti un View in Compose, cerca di separare i dati di rendering per ottenere un flusso di dati unidirezionale, come spiegato più avanti in state hoisting.

Ad esempio, un View ha una proprietà visibility che descrive se è visibile, invisibile o scomparso. Si tratta di una proprietà intrinseca di View. Sebbene altri frammenti di codice possano modificare la visibilità di un View, solo il View stesso conosce la sua visibilità attuale. La logica per garantire che un View sia visibile può essere soggetta a errori ed è spesso legata al View stesso.

Al contrario, Compose consente di visualizzare facilmente composable completamente diversi utilizzando la logica condizionale in Kotlin:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

Per progettazione, CautionIcon non ha bisogno di sapere o di preoccuparsi del motivo per cui viene visualizzato e non esiste il concetto di visibility: è nella composizione o non lo è.

Separando in modo pulito la gestione dello stato e la logica di presentazione, puoi modificare più liberamente il modo in cui visualizzi i contenuti come conversione dello stato in UI. La possibilità di sollevare lo stato quando necessario rende i composable più riutilizzabili, poiché la proprietà dello stato è più flessibile.

Promuovere componenti incapsulati e riutilizzabili

Gli elementi View spesso hanno un'idea di dove si trovano: all'interno di un Activity, di un Dialog, di un Fragment o in un altro punto della gerarchia View. Poiché sono spesso gonfiate da 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 modificare o riutilizzare 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 relative proprietà direttamente in risposta a un'azione. In questo modo, gli elementi View sono strettamente accoppiati: l'elemento View personalizzato potrebbe bloccarsi o danneggiarsi se non riesce a trovare l'elemento secondario e quest'ultimo probabilmente non può essere riutilizzato senza l'elemento padre View personalizzato.

Questo problema è meno frequente in Compose con i composable riutilizzabili. I genitori possono specificare facilmente lo stato e i callback, in modo da poter scrivere composable riutilizzabili senza dover conoscere il punto 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 attuale di isEnabled. Non ha bisogno di sapere che ControlPanelWithToggle esiste o come è controllabile.

  • ControlPanelWithToggle non sa che ImageWithEnabledOverlay esiste. Esistono zero, uno o più modi in cui viene visualizzato isEnabled e ControlPanelWithToggle non deve essere modificato.

  • Per il genitore, non importa quanto siano nidificati in profondità ImageWithEnabledOverlay o ControlPanelWithToggle. Questi bambini potrebbero animare i cambiamenti, scambiare contenuti o trasmetterli ad altri bambini.

Questo pattern è noto come inversione del controllo, di cui puoi leggere di più nella documentazione di CompositionLocal.

Gestire le modifiche alle dimensioni dello schermo

Avere risorse diverse per dimensioni diverse delle finestre è uno dei modi principali per creare layout View adattabili. Sebbene le risorse qualificate siano ancora un'opzione per le decisioni sul layout a livello di schermo, Compose semplifica notevolmente la modifica dei layout interamente nel codice con la normale logica condizionale. Per saperne di più, consulta la sezione Utilizzare le classi di dimensioni della finestra.

Inoltre, consulta Supportare diverse dimensioni di visualizzazione per scoprire le tecniche offerte da Compose per creare UI adattive.

Scorrimento nidificato con Views

Per ulteriori informazioni su come abilitare l'interoperabilità dello scorrimento nidificato tra gli elementi View scorrevoli e i composable scorrevoli, nidificati in entrambe le direzioni, leggi Interoperabilità dello scorrimento nidificato.

Scrivi in RecyclerView

I composable in RecyclerView sono efficienti 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 di RecyclerView.

WindowInsets interoperabilità con le visualizzazioni

Potresti dover ignorare gli inset predefiniti quando lo schermo contiene sia Views che Compose code nella stessa gerarchia. In questo caso, devi specificare quale deve utilizzare gli inset e quale deve ignorarli.

Ad esempio, se il layout più esterno è un layout View di Android, devi utilizzare gli inset nel sistema View e ignorarli per Compose. In alternativa, se il layout più esterno è un elemento componibile, devi utilizzare gli inset in Compose e aggiungere il padding agli elementi componibili AndroidView di conseguenza.

Per impostazione predefinita, ogni ComposeView consuma tutti gli inserti al livello di consumo WindowInsetsCompat. Per modificare questo comportamento predefinito, imposta ComposeView.consumeWindowInsets su false.

Per saperne di più, leggi la documentazione relativa all'WindowInsets in Compose.