Cómo controlar cambios de configuración

IU responsiva y navegación

A fin de brindar la mejor experiencia de navegación posible a los usuarios, deberías proporcionar una IU de navegación que se adapte al ancho, la altura y el ancho mínimo del dispositivo del usuario. Te recomendamos que uses una barra de la app inferior, un panel lateral de navegación que se muestre en todo momento o que se pueda contraer, un riel o tal vez algo completamente nuevo en función del espacio de pantalla disponible y el estilo único de tu app.

ejemplos de un riel, paneles laterales de navegación y una barra de la app inferior
Figura 1: Ejemplos de un riel, paneles laterales de navegación y una barra de app inferior

La guía de arquitectura de productos de Material Design proporciona más contexto y consideraciones para compilar una IU responsiva (es decir, una IU que se adapte en forma dinámica a los cambios en el entorno). Algunos ejemplos de este tipo de cambios incluyen ajustes en el ancho, la altura, la orientación y la preferencia de idioma del usuario. Estas propiedades del entorno se denominan en forma colectiva la configuración del dispositivo.

Cuando una o más de esas propiedades cambian durante el tiempo de ejecución, el SO Android responde destruyendo y volviendo a crear las actividades y los fragmentos de tu app. Por lo tanto, lo mejor que puedes hacer para admitir una IU responsiva en Android es asegurarte de usar calificadores de configuración de recursos cuando corresponda y evitar el uso de tamaños de diseño hard-coded.

Cómo implementar la navegación global en una IU responsiva

La implementación de la navegación global como parte de una IU responsiva comienza con la actividad que aloja tu gráfico de navegación. Si quieres ver un ejemplo práctico, consulta el Codelab de Navigation. El codelab usa una NavigationView para mostrar el menú de navegación, como se muestra en la figura 2. Cuando se ejecuta en un dispositivo que renderiza en un ancho de al menos 960 dp, esta NavigationView siempre está en pantalla.

el codelab de Navigation usa una vista de navegación que siempre está visible cuando el ancho del dispositivo es de al menos 960 dp
Figura 2: El codelab de Navigation usa un NavigationView para mostrar el menú de navegación

Otros tamaños y orientaciones de dispositivos cambian de forma dinámica entre DrawerLayout o BottomNavigationView según sea necesario.

una bottomnavigationview y un drawerlayout, utilizados en el menú de navegación según sea necesario en diseños de dispositivos más pequeños
Figura 3: El codelab de Navigation usa BottomNavigationView y DrawerLayout para mostrar el menú de navegación en dispositivos más pequeños

Puedes implementar este comportamiento si creas tres diseños diferentes, en los que cada uno defina los elementos de navegación deseados y la jerarquía de vistas según la configuración actual del dispositivo.

La configuración a la que se aplica cada diseño se determina según la estructura de directorios en la que se ubica el archivo de diseño. Por ejemplo, el archivo de diseño NavigationView se encuentra en el directorio res/layout-w960dp.

<!-- res/layout-w960dp/navigation_activity.xml -->
<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <com.google.android.material.navigation.NavigationView
       android:id="@+id/nav_view"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_alignParentStart="true"
       app:elevation="0dp"
       app:headerLayout="@layout/nav_view_header"
       app:menu="@menu/nav_drawer_menu" />

   <View
       android:layout_width="1dp"
       android:layout_height="match_parent"
       android:layout_toEndOf="@id/nav_view"
       android:background="?android:attr/listDivider" />

   <androidx.appcompat.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_alignParentTop="true"
       android:layout_toEndOf="@id/nav_view"
       android:background="@color/colorPrimary"
       android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/my_nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_below="@id/toolbar"
       android:layout_toEndOf="@id/nav_view"
       app:defaultNavHost="true"
       app:navGraph="@navigation/mobile_navigation" />
</RelativeLayout>

La vista de navegación inferior se encuentra en el directorio res/layout-h470dp:

<!-- res/layout-h470dp/navigation_activity.xml -->
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <androidx.appcompat.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="@color/colorPrimary"
       android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/my_nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       app:defaultNavHost="true"
       app:navGraph="@navigation/mobile_navigation" />

   <com.google.android.material.bottomnavigation.BottomNavigationView
       android:id="@+id/bottom_nav_view"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:menu="@menu/bottom_nav_menu" />
</LinearLayout>

El diseño del panel lateral se encuentra en el directorio res/layout. Usa ese directorio para diseños predeterminados sin calificadores específicos según la configuración:

<!-- res/layout/navigation_activity.xml -->
<androidx.drawerlayout.widget.DrawerLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/drawer_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical">

       <androidx.appcompat.widget.Toolbar
           android:id="@+id/toolbar"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:background="@color/colorPrimary"
           android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

       <androidx.fragment.app.FragmentContainerView
           android:id="@+id/my_nav_host_fragment"
           android:name="androidx.navigation.fragment.NavHostFragment"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           app:defaultNavHost="true"
           app:navGraph="@navigation/mobile_navigation" />
   </LinearLayout>

   <com.google.android.material.navigation.NavigationView
       android:id="@+id/nav_view"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_gravity="start"
       app:menu="@menu/nav_drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

