Supportare diverse dimensioni di visualizzazione

Il supporto di diverse dimensioni di visualizzazione consente l'accesso alla tua app da una vasta gamma di dispositivi e da un numero maggiore di utenti.

Per supportare il maggior numero possibile di dimensioni del display, che si tratti di schermi di dispositivi diversi o di finestre di app diverse in modalità multi-finestra, progetta i layout delle app in modo che siano adattabili e reattivi. I layout adattabili/responsive offrono un'esperienza utente ottimizzata indipendentemente dalle dimensioni del display, consentendo alla tua app di adattarsi a smartphone, tablet, pieghevoli, dispositivi ChromeOS, orientamenti verticale e orizzontale e configurazioni del display ridimensionabili come la modalità schermo diviso e le finestre delle app.

I layout adattabili/responsive cambiano in base allo spazio di visualizzazione disponibile. Le modifiche vanno da piccoli aggiustamenti del layout che riempiono lo spazio (responsive design) alla sostituzione completa di un layout con un altro, in modo che l'app possa adattarsi al meglio a diverse dimensioni del display (adaptive design).

In quanto toolkit UI dichiarativo, Jetpack Compose è ideale per progettare e implementare layout che cambiano dinamicamente per visualizzare i contenuti in modo diverso su display di dimensioni diverse.

Rendere esplicite le modifiche sostanziali al layout per i componenti combinabili a livello di contenuti

I composable a livello di app e di contenuti occupano tutto lo spazio di visualizzazione disponibile per la tua app. Per questi tipi di composable, potrebbe essere opportuno modificare il layout complessivo dell'app sui display di grandi dimensioni.

Evita di utilizzare valori hardware fisici per prendere decisioni sul layout. Potrebbe essere allettante prendere decisioni basate su un valore tangibile fisso (il dispositivo è un tablet? Lo schermo fisico ha un determinato aspect ratio?), ma le risposte a queste domande potrebbero non essere utili per determinare lo spazio disponibile per la tua UI.

Figura 1. Fattori di forma di smartphone, pieghevoli, tablet e 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. In modalità finestra desktop o su ChromeOS, un'app potrebbe trovarsi in una finestra ridimensionabile. Potrebbe esserci anche più di uno schermo fisico, come nel caso di un dispositivo pieghevole. In tutti questi casi, le dimensioni fisiche dello schermo non sono pertinenti per decidere come visualizzare i contenuti.

Prendi invece decisioni in base alla porzione effettiva dello schermo allocata alla tua app descritta dalle metriche della finestra corrente fornite dalla libreria WindowManager di Jetpack. Per un esempio di come utilizzare WindowManager in un'app Compose, consulta l'esempio JetNews.

Se rendi i layout adattabili allo spazio di visualizzazione disponibile, riduci anche la quantità di gestione speciale necessaria per supportare piattaforme come ChromeOS e fattori di forma come tablet e pieghevoli.

Una volta determinate le metriche dello spazio disponibile per la tua app, converti le dimensioni non elaborate in una classe di dimensioni della finestra come descritto in Utilizzare le classi di dimensioni della finestra. Le classi di dimensioni delle finestre sono punti di interruzione progettati per bilanciare la semplicità della logica dell'app con la flessibilità di ottimizzare l'app per la maggior parte delle dimensioni di visualizzazione.

Le classi di dimensioni della finestra si riferiscono alla finestra complessiva della tua app, quindi utilizzale per le decisioni di layout che influiscono sul layout complessivo dell'app. Puoi passare le classi di dimensioni della finestra come stato oppure puoi eseguire una logica aggiuntiva per creare uno stato derivato da passare ai componenti componibili nidificati.

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Decide whether to show the top app bar based on window size class.
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)

    // MyScreen logic is based on the showTopAppBar boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Un approccio a più livelli limita la logica delle dimensioni del display a un'unica posizione anziché distribuirla in più punti dell'app che devono essere sincronizzati. Una singola posizione produce uno stato, che può essere passato esplicitamente ad altri componenti componibili proprio come qualsiasi altro stato dell'app. Il passaggio esplicito dello stato semplifica i singoli composable perché questi ultimi prendono la classe di dimensioni della finestra o la configurazione specificata insieme ad altri dati.

I composable nidificati flessibili sono riutilizzabili

I composable sono più riutilizzabili quando possono essere inseriti in una vasta gamma di posizioni. Se un componente deve essere posizionato in una posizione specifica con una dimensione specifica, è improbabile che possa essere riutilizzato in altri contesti. Ciò significa anche che i composable riutilizzabili individuali devono evitare di dipendere implicitamente dalle informazioni sulle dimensioni di visualizzazione globali.

