Supportare diverse dimensioni di visualizzazione

Il supporto di diverse dimensioni di visualizzazione consente l'accesso alla tua app da parte della più ampia varietà di dispositivi e del maggior numero di utenti.

Per supportare il maggior numero possibile di dimensioni di visualizzazione, sia 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/reattivi offrono un'esperienza utente ottimizzata indipendentemente dalle dimensioni di visualizzazione, consentendo alla tua app di adattarsi a smartphone, tablet, pieghevoli, dispositivi ChromeOS, orientamenti verticale e orizzontale e configurazioni di visualizzazione 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 (design reattivo) alla sostituzione completa di un layout con un altro, in modo che l'app possa adattarsi al meglio a diverse dimensioni di visualizzazione (design adattivo).

In quanto toolkit UI dichiarativo, Jetpack Compose è ideale per progettare e implementare layout che cambiano dinamicamente per eseguire il rendering dei contenuti in modo diverso su diverse dimensioni di visualizzazione.

Rendere esplicite le modifiche di layout di grandi dimensioni per i composable 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 utile modificare il layout complessivo dell'app su schermi di grandi dimensioni.

Evita di utilizzare i valori dell'hardware fisico 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 formato?), ma le risposte a queste domande potrebbero non essere utili per determinare lo spazio disponibile per l'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 anche essere presente più di uno schermo fisico, ad esempio con un dispositivo pieghevole. In tutti questi casi, le dimensioni dello schermo fisico non sono pertinenti per decidere come visualizzare i contenuti.

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

Rendere i layout adattabili allo spazio di visualizzazione disponibile riduce anche la quantità di gestione speciale necessaria per supportare piattaforme come ChromeOS e fattori di forma come tablet e 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 di visualizzazione.

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 dimensioni della finestra come stato oppure eseguire una logica aggiuntiva per creare uno stato derivato da passare ai composable nidificati.

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).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 livelli limita la logica delle dimensioni di visualizzazione a una singola posizione anziché distribuirla nell'app in molti punti che devono essere sincronizzati. Una singola posizione produce uno stato, che può essere passato esplicitamente ad altri composable come qualsiasi altro stato dell'app. Il passaggio esplicito dello stato semplifica i singoli composable perché questi 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 un'ampia varietà di posizioni. Se un composable deve essere inserito in una posizione specifica con una dimensione specifica, è improbabile che sia riutilizzabile in altri contesti. Ciò significa anche che i composable individuali e riutilizzabili devono evitare di dipendere implicitamente dalle informazioni sulle dimensioni di visualizzazione globali.

Immagina un composable nidificato che implementa un layout list-detail, 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 list-detail: 1 è l'area dell'elenco; 2, l'area dei dettagli.

La decisione list-detail deve far parte del layout complessivo dell'app, quindi la decisione viene passata da un composable a livello di contenuti:

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

Cosa succede se invece vuoi che un composable modifichi autonomamente il suo layout in base allo spazio di visualizzazione disponibile, ad esempio una scheda che mostra dettagli aggiuntivi se lo spazio lo consente? Vuoi eseguire una logica basata su una dimensione di visualizzazione disponibile, ma quale 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 non sarà accurato per i 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 inserito con un padding (ad esempio con gli inset) o se l'app include componenti come barre di navigazione o barre delle app, la quantità di spazio di visualizzazione disponibile per il composable potrebbe differire in modo significativo 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 reattivo. Potrebbe essere semplice come far sì che un elemento figlio riempia tutto lo spazio disponibile o disporre gli elementi figli con più colonne se c'è spazio sufficiente.

  • Se vuoi modificare cosa mostrare, utilizza BoxWithConstraints come a alternativa più potente. BoxWithConstraints fornisce vincoli di misurazione che puoi utilizzare per chiamare composable diversi in base allo spazio di visualizzazione disponibile. Tuttavia, questo ha un costo, poiché BoxWithConstraints posticipa la composizione fino 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(/* ... */)
            }
        }
    }
}

Rendere disponibili tutti i dati per diverse dimensioni di visualizzazione

Quando implementi un composable che sfrutta lo spazio di visualizzazione aggiuntivo, potresti essere tentato di essere efficiente e caricare i dati 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 composable per il rendering appropriato. Al composable devono essere forniti dati sufficienti 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)
            }
        }
    }
}

Basandoci sull'esempio di Card, tieni presente che la description viene sempre passata a Card. Anche se la description viene utilizzata solo quando la larghezza ne consente la visualizzazione, Card richiede sempre la description, indipendentemente dalla larghezza disponibile.

Il passaggio costante di contenuti sufficienti semplifica i layout adattivi 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 un ridimensionamento della finestra, di una modifica dell'orientamento o della piegatura e dell'apertura di un dispositivo).

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

Ad esempio, puoi sollevare un flag booleano showMore in modo che lo stato dell'app venga conservato quando il ridimensionamento della visualizzazione fa sì che il layout passi dalla visualizzazione alla nascondimento 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 scoprire di più sui layout adattivi in Compose, consulta le seguenti risorse:

App di esempio

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

Video