Android sigue un orden de prioridad a los efectos de determinar qué recursos aplicar. En particular, en este ejemplo, -w960dp (o ancho disponible >= 960 dp) tiene prioridad sobre -h470dp (o altura disponible >= 470). Si la configuración del dispositivo no coincide con ninguna de esas condiciones, se usa el recurso de diseño predeterminado (res/layout/navigation_activity.xml).

Cuando administres eventos de navegación, deberás conectar solo los eventos que correspondan a los widgets que estén presentes, como se muestra en el siguiente ejemplo.

Kotlin

class MainActivity : AppCompatActivity() {

   private lateinit var appBarConfiguration : AppBarConfiguration

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.navigation_activity)
      val drawerLayout : DrawerLayout? = findViewById(R.id.drawer_layout)
      appBarConfiguration = AppBarConfiguration(
                  setOf(R.id.home_dest, R.id.deeplink_dest),
                  drawerLayout)

      ...

      // Initialize the app bar with the navigation drawer if present.
      // If the drawerLayout is not null here, a Navigation button will be added
      // to the app bar whenever the user is on a top-level destination.
      setupActionBarWithNavController(navController, appBarConfig)

      // Initialize the NavigationView if it is present,
      // so that clicking an item takes
      // the user to the appropriate destination.
      val sideNavView = findViewById<NavigationView>(R.id.nav_view)
      sideNavView?.setupWithNavController(navController)

      // Initialize the BottomNavigationView if it is present,
      // so that clicking an item takes
      // the user to the appropriate destination.
      val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
      bottomNav?.setupWithNavController(navController)

      ...
    }

    ...
}

Java

public class MainActivity extends AppCompatActivity {

   private AppBarConfiguration appBarConfiguration;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.navigation_activity);
       NavHostFragment host = (NavHostFragment) getSupportFragmentManager()
               .findFragmentById(R.id.my_nav_host_fragment);
       NavController navController = host.getNavController();

       DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
       appBarConfiguration = new AppBarConfiguration.Builder(
               R.id.home_dest, R.id.deeplink_dest)
               .setDrawerLayout(drawerLayout)
               .build();

       // Initialize the app bar with the navigation drawer if present.
       // If the drawerLayout is not null here, a Navigation button will be added to
       // the app bar whenever the user is on a top-level destination.
       NavigationUI.setupActionBarWithNavController(
               this, navController, appBarConfiguration);


       // Initialize the NavigationView if it is present,
       // so that clicking an item takes
       // the user to the appropriate destination.
       NavigationView sideNavView = findViewById(R.id.nav_view);
       if(sideNavView != null) {
           NavigationUI.setupWithNavController(sideNavView, navController);
       }

       // Initialize the BottomNavigationView if it is present,
       // so that clicking an item takes
       // the user to the appropriate destination.
       BottomNavigationView bottomNav = findViewById(R.id.bottom_nav_view);
       if(bottomNav != null) {
           NavigationUI.setupWithNavController(bottomNav, navController);
       }

   }
}

Si cambia la configuración del dispositivo, a menos que se configure lo contrario de forma explícita, Android destruirá la actividad de la configuración anterior junto con las vistas asociadas. Luego, vuelve a crear la actividad con recursos diseñados para la configuración nueva. La actividad, que se destruye y se vuelve a crear, luego conecta automáticamente los elementos de navegación globales adecuados en onCreate().

Considera alternativas a los diseños de vista dividida

Los diseños de vista dividida, o diseños principales o de detalles, solían ser una forma muy popular y recomendada de diseño para tablets y otros dispositivos de pantalla grande.

Desde la presentación de las tablets Android, el ecosistema de dispositivos creció rápidamente. Un factor que influyó de forma considerable en el espacio de diseño para dispositivos con pantallas grandes fue la presentación de modos multiventana, en especial las ventanas de formato libre cuyo tamaño puede cambiarse por completo, como las de los dispositivos ChromeOS. Esto pone un énfasis mucho mayor en lograr que todas las pantallas de tu app sean responsivas, en lugar de cambiar la estructura de navegación según el tamaño de la pantalla.

Si bien es posible implementar una interfaz de diseño de vista dividida usando la biblioteca de Navigation, deberías considerar otras alternativas.

Nombres de destino

Si proporcionas nombres de destino en tu gráfico con el atributo android:label, asegúrate de usar siempre valores de recursos de forma que aún se pueda ubicar tu contenido.

<navigation ...>
    <fragment
        android:id="@+id/my_dest"
        android:name="com.example.MyFragment"
        android:label="@string/my_dest_label"
        tools:layout="@layout/my_fragment" />
    ...

Mediante el uso de los valores de recursos, tus destinos tendrán aplicados automáticamente los recursos más adecuados cada vez que cambie la configuración.