Creare layout adattivi

L'interfaccia utente della tua app deve adattarsi a diverse dimensioni dello schermo, orientamenti e fattori di forma. Un layout adattivo cambia in base allo spazio disponibile sullo schermo. Queste modifiche vanno da semplici aggiustamenti di layout per riempire lo spazio, a modifiche del layout completamente per utilizzare spazio aggiuntivo.

In quanto toolkit dell'interfaccia utente dichiarativa, Jetpack Compose è adatto a progettare e implementare layout che si adattino per visualizzare i contenuti in modo diverso su diverse dimensioni. Questo documento contiene alcune linee guida su come utilizzare Compose per rendere reattiva la tua UI.

Apportare modifiche significative al layout per elementi componibili a livello di schermo

Quando usi Compose per disporre un'intera applicazione, i componenti componibili a livello di applicazione e di schermata occupano tutto lo spazio concesso all'applicazione per il rendering. A questo livello del design, potrebbe essere utile modificare il layout generale di una schermata per sfruttare schermi più grandi.

Evita di utilizzare valori fisici, hardware per prendere decisioni sul layout. Potresti essere tentata di prendere decisioni in base a 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 su cui può essere utilizzata l'UI.

Un diagramma che mostra diversi fattori di forma del dispositivo: telefono, pieghevole, tablet e laptop

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

Devi prendere decisioni in base alla porzione effettiva dello schermo allocata alla tua app, ad esempio le metriche attuali relative alla finestra fornite dalla libreria WindowManager di Jetpack. Per scoprire come utilizzare WindowManager in un'app Compose, guarda l'esempio JetNews.

Seguendo questo approccio rendete la vostra app più flessibile, in quanto si comporterà bene in tutti gli scenari descritti in precedenza. La possibilità di adattare i layout allo spazio dello schermo a loro disposizione 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 l'app, è utile convertire le dimensioni non elaborate in una classe di dimensioni significativa, come descritto in Classi di dimensioni della finestra. Questo raggruppa le dimensioni in bucket di dimensioni standard, che sono punti di interruzione progettati per trovare un equilibrio tra la semplicità e la flessibilità per ottimizzare l'app per la maggior parte dei casi unici. Queste classi di dimensioni si riferiscono alla finestra complessiva dell'app, pertanto utilizzale per decidere il layout che influisce sul layout dello schermo complessivo. Puoi passare queste classi di dimensioni come stato oppure eseguire una logica aggiuntiva per creare uno stato derivato da passare 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, anziché distribuirla nella tua app in molte posizioni che deve essere sincronizzata. Questa singola posizione produce uno stato, che può essere trasmesso esplicitamente ad altri componenti come per qualsiasi altro stato dell'app. Lo stato di passaggio esplicito semplifica i singoli componenti, poiché saranno semplicemente funzioni componibili normali che prendono la classe di dimensione o la configurazione specificata insieme ad altri dati.

I componenti flessibili nidificati di elementi riutilizzabili

Gli oggetti componibili sono più riutilizzabili quando possono essere collocati in una vasta gamma di punti. Se un elemento componibile presuppone che sia sempre posizionato in una determinata posizione con dimensioni specifiche, sarà più difficile riutilizzarlo in un'altra posizione o con una quantità di spazio diversa. Questo significa anche che i composti singoli e riutilizzabili devono evitare implicitamente in base alle informazioni sulle dimensioni "globali".

Vediamo un esempio: immagina un composito composta che implementa un layout di dettaglio dell'elenco, che può mostrare uno o due riquadri affiancati.

Screenshot di un'app che mostra due riquadri uno accanto all'altro

Screenshot di un'app che mostra il tipico layout di elenco/dettagli. 1 è l'area elenco, 2 è l'area dei dettagli.

Vogliamo che questa decisione venga inclusa nel layout generale dell'app, pertanto trasferiamo la decisione da un'immagine componibile a livello di schermata, come abbiamo visto sopra:

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

