Administra los cambios en la configuración

Algunas configuraciones del dispositivo pueden cambiar mientras se ejecuta la app. Estos cambios incluyen, entre otros:

  • El tamaño de visualización de la app
  • Orientación de la pantalla
  • Tamaño y grosor de la fuente
  • Configuración regional
  • Diferencias entre el modo oscuro y el modo claro
  • Disponibilidad del teclado

La mayoría de estos cambios en la configuración se deben a algunas interacciones del usuario. Por ejemplo, cuando se rota o pliega el dispositivo, cambia la cantidad de espacio de pantalla disponible para tu app. Del mismo modo, modificar los parámetros de configuración del dispositivo, como el tamaño de la fuente, el idioma o el tema preferido, cambia sus valores respectivos en el objeto Configuration.

Estos parámetros suelen requerir cambios lo suficientemente grandes en la IU de tu app como para que la plataforma de Android tenga un mecanismo específico destinado a ellos. Este mecanismo es la recreación de Activity.

Recreación de actividades

El sistema recrea una Activity cuando se produce un cambio de configuración. Para ello, el sistema llama a onDestroy() y destruye la instancia de Activity existente. Luego, crea una instancia nueva con onCreate(), y esta nueva instancia de Activity se inicializa con la configuración nueva y actualizada. Esto también significa que el sistema recrea la IU con la nueva configuración.

El comportamiento de recreación ayuda a tu app a adaptarse a las nuevas configuraciones, ya que vuelve a cargarse de forma automática con recursos alternativos que coinciden con la nueva configuración del dispositivo.

Ejemplo de recreación

Considera un objeto TextView que muestra un título estático con android:text="@string/title", como se define en un archivo en formato XML de diseño. Cuando se crea la vista, el texto se configura exactamente una vez, según el idioma actual. Si el idioma cambia, el sistema vuelve a crear la actividad. En consecuencia, el sistema también recrea la vista y la inicializa con el valor correcto según el lenguaje nuevo.

Además, la recreación borra todo estado que se haya conservado como campo en la Activity, en cualquiera de sus objetos Fragment o View, o en otros objetos contenidos. Esto se debe a que la recreación de Activity crea una instancia completamente nueva de la Activity y de la IU. Además, la Activity anterior ya no será visible ni válida, por lo que cualquier referencia que quede de ella o de sus objetos contenidos estará inactiva. Es posible que provoque errores, fugas de memoria y fallas.

Expectativas de los usuarios

El usuario de una app espera que se preserve el estado. Cuando un usuario completa un formulario y abre otra app en el modo multiventana para consultar información, su experiencia no será buena si regresa y los datos del formulario se borraron o si regresa a otra parte de la app. Como desarrollador, debes proporcionar una experiencia del usuario coherente que no se vea afectada por los cambios de configuración y la recreación de actividades.

Para verificar si el estado se preserva en tu aplicación, puedes realizar acciones que provocan cambios de configuración mientras la app está en primer plano y cuando está en segundo plano. Estas acciones incluyen lo siguiente:

  • Rotar el dispositivo
  • Ingresar al modo multiventana
  • Cambiar el tamaño de la app en el modo multiventana o en una ventana de formato libre
  • Plegar un dispositivo plegable con varias pantallas
  • Cambiar el tema del sistema, como del modo oscuro al modo claro
  • Cambiar el tamaño de la fuente
  • Cambiar el idioma del sistema o de la app
  • Conectar o desconectar un teclado de hardware
  • Conectar o desconectar un conector

Existen tres enfoques principales que puedes adoptar para preservar el estado relevante ante una recreación de Activity. Para determinar cuál usar, debes considerar el tipo de estado que quieres preservar:

  • La persistencia local para controlar el cierre del proceso en el caso de datos complejos o grandes (el almacenamiento local persistente incluye bases de datos o DataStore)
  • Objetos retenidos, como instancias de ViewModel, para controlar el estado relacionado con la IU en la memoria mientras el usuario usa la app de forma activa
  • Estado de la instancia guardada para controlar el cierre del proceso iniciado por el sistema y mantener el estado transitorio que depende de las entradas del usuario o de la navegación

Para obtener información detallada sobre las APIs de cada enfoque y cuándo es apropiado usarlas, consulta Cómo guardar estados de la IU.

Cómo restringir la recreación de actividades

Puedes evitar la recreación de actividades automática para ciertos cambios de configuración. La recreación de Activity genera la recreación de la IU completa y de cualquier objeto derivado de la Activity. Tal vez tengas buenas razones para evitarla. Por ejemplo, es posible que tu app no necesite actualizar recursos durante un cambio de configuración específico, o tal vez tengas una limitación de rendimiento. En ese caso, puedes declarar que tu actividad maneja el cambio de configuración por sí misma y evitar que el sistema la reinicie.

