Creare layout adattivi

L'interfaccia utente dell'app deve adattarsi a dimensioni dello schermo, orientamenti e fattori di forma diversi. Il layout adattivo cambia in base allo spazio disponibile sullo schermo. Queste modifiche spaziano dalle semplici modifiche del layout per riempire lo spazio al cambiamento completo dei layout per utilizzare spazio aggiuntivo.

Come toolkit di interfaccia utente dichiarativa, Jetpack Compose è ideale per la progettazione e l'implementazione di layout che si adattano automaticamente per eseguire il rendering dei contenuti in modo diverso in diverse dimensioni. Questo documento contiene alcune linee guida su come usare Scrivi per rendere reattiva l'interfaccia utente.

Apporta modifiche esplicite al layout dei componibili a livello di schermo

Quando utilizzi Scrivi per impaginare un'intera applicazione, gli elementi componibili a livello di app e di schermo occupano tutto lo spazio disponibile per il rendering dell'app. A questo livello del design, potrebbe essere opportuno modificare il layout complessivo di uno schermo per sfruttare schermi più grandi.

Evita di usare valori fisici e hardware per prendere decisioni relative al layout. La tentazione di prendere decisioni basate su un valore tangibile fisso (il dispositivo è un tablet? Lo schermo fisico ha proporzioni specifiche?), ma le risposte a queste domande potrebbero non essere utili per determinare lo spazio con cui può funzionare l'UI.

Un diagramma che mostra diversi fattori di forma dei dispositivi: uno smartphone, un pieghevole, un tablet e un laptop

Sui tablet, un'app potrebbe essere in esecuzione in modalità multi-finestra, il che significa che l'app potrebbe dividere lo schermo con un'altra app. Su ChromeOS, un'app potrebbe essere in una finestra ridimensionabile. Potrebbero esserci anche più schermi fisici, ad esempio un dispositivo pieghevole. In tutti questi casi, le dimensioni dello schermo fisico non sono pertinenti per decidere come visualizzare i contenuti.

Dovresti invece prendere decisioni in base alla parte effettiva dello schermo assegnata alla tua app, ad esempio le metriche correnti relative alla finestra fornite dalla libreria WindowManager di Jetpack. Per scoprire come utilizzare WindowManager in un'app Compose, guarda l'esempio di JetNews.

Seguendo questo approccio, la tua app sarà più flessibile, poiché si comporterà bene in tutti gli scenari precedenti. Se i layout si adattano allo spazio disponibile sullo schermo, si riduce anche la quantità di gestione speciale per supportare piattaforme come ChromeOS e fattori di forma come tablet e pieghevoli.

Dopo aver osservato lo spazio pertinente disponibile per la tua app, è utile convertire le dimensioni non elaborate in una classe di dimensioni significativa, come descritto in Classi di dimensioni finestre. Questo raggruppa le dimensioni in bucket di dimensioni standard, che sono punti di interruzione progettati per bilanciare semplicità e flessibilità per ottimizzare l'app per la maggior parte dei casi specifici. Queste classi di dimensioni fanno riferimento alla finestra complessiva dell'app; pertanto, utilizzale per prendere decisioni sul layout che incidono sul layout generale dello schermo. Puoi trasmettere queste classi di dimensioni come stato oppure eseguire logica aggiuntiva per creare uno stato derivato da trasmettere a componibili nidificati.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Questo approccio a più livelli limita la logica delle dimensioni dello schermo a un'unica posizione, invece di distribuirla nell'app in molti punti che è necessario mantenere sincronizzati. Questa singola posizione produce uno stato, che può essere trasmesso esplicitamente ad altri componibili, come faresti per qualsiasi altro stato dell'app. Lo stato di passaggio esplicito semplifica i singoli componibili, poiché si tratta di normali funzioni componibili che prendono la classe di dimensione o la configurazione specificata insieme ad altri dati.

Gli elementi componibili nidificati flessibili sono riutilizzabili

I componibili sono più riutilizzabili quando possono essere inseriti in un'ampia varietà di luoghi. Se un componibile presuppone che verrà sempre collocato in una determinata posizione con una dimensione specifica, sarà più difficile riutilizzarlo altrove in un'altra posizione o con una quantità di spazio disponibile diversa. Ciò significa anche che singoli elementi componibili riutilizzabili devono evitare implicitamente a seconda delle informazioni sulle dimensioni "globali".

Esaminiamo un esempio: immagina un componibile nidificato che implementa un layout elenco-dettagli, che può mostrare uno o due riquadri affiancati.

Screenshot di un'app che mostra due riquadri affiancati

Figura 1. Screenshot di un'app che mostra un tipico layout di elenco/dettagli. 1 è l'area dell'elenco e 2 è l'area dei dettagli.