Immagina un composable nidificato che implementa un layout elenco-dettagli, che può mostrare un singolo riquadro o due riquadri affiancati:

Un'app che mostra due riquadri affiancati.
Figura 2. App che mostra un tipico layout elenco-dettagli: 1 è l'area dell'elenco; 2, l'area dei dettagli.

La decisione elenco-dettagli deve far parte del layout generale dell'app, quindi la decisione viene trasmessa da un elemento componibile a livello di contenuti:

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

Cosa succede se invece vuoi che un composable modifichi in modo indipendente il suo layout in base allo spazio di visualizzazione disponibile, ad esempio una scheda che mostra ulteriori dettagli se lo spazio lo consente? Vuoi eseguire una logica in base a una dimensione di visualizzazione disponibile, ma quale dimensione in particolare?

Figura 3. Scheda stretta che mostra solo un'icona e un titolo e una scheda più ampia che mostra l'icona, il titolo e una breve descrizione.

Evita di provare a utilizzare le dimensioni dello schermo effettivo del dispositivo. Questo valore non sarà preciso per diversi tipi di schermi e nemmeno se l'app non è a schermo intero.

Poiché il composable non è un composable a livello di contenuti, non utilizzare direttamente le metriche della finestra corrente.

Se il componente viene posizionato con spaziatura interna (ad esempio con rientri) o se l'app include componenti come barre di navigazione o barre delle app, la quantità di spazio di visualizzazione disponibile per il composable può differire in modo significativo dallo spazio complessivo disponibile per l'app.

Utilizza la larghezza effettiva assegnata al componibile per il rendering. Hai due opzioni per ottenere questa larghezza:

  • Se vuoi modificare dove o come vengono visualizzati i contenuti, utilizza una raccolta di modificatori o un layout personalizzato per rendere il layout reattivo. Potrebbe trattarsi di un'operazione semplice come far riempire a un bambino tutto lo spazio disponibile o disporre i bambini su più colonne se c'è spazio sufficiente.

  • Se vuoi modificare cosa mostrare, utilizza BoxWithConstraints come alternativa più efficace. BoxWithConstraints fornisce vincoli di misurazione che puoi utilizzare per chiamare diversi componenti combinabili in base allo spazio di visualizzazione disponibile. Tuttavia, questo ha un costo, in quanto BoxWithConstraints posticipa la composizione fino alla fase di layout, quando questi vincoli sono noti, causando un maggiore lavoro da svolgere durante il layout.

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

Rendere disponibili tutti i dati per diverse dimensioni di visualizzazione

Quando implementi un componente componibile che sfrutta lo spazio di visualizzazione aggiuntivo, potresti essere tentato di caricare i dati in modo efficiente come effetto collaterale delle dimensioni di visualizzazione correnti.

Tuttavia, questa operazione va contro il principio del flusso di dati unidirezionale, in cui i dati possono essere sollevati e forniti ai componenti componibili per il rendering appropriato. Devono essere forniti dati sufficienti al componente componibile in modo che abbia sempre contenuti sufficienti per qualsiasi dimensione di visualizzazione, anche se una parte dei contenuti 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)
            }
        }
    }
}

Basandosi sull'esempio di Card, tieni presente che description viene sempre passato a Card. Anche se description viene utilizzato solo quando la larghezza consente la visualizzazione, Card richiede sempre description, indipendentemente dalla larghezza disponibile.

Passare sempre contenuti sufficienti semplifica i layout adattabili rendendoli meno stateful ed evita di attivare effetti collaterali quando si passa da una dimensione all'altra del display (il che può verificarsi a causa del ridimensionamento di una finestra, del cambio di orientamento o della chiusura e dell'apertura di un dispositivo).

Questo principio consente anche di conservare lo stato durante le modifiche al layout. Sollevando le informazioni che potrebbero non essere utilizzate in tutte le dimensioni di visualizzazione, puoi preservare lo stato dell'app man mano che le dimensioni del layout cambiano.

Ad esempio, puoi alzare un flag booleano showMore in modo che lo stato dell'app venga preservato quando il ridimensionamento del display fa sì che il layout passi dalla visualizzazione all'occultamento dei contenuti:

@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 saperne di più sui layout adattabili in Compose, consulta le seguenti risorse:

App di esempio

  • CanonicalLayouts è un repository di pattern di progettazione collaudati che offrono un'esperienza utente ottimale sui display di grandi dimensioni
  • JetNews mostra come progettare un'app che adatta la sua UI per sfruttare lo spazio di visualizzazione disponibile
  • Rispondi è un esempio adattivo per supportare dispositivi mobili, tablet e pieghevoli
  • Now in Android è un'app che utilizza layout adattivi per supportare diverse dimensioni di visualizzazione.

Video