Supportare diverse dimensioni di visualizzazione

Il supporto di dimensioni diverse del display consente di accedere alla tua app alla più ampia gamma di dispositivi e al maggior numero di utenti.

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

I layout adattabili/reattivi 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 dimensioni diverse del display (design adattivo).

In qualità di 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 composabili a livello di contenuti

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

Evita di utilizzare i valori hardware fisici per prendere decisioni sul layout. Potrebbe essere temptinge prendere decisioni in base a un valore tangibile fisso (il dispositivo è un tablet? Lo schermo fisico ha un determinato formato?), 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, dispositivi pieghevoli, tablet e laptop

Sui tablet, un'app potrebbe essere in esecuzione in modalità multifinestra, il che significa che potrebbe dividere lo schermo con un'altra app. In modalità finestra del computer o su ChromeOS, un'app potrebbe trovarsi in una finestra ridimensionabile. Potrebbe anche essere presente più di un suo 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 invece prendere decisioni in base alla parte effettiva dello schermo assegnata alla tua app descritta dalle metriche della finestra correnti fornite dalla libreria WindowManager di Jetpack. Per un esempio di come utilizzare WindowManager in un'app Compose, consulta l'esempio JetNews.

Se rendi i tuoi 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 dispositivi pieghevoli.

Dopo aver determinato 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 della finestra 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 dei display. Le classi di dimensioni della finestra si riferiscono alla finestra complessiva dell'app, quindi utilizzale per le decisioni di layout che influiscono sul layout complessivo dell'app. Puoi passare le classi di dimensione della finestra come stato oppure puoi eseguire una logica aggiuntiva per creare uno stato derivato da trasmettere ai composabili nidificati.

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

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

Un approccio a più livelli limita la logica delle dimensioni dei display a un'unica posizione anziché spargerla nell'app in molti punti che devono essere mantenuti sincronizzati. Una singola posizione genera uno stato che può essere trasmesso esplicitamente ad altri composabili come qualsiasi altro stato dell'app. Il passaggio esplicito dello stato semplifica i singoli composabili perché questi assumono la classe della dimensione della finestra o la configurazione specificata insieme ad altri dati.

I composabili nidificati flessibili sono riutilizzabili

I composabili sono più riutilizzabili se possono essere posizionati in una vasta gamma di luoghi. Se un composable deve essere posizionato in una posizione specifica con dimensioni specifiche, è improbabile che possa essere riutilizzato in altri contesti. Ciò significa anche che i singoli componenti composable riutilizzabili devono evitare di dipendere implicitamente dalle informazioni sulle dimensioni dello schermo 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 layout di elenco e dettagli tipico: 1 è l'area dell'elenco; 2, l'area dei dettagli.

La decisione relativa ai dettagli dell'elenco deve far parte del layout complessivo dell'app, pertanto la decisione viene trasmessa da un composable a livello di contenuti:

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

E se invece vuoi che un composable modifichi autonomamente il layout in base allo spazio di visualizzazione disponibile, ad esempio una scheda che mostri dettagli aggiuntivi se lo spazio lo consente? Vuoi eseguire una logica in base a una dimensione dello schermo disponibile, ma di quale dimensione nello specifico?

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

Evita di utilizzare le dimensioni dello schermo effettivo del dispositivo. Il risultato non sarà accurato per diversi tipi di schermi e non sarà accurato 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 è posizionato con spaziatura interna (ad esempio con rientranze) o se l'app include componenti come barre di navigazione o barre app, la quantità di spazio di visualizzazione disponibile per il composable può differire notevolmente dallo spazio complessivo disponibile per l'app.

Utilizza la larghezza effettivamente assegnata al composable 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 adattabile. Potrebbe essere sufficiente che un elemento figlio occupi tutto lo spazio disponibile o disporre gli elementi figlio in più colonne se lo spazio è sufficiente.

  • Se vuoi modificare cosa mostrare, utilizza BoxWithConstraints come alternativa più efficace. BoxWithConstraints fornisce limiti di misurazione che puoi utilizzare per chiamare diversi composabili in base allo spazio di visualizzazione disponibile. Tuttavia, questo comporta un costo, poiché BoxWithConstraints rimanda la composizione alla fase di layout, quando questi vincoli sono noti, causando un maggiore lavoro 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 diverse dimensioni di visualizzazione

Quando implementi un composable che sfrutta uno spazio di visualizzazione aggiuntivo, potresti essere tentato di essere efficiente e caricare i dati come effetto collaterale delle dimensioni dello schermo attuali.

Tuttavia, questa operazione è in contrasto con il principio del flusso di dati unidirezionale, in cui i dati possono essere sollevati e forniti ai composabili per il rendering appropriato. È necessario fornire al composable dati sufficienti in modo che abbia sempre contenuti sufficienti per qualsiasi dimensione di visualizzazione, anche se 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)
            }
        }
    }
}

Nell'esempio Card, tieni presente che description viene sempre passato al Card. Anche se description viene utilizzato solo quando la larghezza consente di visualizzarlo, 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 di visualizzazione all'altra (che può verificarsi a causa di una ridimensionamento della finestra, di una modifica dell'orientamento o del ripiegamento e dell'apertura di un dispositivo).

Questo principio consente inoltre di preservare lo stato durante le modifiche del layout. Se rimuovi le informazioni che potrebbero non essere utilizzate per tutte le dimensioni del display, puoi mantenere invariato lo stato dell'app quando le dimensioni del layout cambiano. Ad esempio, puoi impostare un showMore flag booleano in modo che lo stato dell'app venga mantenuto quando il ridimensionamento del display fa sì che il layout passi dal nascondere i contenuti alla loro visualizzazione:

@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 adattabili in Compose, consulta le seguenti risorse:

App di esempio

  • CanonicalLayouts è un repository di pattern di progettazione comprovati che offrono un'esperienza utente ottimale su display di grandi dimensioni
  • JetNews mostra come progettare un'app che adatti la propria UI per sfruttare lo spazio di visualizzazione disponibile
  • Rispondi è un sample adattabile per il supporto di dispositivi mobili, tablet e pieghevoli
  • Ora su Android è un'app che utilizza layout adattivi per supportare diversi formati di visualizzazione

Video