Ridimensionamento delle app per Android

1. Introduzione

L'ecosistema di dispositivi Android è in continua evoluzione. Dai primi giorni delle tastiere hardware integrate al panorama moderno di flippable, pieghevoli, tablet e finestre ridimensionabili in formato libero, le app per Android non sono mai state eseguite su una serie di dispositivi più diversificata rispetto a quella attuale.

Anche se questa è un'ottima notizia per gli sviluppatori, alcune ottimizzazioni dell'app sono necessarie per soddisfare le aspettative di usabilità e offrire un'esperienza utente eccellente su schermi di diverse dimensioni. Invece di scegliere come target ogni nuovo dispositivo uno alla volta, una UI reattiva/adattiva e un'architettura resiliente possono aiutarti a far funzionare al meglio la tua app ovunque si trovino gli utenti attuali e futuri, su dispositivi di qualsiasi dimensione e forma.

L'introduzione di ambienti Android ridimensionabili in formato libero è un ottimo modo per testare la tua UI reattiva/adattiva per prepararla per qualsiasi dispositivo. Questo codelab ti guiderà nella comprensione delle implicazioni del ridimensionamento e nell'implementazione di alcune best practice per ridimensionare un'app in modo affidabile e semplice.

Cosa creerai

Esplorerai le implicazioni del ridimensionamento in formato libero e ottimizzerai un'app per Android per illustrare le best practice per il ridimensionamento. La tua app sarà in grado di:

Avere un manifest compatibile

  • Rimuovi le limitazioni che impediscono il ridimensionamento di un'app liberamente

Mantieni lo stato quando ridimensionato

  • Mantiene lo stato della UI quando viene ridimensionato utilizzando rememberSaveable
  • Evita di duplicare inutilmente il lavoro in background per inizializzare l'UI

Che cosa ti serve

  1. Conoscenza della creazione di applicazioni Android di base
  2. Conoscenza di ViewModel e stato in Compose.
  3. Un dispositivo di test che supporti il ridimensionamento delle finestre in formato libero, ad esempio uno dei seguenti:

Se riscontri problemi (bug del codice, errori grammaticali, parole poco chiare e così via) mentre lavori con il codelab, segnalali tramite il link Segnala un errore nell'angolo in basso a sinistra del codelab.

2. Per iniziare

Clona il repository da GitHub.

git clone https://github.com/android/large-screen-codelabs/

...o scaricare un file ZIP del repository ed estrarlo

Importa progetto

  • Apri Android Studio
  • Scegli Importa progetto o File->Nuovo->Importa progetto
  • Vai alla posizione in cui hai clonato o estratto il progetto
  • Apri la cartella per il ridimensionamento.
  • Apri il progetto nella cartella start. Contiene il codice di avvio.

Prova l'app

  • Crea ed esegui l'app
  • Prova a ridimensionare l'app

Cosa ne pensi?

A seconda del supporto per la compatibilità del tuo dispositivo di test, probabilmente avrai notato che l'esperienza utente non è ideale. L'app non può essere ridimensionata ed è bloccata nelle proporzioni iniziali. Che cosa sta succedendo?

Limitazioni relative ai file manifest

Se guardi nel file AndroidManifest.xml dell'app, puoi vedere che sono state aggiunte alcune limitazioni che impediscono all'app di funzionare correttamente in un ambiente di ridimensionamento delle finestre in formato libero.

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

Prova a rimuovere queste tre righe problematiche dal file manifest, ricrea l'app e riprova sul dispositivo di test. Noterai che il ridimensionamento in formato libero non è più limitato per l'app. La rimozione di limitazioni come questa dal file manifest è un passaggio importante per ottimizzare la tua app per il ridimensionamento delle finestre in formato libero.

3. Modifiche alla configurazione del ridimensionamento

Quando la finestra dell'app viene ridimensionata, la relativa Configurazione viene aggiornata. Questi aggiornamenti hanno un impatto sulla tua app. Capirli e anticiparli può aiutarti a offrire agli utenti un'esperienza straordinaria. Le modifiche più evidenti sono la larghezza e l'altezza della finestra dell'app, ma queste modifiche influiscono anche sulle proporzioni e sull'orientamento.