Para inhabilitar la recreación de actividades por ciertos cambios de configuración, agrega el tipo de configuración a android:configChanges en la entrada <activity> de tu archivo AndroidManifest.xml. Los valores posibles aparecen en la documentación del atributo android:configChanges.

El siguiente código de manifiesto inhabilita la recreación de Activity de MyActivity cuando cambia la orientación de la pantalla y la disponibilidad del teclado:

<activity
    android:name=".MyActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
    android:label="@string/app_name">

Algunos cambios de configuración siempre hacen que la actividad se reinicie. No puedes inhabilitarlos. Por ejemplo, no puedes inhabilitar el cambio de colores dinámicos que se introdujo en Android 12L (nivel de API 32).

Cómo reaccionar a los cambios de configuración en el sistema de View

En el sistema de View, cuando se produce un cambio de configuración para el que se inhabilitó la recreación de Activity, la actividad recibe una llamada a Activity.onConfigurationChanged(). Las vistas adjuntas también reciben una llamada a View.onConfigurationChanged(). En el caso de los cambios de configuración que no hayas agregado a android:configChanges, el sistema recreará la actividad como de costumbre.

El método de devolución de llamada onConfigurationChanged() recibe un objeto Configuration que especifica la nueva configuración del dispositivo. Lee los campos del objeto Configuration para determinar cuál es tu configuración nueva. Para realizar los cambios posteriores, actualiza los recursos que usas en tu interfaz. Cuando el sistema llama a este método, el objeto Resources de tu actividad se actualiza para devolver recursos basados en la nueva configuración. Esto te permite restablecer elementos de la IU sin que el sistema reinicie la actividad.

Por ejemplo, la siguiente implementación de onConfigurationChanged() verifica si hay algún teclado disponible:

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show()
    } else if (newConfig.keyboardHidden === Configuration.KEYBOARDHIDDEN_NO) {
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show()
    }
}

Java

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks whether a keyboard is available
    if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_YES) {
        Toast.makeText(this, "Keyboard available", Toast.LENGTH_SHORT).show();
    } else if (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO){
        Toast.makeText(this, "No keyboard", Toast.LENGTH_SHORT).show();
    }
}

Si no necesitas actualizar tu aplicación según estos cambios de configuración, como alternativa, puedes optar por no implementar onConfigurationChanged(). En ese caso, todos los recursos utilizados antes del cambio de configuración se siguen utilizando, y solamente evitas el reinicio de tu actividad. Por ejemplo, es posible que una app para TVs no reaccione cuando se conecta o desconecta un teclado Bluetooth.

Estado de retención

Cuando usas esta técnica, debes conservar el estado durante el ciclo de vida normal de la actividad. Esto se debe a los siguientes motivos:

  • Cambios inevitables: Los cambios de configuración que no puedes evitar pueden reiniciar tu aplicación.
  • Cierre del proceso: Tu aplicación debe poder controlar el cierre del proceso iniciado por el sistema. Si el usuario abandona tu aplicación y esta pasa a segundo plano, es posible que el sistema la destruya.

Cómo reaccionar a los cambios de configuración en Jetpack Compose

Jetpack Compose permite que tu app reaccione con mayor facilidad a los cambios de configuración. Sin embargo, si inhabilitas la recreación de Activity para todos los cambios de configuración en los que es posible hacerlo, la app debe controlar correctamente los cambios de configuración.

El objeto Configuration está disponible en la jerarquía de la IU de Compose con el elemento local de composición de LocalConfiguration. Cada vez que cambia, se vuelven a componer las funciones de componibilidad que leen de LocalConfiguration.current. Para obtener información sobre cómo funcionan los elementos locales de composición, consulta Datos de alcance local con CompositionLocal.

Ejemplo

En el siguiente ejemplo, un elemento componible muestra una fecha con un formato específico. Este objeto componible reacciona a los cambios de configuración regional del sistema llamando a ConfigurationCompat.getLocales() con LocalConfiguration.current.

@Composable
fun DateText(year: Int, dayOfYear: Int) {
    val dateTimeFormatter = DateTimeFormatter.ofPattern(
        "MMM dd",
        ConfigurationCompat.getLocales(LocalConfiguration.current)[0]
    )
    Text(
        dateTimeFormatter.format(LocalDate.ofYearDay(year, dayOfYear))
    )
}

Para evitar la recreación de Activity cuando cambia la configuración regional, la Activity que aloja el código de Compose debe inhabilitar los cambios de configuración regional. Para hacerlo, configura android:configChanges como locale|layoutDirection.

Cambios de configuración: conceptos clave y prácticas recomendadas