Vogliamo che questa decisione faccia parte del layout generale dell'app, quindi trascorriamo la decisione di un componibile a livello di schermo, come abbiamo visto sopra:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

E se volessimo invece che un componibile modifichi autonomamente il proprio layout in base allo spazio disponibile? Ad esempio, una scheda che vuole mostrare ulteriori dettagli se lo spazio lo consente. Vogliamo eseguire alcune logiche in base alle dimensioni disponibili, ma a quale dimensione nello specifico?

Esempi di due schede diverse: una scheda stretta con solo un'icona e un titolo e una scheda più grande con icona, titolo e breve descrizione

Come abbiamo visto prima, dovremmo evitare di provare a utilizzare le dimensioni dello schermo effettivo del dispositivo. Questa informazione non sarà precisa per più schermi e non sarà precisa se l'app non è a schermo intero.

Poiché l'elemento componibile non è un componibile a livello di schermo, non dovremmo neanche usare direttamente le metriche correnti della finestra per massimizzare la riusabilità. Se il componente viene inserito con una spaziatura interna (ad esempio per i set) o se sono presenti componenti come binari di navigazione o barre delle app, la quantità di spazio disponibile per l'elemento componibile potrebbe essere notevolmente diversa da quella complessiva a disposizione dell'app.

Di conseguenza, dobbiamo utilizzare la larghezza data all'elemento componibile effettivamente fornito per il rendering stesso. Per ottenere questa larghezza, abbiamo due opzioni:

Se vuoi cambiare dove o come vengono visualizzati i contenuti, puoi utilizzare una raccolta di modificatori o un layout personalizzato per rendere il layout reattivo. Ad esempio, potresti assegnare ai tuoi figli più facilmente tutto lo spazio disponibile o, se c'è spazio a sufficienza, con più colonne.

Se vuoi modificare cosa mostri, puoi utilizzare BoxWithConstraints come alternativa più efficace. Questo componibile fornisce vincoli di misurazione che puoi utilizzare per chiamare diversi elementi componibili in base allo spazio disponibile. Tuttavia, ciò ha alcune spese, poiché BoxWithConstraints rinvia la composizione fino alla fase di layout, quando questi vincoli sono noti, causando l'esecuzione di ulteriori operazioni durante il layout.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

Assicurati che tutti i dati siano disponibili per dimensioni diverse

Quando utilizzi uno spazio aggiuntivo sullo schermo, su uno schermo grande potresti avere spazio per mostrare all'utente più contenuti rispetto a uno schermo piccolo. Quando implementi un elemento componibile con questo comportamento, potresti avere la tentazione di essere efficiente e di caricare i dati come effetto collaterale della dimensione attuale.

Tuttavia, questo viola i principi del flusso di dati unidirezionale, in cui i dati possono essere istruiti e semplicemente forniti a elementi componibili per il rendering appropriato. Deve essere fornito un volume sufficiente di dati all'elemento componibile in modo che abbia sempre ciò che deve essere visualizzato su qualsiasi dimensione, anche se una parte dei dati potrebbe non essere sempre utilizzata.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Partendo dall'esempio Card, tieni presente che passiamo sempre description a Card. Anche se description viene utilizzato solo quando la larghezza ne consente la visualizzazione, Card lo richiede sempre, indipendentemente dalla larghezza disponibile.

Il passaggio costante dei dati semplifica la semplificazione dei layout adattivi rendendoli meno stateful ed evita l'attivazione di effetti collaterali quando si passa da una dimensione all'altra (il che può verificarsi a causa del ridimensionamento della finestra, del cambiamento dell'orientamento o della chiusura e dell'apertura di un dispositivo).

Questo principio consente inoltre di mantenere lo stato nelle modifiche al layout. Raccogliendo informazioni che potrebbero non essere utilizzate in tutte le dimensioni, possiamo preservare lo stato dell'utente man mano che le dimensioni del layout cambiano. Ad esempio, possiamo visualizzare un flag booleano showMore in modo che lo stato dell'utente venga conservato durante i ridimensionamenti e il layout passi dalla visualizzazione della descrizione a quella nascosta e viceversa:

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

Scopri di più

Per scoprire di più sui layout personalizzati in Compose, consulta le seguenti risorse aggiuntive.

App di esempio

  • I layout canonici per schermi di grandi dimensioni sono un repository di pattern di progettazione comprovati che forniscono un'esperienza utente ottimale su dispositivi con schermi grandi
  • JetNews mostra come progettare un'app che adatta la sua UI per usare lo spazio disponibile
  • Rispondi è un esempio adattivo per il supporto di dispositivi mobili, tablet e pieghevoli
  • Ora disponibile in Android è un'app che utilizza layout adattivi per supportare schermi di dimensioni diverse

Video