Osservazione delle modifiche alla configurazione in corso...

Per vedere queste modifiche in un'app realizzata con il sistema di visualizzazione Android, puoi eseguire l'override di View.onConfigurationChanged. In Jetpack Compose, abbiamo accesso a LocalConfiguration.current, che viene aggiornato automaticamente ogni volta che viene chiamato View.onConfigurationChanged.

Per visualizzare queste modifiche alla configurazione nell'app di esempio, aggiungi un componibile all'app che mostri i valori di LocalConfiguration.current oppure crea un nuovo progetto di esempio con questo componibile. Ecco un esempio di UI per la visualizzazione di questi elementi:

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

Puoi vedere un esempio di implementazione nella cartella del progetto observing-configuration-changes. Prova ad aggiungerlo all'interfaccia utente dell'app, eseguilo sul dispositivo di test e osserva l'aggiornamento dell'interfaccia utente man mano che la configurazione dell'app cambia.

Man mano che l'app viene ridimensionata, le modifiche alle informazioni di configurazione vengono visualizzate nell'interfaccia dell'app in tempo reale

Queste modifiche alla configurazione dell'app ti consentono di simulare un rapido passaggio dagli estremi previsti per lo schermo diviso di un piccolo smartphone a uno schermo intero su un tablet o un computer. Non solo è un buon metodo per testare il layout della tua app su tutti gli schermi, ma ti consente anche di verificare la capacità dell'app di gestire rapidi eventi di modifica della configurazione.

4. Logging degli eventi del ciclo di vita delle attività

Un'altra implicazione del ridimensionamento delle finestre in formato libero per la tua app sono le varie modifiche del ciclo di vita di Activity che verranno apportate alla tua app. Per vedere queste modifiche in tempo reale, aggiungi un osservatore del ciclo di vita al tuo metodo onCreate e registra ogni nuovo evento del ciclo di vita eseguendo l'override di onStateChanged.

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

Con il logging attivo, esegui di nuovo l'app sul dispositivo di test e osserva logcat mentre cerchi di ridurre a icona l'app e di riportarla in primo piano.

Osserva che la tua app viene messa in pausa quando è ridotta a icona e poi viene ripresa quando viene mostrata in primo piano. Ciò ha implicazioni per la tua app, che esplorerai nella prossima sezione di questo codelab incentrato sulla continuità.

logcat che mostra i metodi del ciclo di vita delle attività richiamati durante il ridimensionamento

Ora osserva Logcat per vedere quali callback del ciclo di vita delle attività vengono chiamati quando ridimensiona la tua app dalla dimensione più piccola alla dimensione massima possibile.

A seconda del dispositivo di test, potresti osservare comportamenti diversi, ma probabilmente avrai notato che la tua attività viene eliminata e ricreata quando le dimensioni della finestra dell'app cambiano in modo significativo, ma non quando vengono leggermente modificate. Questo perché, nell'API 24 e versioni successive, solo le modifiche significative alle dimensioni comportano Activity ricreazione.

Hai notato alcune delle modifiche più comuni alla configurazione che puoi aspettarti in un ambiente di windowing in formato libero, ma ci sono altre modifiche di cui devi essere a conoscenza. Ad esempio, se al tuo dispositivo di test è collegato un monitor esterno, vedrai che il Activity è stato eliminato e ricreato per tenere conto di modifiche alla configurazione, come la densità dello schermo.

Per astrarre parte della complessità associata alle modifiche alla configurazione, utilizza API di livello superiore, come WindowSizeClass, per implementare l'UI adattiva. Vedi anche Supporto di schermi di dimensioni diverse.

5. Continuità, mantenimento degli elementi componibili stato interno quando viene ridimensionato

Nella sezione precedente, hai notato alcune modifiche alla configurazione che la tua app può aspettarsi in un ambiente di ridimensionamento delle finestre in formato libero. In questa sezione lo stato dell'interfaccia utente dell'app sarà continuo per tutta la durata di queste modifiche.

Inizia facendo espandere la funzione componibile NavigationDrawerHeader (disponibile in ReplyHomeScreen.kt) per mostrare l'indirizzo email quando viene fatto clic.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