Estos son los conceptos clave que debes conocer cuando trabajas en cambios de configuración:

  • Configuraciones: Las configuraciones del dispositivo definen cómo debe mostrarse la IU al usuario; por ejemplo, el tamaño de visualización de la app, la configuración regional o el tema del sistema.
  • Cambios de configuración: Las configuraciones cambian debido a la interacción del usuario. Por ejemplo, el usuario puede cambiar la configuración del dispositivo o cómo interactúa físicamente con él. No hay forma de evitar los cambios de configuración.
  • Recreación de Activity: Los cambios de configuración generan la recreación de Activity de forma predeterminada. Este es un mecanismo integrado para reinicializar el estado de la app para la configuración nueva.
  • Destrucción de Activity: La recreación de Activity hace que el sistema destruya la instancia de Activity anterior y cree una nueva en su lugar. La instancia anterior es obsoleta. Cualquier referencia restante hace que se produzcan fugas de memoria, errores o fallas.
  • Estado: El estado de la instancia de Activity anterior no está presente en la instancia de Activity nueva, ya que son dos instancias de objeto diferentes. Conserva el estado de la app y del usuario, como se describe en Cómo guardar estados de la IU.
  • Inhabilitación: Inhabilitar la recreación de actividades para un tipo de cambio de configuración es una posible optimización. Requiere que tu app se actualice de forma adecuada en respuesta a la nueva configuración.

Para proporcionar una buena experiencia del usuario, ten en cuenta las siguientes prácticas recomendadas:

  • Prepárate para cambios de configuración frecuentes: No supongas que los cambios de configuración son poco frecuentes o nunca ocurren, independientemente del nivel de API, el factor de forma o el kit de herramientas de la IU. Cuando un usuario genera un cambio de configuración, espera que las apps se actualicen y continúen funcionando de forma correcta con la nueva configuración.
  • Conserva el estado: No pierdas el estado del usuario cuando se produzca una recreación de Activity. Conserva el estado como se describe en Cómo guardar estados de la IU.
  • Evita recurrir a inhabilitar como una solución rápida: No inhabilites la recreación de Activity como un atajo para evitar la pérdida de estado. Inhabilitar la recreación de actividades requiere que cumplas con la promesa de controlar el cambio, y aún puedes perder el estado debido a la recreación de Activity a partir de otros cambios de configuración, el cierre del proceso o el cierre de la app. Es imposible inhabilitar por completo la recreación de Activity. Conserva el estado como se describe en Cómo guardar estados de la IU.
  • No evites los cambios de configuración: No establezcas restricciones con respecto a la orientación, la relación de aspecto ni el cambio de tamaño para evitar los cambios de configuración y la recreación de Activity. Esto afectará de forma negativa a los usuarios que prefieran usar tu app de una manera determinada.

Cómo controlar los cambios de configuración basados en el tamaño

Los cambios de configuración basados en el tamaño pueden ocurrir en cualquier momento y son más probables cuando tu app se ejecuta en un dispositivo de pantalla grande en el que los usuarios pueden ingresar en el modo multiventana. Los usuarios esperan que tu app funcione bien en ese entorno.

Existen dos tipos generales de cambios de tamaño: significativos y mínimos. Un cambio de tamaño significativo es aquel en el que un conjunto diferente de recursos alternativos se aplica a la nueva configuración debido a una diferencia en el tamaño de la pantalla, como el ancho, la altura o el ancho mínimo. Entre estos recursos, se incluyen aquellos que la app define por sí misma y los de cualquiera de sus bibliotecas.

Cómo restringir la recreación de actividades para los cambios de configuración basados en el tamaño

Cuando inhabilitas la recreación de Activity para cambios de configuración basados en el tamaño, el sistema no recrea la Activity. En su lugar, recibe una llamada a Activity.onConfigurationChanged(). Todas las vistas adjuntas reciben una llamada a View.onConfigurationChanged().

La recreación de Activity está inhabilitada para los cambios de configuración basados en el tamaño cuando tienes android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout" en tu archivo de manifiesto.

Cómo permitir la recreación de actividades para los cambios de configuración basados en el tamaño

En Android 7.0 (nivel de API 24) y versiones posteriores, la recreación de Activity solo se produce para los cambios de configuración basados en el tamaño si el cambio es significativo. Cuando el sistema no recrea una Activity debido a un tamaño insuficiente, el sistema puede llamar a Activity.onConfigurationChanged() y a View.onConfigurationChanged() en su lugar.

Ten en cuenta las siguientes advertencias sobre las devoluciones de llamada de Activity y View cuando no se recrea Activity:

  • En Android 11 (nivel de API 30) hasta Android 13 (nivel de API 33), no se llama a Activity.onConfigurationChanged().
  • Hay un problema conocido en el que es posible que no se llame a View.onConfigurationChanged() en algunos casos en Android 12L (nivel de API 32) y versiones anteriores de Android 13 (nivel de API 33). Para obtener más información, consulta este problema público. Esto se resolvió en versiones posteriores de Android 13 y en Android 14.

Para el código que depende de la detección de cambios de configuración basados en el tamaño, te recomendamos que uses una View de utilidad con un View.onConfigurationChanged() anulado, en lugar de depender de la recreación de Activity o de Activity.onConfigurationChanged().