E se volessimo cambiare il layout in modo indipendente in base allo spazio disponibile? Ad esempio, una scheda che desidera mostrare ulteriori dettagli se lo spazio lo consente. Vogliamo eseguire una logica in base alle dimensioni disponibili, ma a quale dimensione in modo specifico?

Esempi di due schede diverse: una scheda ristretta che mostra solo un'icona e un titolo e una scheda più ampia che mostra l'icona, il titolo e una breve descrizione

Come abbiamo visto sopra, dobbiamo evitare di provare a usare le dimensioni dello schermo effettivo del dispositivo. Questo non sarà preciso per più schermi e non sarà preciso se l'app non è a schermo intero.

Poiché il formato Composito non è componibile a livello di schermata, non dobbiamo utilizzare direttamente le metriche correnti della finestra per massimizzare la riutilizzabilità. Se il componente viene posizionato con una spaziatura interna (ad esempio per gli inserti) o se sono presenti componenti come barre di navigazione o barre delle app, la quantità di spazio disponibile per l'elemento componibile può essere notevolmente diversa dallo spazio complessivo disponibile per l'app.

Dobbiamo quindi usare la larghezza data dalla composizione per creare automaticamente il rendering. Abbiamo due opzioni per ottenere questa larghezza:

Se vuoi modificare dove o come vengono visualizzati i contenuti, puoi utilizzare una serie di modificatori o un layout personalizzato per rendere il layout adattabile. Questo potrebbe essere tanto semplice quanto fare in modo che alcuni bambini occupino tutto lo spazio disponibile oppure disporre dei bambini con più colonne se lo spazio è sufficiente.

Se vuoi modificare cosa viene mostrato, puoi utilizzare BoxWithConstraints come alternativa più efficace. Questo oggetto componibile fornisce i vincoli di misurazione che puoi utilizzare per chiamare diversi componibili in base allo spazio disponibile. Tuttavia, questo comporta un certo disagio, dal momento che BoxWithConstraints rimanda la composizione fino alla fase di layout, quando sono noti questi vincoli, 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 sfrutti lo spazio aggiuntivo sullo schermo, su uno schermo di grandi dimensioni potresti avere spazio per mostrare più contenuti all'utente rispetto a uno schermo piccolo. Quando implementi un elemento componibile con questo comportamento, potrebbe essere allettante l'efficienza e caricare i dati come effetto collaterale delle dimensioni correnti.

Tuttavia, ciò vale per i principi del flusso di dati unidirezionale, in cui i dati possono essere ispezionati e forniti semplicemente ai dati componibili per un rendering appropriato. È necessario fornire dati sufficienti per un'immagine componibile in modo che l'elemento componibile abbia sempre il contenuto necessario per essere visualizzato in qualsiasi dimensione, anche se 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 il description all'Card. description viene utilizzato solo quando la larghezza consente la visualizzazione, mentre Card lo richiede sempre, indipendentemente dalla larghezza disponibile.

Il trasferimento dei dati semplifica sempre i layout adattivi rendendoli meno stateful ed evita di attivare effetti collaterali quando si passa da una dimensione all'altra (il che può verificarsi a causa del ridimensionamento di una finestra, del cambio di orientamento o della piegatura e dell'apertura di un dispositivo).

Questo principio consente anche di preservare lo stato nelle modifiche al layout. Sollevando le informazioni che non possono essere utilizzate in tutte le dimensioni, possiamo preservare lo stato dell'utente quando le dimensioni del layout cambiano. Ad esempio, possiamo isolare un flag booleano showMore in modo che lo stato dell'utente venga preservato quando le dimensioni aumentano, cambiando così il layout tra visualizzazione e descrizione:

@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

  • Layout canonici per schermi di grandi dimensioni: è un repository di pattern di progettazione comprovati che offrono un'esperienza utente ottimale sui dispositivi con schermi di grandi dimensioni.
  • JetNews mostra come progettare un'app che adatti la sua interfaccia utente per sfruttare lo spazio disponibile
  • Risposta è un campione adattivo che supporta dispositivi mobili, tablet e pieghevoli
  • Ora in Android è un'app che usa i layout adattivi per supportare schermi di dimensioni diverse

Video