Dopo aver aggiunto l'intestazione espandibile all'app,

  1. esegui l'app sul dispositivo di test
  2. tocca l'intestazione per espanderla
  3. prova a ridimensionare la finestra

Quando viene ridimensionata in modo significativo, l'intestazione perde il suo stato.

L'intestazione nel riquadro di navigazione a scomparsa dell'app viene toccata per espanderla, ma si comprime dopo il ridimensionamento dell'app.

Lo stato dell'interfaccia utente viene perso perché remember ti aiuta a mantenere lo stato nelle ricomposizioni, ma non nell'attività o nella ricreazione dei processi. È pratica comune utilizzare il sollevamento dello stato, ovvero lo spostamento dello stato al chiamante di un componibile per rendere gli elementi componibili stateless, evitando così del tutto il problema. Detto questo, puoi utilizzare remember in posizioni quando mantieni lo stato di un elemento UI interno alle funzioni componibili.

Per risolvere questi problemi, sostituisci remember con rememberSaveable. Questo comando funziona perché rememberSaveable salva e ripristina il valore memorizzato in savedInstanceState. Modifica remember in rememberSaveable, esegui l'app sul dispositivo di test e prova a ridimensionarla. Noterai che lo stato dell'intestazione espandibile viene mantenuto durante il ridimensionamento, come previsto.

6. Evitare la duplicazione inutile del lavoro in background

Hai visto come utilizzare rememberSaveable per preservare l'elemento componibile stato interno dell'interfaccia utente attraverso modifiche alla configurazione che possono verificarsi spesso a seguito del ridimensionamento delle finestre in formato libero. Tuttavia, un'app dovrebbe spesso estrarre lo stato e la logica dell'UI dai componenti componibili. Il trasferimento della proprietà dello stato a un ViewModel è uno dei modi migliori per preservare lo stato durante il ridimensionamento. Quando aumenti lo stato in un ViewModel, potresti riscontrare problemi con operazioni in background a lunga esecuzione, come un accesso intensivo al file system o chiamate di rete necessarie per inizializzare lo schermo.

Per vedere un esempio dei tipi di problemi che potresti riscontrare, aggiungi un'istruzione di log al metodo initializeUIState in ReplyViewModel.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

Ora esegui l'app sul dispositivo di test e prova a ridimensionare più volte la finestra dell'app.

Quando esamini Logcat, noterai che l'app mostra che il metodo di inizializzazione è stato eseguito diverse volte. Questo può essere un problema nel caso di lavoro che vuoi eseguire una sola volta per inizializzare la tua UI. Le chiamate di rete aggiuntive, i file I/O o altre operazioni possono ostacolare le prestazioni del dispositivo e causare altri problemi involontari.

Per evitare di svolgere operazioni in background non necessarie, rimuovi la chiamata a initializeUIState() dal metodo onCreate() della tua attività. Inizializza invece i dati nel metodo init di ViewModel. Ciò garantisce che il metodo di inizializzazione venga eseguito una sola volta, quando viene creata l'istanza di ReplyViewModel per la prima volta:

init {
    initializeUIState()
}

Prova a eseguire di nuovo l'app. Vedrai che l'attività di inizializzazione simulata non necessaria viene eseguita una sola volta, indipendentemente dal numero di volte in cui ridimensiona la finestra dell'app. Questo perché i ViewModel rimangono oltre il ciclo di vita di Activity. Eseguendo l'inizializzazione del codice solo una volta al momento della creazione dell'elemento ViewModel, lo separiamo da qualsiasi ricreazione di Activity e impediamo il lavoro non necessario. Se si trattasse effettivamente di una chiamata al server costosa o di un'intensa operazione di I/O su file per inizializzare la tua UI, risparmieresti risorse significative e miglioreresti l'esperienza utente.

7. CONGRATULAZIONI

Ce l'hai fatta! Ottimo! Hai implementato alcune best practice per consentire alle app per Android di ridimensionare bene su ChromeOS e in altri ambienti multi-finestra e multischermo.

Codice sorgente di esempio

Clona il repository da GitHub

git clone https://github.com/android/large-screen-codelabs/

...o scaricare un file ZIP del repository ed estrarlo