Los fragmentos y el componente de Navigation

En el codelab de intents y actividades, agregaste intents en la app de Words a fin de navegar entre dos actividades. Si bien resulta útil que conozcas este patrón de navegación, es solo una parte de la historia de crear interfaces de usuario dinámicas para tus apps. Muchas apps para Android no necesitan una actividad separada para cada pantalla. De hecho, muchos patrones de IU comunes, como las pestañas, existen dentro de una única actividad y utilizan algo llamado fragmentos.

586ff7b88b0d2455.png

Un fragmento es una parte reutilizable de la IU. Los fragmentos se pueden reutilizar e incorporar en una o más actividades. En la captura de pantalla anterior, al presionar una pestaña, no se activa un intent para mostrar la siguiente pantalla. En cambio, cuando cambias de pestaña, simplemente, se cambia el fragmento anterior por otro. Todo esto ocurre sin iniciar otra actividad.

Incluso puedes mostrar varios fragmentos a la vez en una única pantalla, como un diseño principal y de detalles en tablets. En el ejemplo que figura a continuación, tanto la IU de navegación de la izquierda como el contenido de la derecha pueden estar en un fragmento independiente. Ambos fragmentos existen simultáneamente en la misma actividad.

92f1ecb9aadb7797.png

Como puedes ver, los fragmentos son una parte integral de la compilación de apps de alta calidad. En este codelab, aprenderás los conceptos básicos de los fragmentos y cambiarás la app de Words a fin de usarlos. También aprenderás a usar el componente de Navigation de Jetpack y a trabajar con un archivo de recursos nuevo llamado Navigation Graph para navegar entre fragmentos en la misma actividad del host. Al final de este codelab, tendrás las habilidades básicas para implementar fragmentos en tu próxima app.

Requisitos previos

Antes de completar este codelab, debes conocer lo siguiente:

  • La manera de agregar archivos de recursos de Kotlin y en formato XML a un proyecto de Android Studio
  • El funcionamiento del ciclo de vida de la actividad en términos generales
  • La forma de anular e implementar métodos en una clase existente
  • La manera de crear instancias de clases de Kotlin, acceder a las propiedades de las clases y llamar a métodos
  • Los aspectos básicos relativos a los valores que admiten valores nulos y los que no lo hacen, así como la forma de controlar los valores nulos de manera segura

Qué aprenderás

  • Cuáles son las diferencias entre el ciclo de vida del fragmento y el ciclo de vida de la actividad
  • Cómo convertir una actividad existente en un fragmento
  • Cómo agregar destinos a un gráfico de navegación y pasar datos entre fragmentos mientras se usa el complemento Safe Args

Qué compilarás

  • Modificarás la app de Words con el fin de usar una sola actividad y varios fragmentos, y navegarás entre fragmentos con el componente de Navigation.

Requisitos

  • Una computadora que tenga Android Studio instalado
  • El código de solución de la app de Words del codelab de intents y actividades

En este codelab, retomarás la app de Words desde donde la dejaste tras finalizar el codelab de intents y actividades. Si ya completaste el codelab de intents y actividades, no dudes en usar tu código como punto de partida. De forma alternativa, puedes descargar de GitHub el código generado hasta el momento.

Descarga el código de inicio para este codelab

En este codelab, se brinda el código de inicio para que lo extiendas con funciones que se explicarán aquí. El código de inicio puede contener código que ya conoces de codelabs anteriores. También puede contener código que no te resulte conocido y que aprenderás en codelabs futuros.

Si usas el código de inicio de GitHub, ten en cuenta que el nombre de la carpeta es android-basics-kotlin-words-app-activities. Selecciona esta carpeta cuando abras el proyecto en Android Studio.

A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:

Obtén el código

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se implementó la app.

Un fragmento es, simplemente, una pieza reutilizable de la interfaz de usuario de tu app. Al igual que las actividades, los fragmentos tienen un ciclo de vida y pueden responder a las entradas del usuario. Un fragmento siempre se encuentra dentro de la jerarquía de vistas de una actividad cuando se muestra en pantalla. Debido a su énfasis en la reutilización y la modularidad, es posible que varios fragmentos se alojen de forma simultánea en una única actividad. Cada fragmento administra su propio ciclo de vida independiente.

Ciclo de vida de los fragmentos

Al igual que las actividades, los fragmentos se pueden inicializar y quitar de la memoria, y, a lo largo de su existencia, pueden aparecer, desaparecer y volver a aparecer en la pantalla. Además, tal como ocurre con las actividades, los fragmentos tienen un ciclo de vida con varios estados y proporcionan varios métodos que puedes anular para responder a transiciones entre ellos. El ciclo de vida de los fragmentos tiene cinco estados, representados por la enumeración Lifecycle.State.

  • INICIALIZADO: Se creó una nueva instancia del fragmento.
  • CREADO: Se llamó a los primeros métodos del ciclo de vida del fragmento. Durante este estado, también se creará la vista asociada con el fragmento.
  • COMENZADO: El fragmento está visible en pantalla, pero no tiene "foco", lo cual significa que no puede responder a las entradas del usuario.
  • REANUDADO: El fragmento está visible y tiene foco.
  • DESTRUIDO: Se eliminó la instancia del objeto del fragmento.

De forma similar a las actividades, la clase Fragment proporciona muchos métodos que puedes anular a fin de responder a eventos de ciclo de vida.

  • onCreate(): Se creó la instancia del fragmento y se encuentra en el estado CREATED. Sin embargo, todavía no se creó la vista correspondiente.
  • onCreateView(): Este es el método en el cual se aumenta el diseño. El fragmento pasa al estado CREATED.
  • onViewCreated(): Se llama a este método después de que se crea la vista. En este método, por lo general, debes vincular vistas específicas con propiedades llamando a findViewById().
  • onStart(): El fragmento pasa al estado STARTED.
  • onResume(): El fragmento pasa al estado RESUMED y ahora tiene foco (puede responder a las entradas del usuario).
  • onPause(): El fragmento vuelve a pasar al estado STARTED. El usuario puede ver la IU.
  • onStop(): El fragmento vuelve a pasar al estado CREATED. Se crearon instancias del objeto, pero ya no se presentan en pantalla.
  • onDestroyView(): Se llama a este método justo antes de que el fragmento pase al estado DESTROYED. La vista ya se ha quitado de la memoria, pero el objeto del fragmento aún existe.
  • onDestroy(): El fragmento pasa al estado DESTROYED.

En el siguiente gráfico, se resume el ciclo de vida del fragmento y las transiciones entre estados.

74470aacefa170bd.png

Los estados del ciclo de vida y los métodos de devolución de llamada son bastante similares a los que se usan para actividades. Sin embargo, ten en cuenta la diferencia con el método onCreate(). Con las actividades, puedes usar este método a fin de aumentar el diseño y vincular vistas. No obstante, en el ciclo de vida del fragmento, se llama a onCreate() antes de crear la vista, de modo que no puedes aumentar el diseño aquí. En cambio, debes hacerlo en onCreateView(). Después de crear la vista, se llama al método onViewCreated(), en el que podrás vincular propiedades a vistas específicas.

Es probable que esto te parezca muy teórico, pero ahora sabes los conceptos básicos sobre cómo funcionan los fragmentos y cuánto se parecen a las actividades o se diferencian de ellas. Durante el resto de este codelab, pondrás en práctica ese conocimiento. Primero, migrarás la app de Words en la que trabajabas antes a efectos de usar un diseño basado en fragmentos. Luego, implementarás la navegación entre fragmentos dentro de una única actividad.

Al igual que con las actividades, cada fragmento que agregues incluirá dos archivos: un archivo en formato XML para el diseño y una clase de Kotlin a fin de mostrar datos y controlar las interacciones del usuario. Agregarás un fragmento para la lista de letras y la de palabras.

  1. Con la app seleccionada en el navegador de proyectos, agrega los siguientes fragmentos (File > New > Fragment > Fragment (Blank)). Se deberá generar una clase y un archivo de diseño para cada uno.
  • Para el primer fragmento, establece el Fragment Name en LetterListFragment. El Fragment Layout Name deberá propagarse como fragment_letter_list.

898650e4cd0b2486.png

  • Para el segundo fragmento, establece el Fragment Name en WordListFragment. El Fragment Layout Name deberá propagarse como fragment_word_list.xml.

4f04fca641487da1.png

  1. Las clases de Kotlin generadas para ambos fragmentos contienen mucho código estándar de uso general a la hora de implementar fragmentos. Sin embargo, como estás aprendiendo sobre ellos por primera vez, borra todo en ambos archivos, excepto la declaración de clase para LetterListFragment y WordListFragment. Te guiaremos a través de la implementación de los fragmentos desde cero para que sepas cómo funciona todo el código. Después de borrar el código estándar, los archivos Kotlin deberían verse así:

LetterListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class LetterListFragment : Fragment() {

}

WordListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class WordListFragment : Fragment() {

}
  1. Copia el contenido de activity_main.xml en fragment_letter_list.xml y el de activity_detail.xml en fragment_word_list.xml. Actualiza tools:context en fragment_letter_list.xml a .LetterListFragment y tools:context en fragment_word_list.xml a .WordListFragment.

Después de los cambios, los archivos de diseño de fragmento deberían verse así:

fragment_letter_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp" />

</FrameLayout>

fragment_word_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp"
       tools:listitem="@layout/item_view" />

</FrameLayout>

Al igual que con las actividades, debes ampliar el diseño y vincular vistas individuales. Solo hay algunas diferencias menores cuando se trabaja con el ciclo de vida del fragmento. Te guiaremos a través del proceso de configuración del LetterListFragment y luego podrás realizar lo mismo para WordListFragment.

Para implementar la vinculación de vistas en LetterListFragment, primero debes establecer una referencia de nulabilidad para FragmentLetterListBinding. Android Studio genera clases de vinculación como esta para cada archivo de diseño cuando la propiedad viewBinding está habilitada en la sección buildFeatures del archivo build.gradle. Solo debes asignar propiedades en tu clase de fragmento para cada vista en la FragmentLetterListBinding.

El tipo debe ser FragmentLetterListBinding? y tener un valor inicial de null. ¿Por qué establecer la nulabilidad? Porque no puedes aumentar el diseño hasta que se llame a onCreateView(). Existe un período entre el momento en que se crea la instancia de LetterListFragment (cuando su ciclo de vida comienza con onCreate()) y el momento en que esta propiedad efectivamente se puede usar. Además, ten en cuenta que las vistas de los fragmentos se pueden crear y destruir varias veces durante el ciclo de vida del fragmento. Por esta razón, también debes restablecer el valor en otro método de ciclo de vida, onDestroyView().

  1. En LetterListFragment.kt, comienza por hacer referencia a la FragmentLetterListBinding y asigna el nombre _binding a la referencia.
private var _binding: FragmentLetterListBinding? = null

Dado que admite un valor nulo, cada vez que accedes a una propiedad de _binding (p. ej., _binding?.someView), debes incluir el ? para garantizar la seguridad nula. Sin embargo, esto no significa que debas llenar tu código con signos de interrogación solo por un valor nulo. Si estás seguro de que un valor no será nulo cuando accedas a él, puedes agregar !! a su nombre de tipo. Entonces, podrás acceder a él como a cualquier otra propiedad, sin el operador ?.

  1. Crea una nueva propiedad, llamada binding (sin el guion bajo), y establécela en _binding!!.
private val binding get() = _binding!!

En este caso, get() significa que esta propiedad se usa para "solo obtener". Esto quiere decir que puedes obtener el valor, pero, una vez que esté asignado (como ocurre aquí), no podrás asignarlo a otro elemento.

  1. Para implementar onCreate(), simplemente, llama a setHasOptionsMenu().
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setHasOptionsMenu(true)
}
  1. Recuerda que, cuando se trata de fragmentos, el diseño aumenta en onCreateView(). Implementa onCreateView() aumentando la vista, configurando el valor de _binding y mostrando la vista raíz.
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentLetterListBinding.inflate(inflater, container, false)
   val view = binding.root
   return view
}
  1. Debajo de la propiedad binding, crea una propiedad para la vista de reciclador.
private lateinit var recyclerView: RecyclerView
  1. Luego, establece el valor de la propiedad recyclerView en onViewCreated() y llama a chooseLayout() como lo hiciste en MainActivity. Pronto moverás el método chooseLayout() a LetterListFragment. No te preocupes si se produce un error.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   chooseLayout()
}

Observa cómo la clase de vinculación ya creó una propiedad para recyclerView y no necesitas llamar a findViewById() para cada vista.

  1. Por último, en onDestroyView(), restablece la propiedad _binding a null, dado que la vista ya no existe.
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. Lo único que también deberás tener en cuenta es que existen algunas diferencias sutiles con el método onCreateOptionsMenu() cuando se trabaja con fragmentos. Si bien la clase Activity tiene una propiedad global llamada menuInflater, los fragmentos no tienen esta propiedad. En su lugar, el amplificador de menú se pasa a onCreateOptionsMenu(). Además, ten en cuenta que el método onCreateOptionsMenu() que se usa con los fragmentos no requiere una sentencia de retorno. Implementa el método como se muestra a continuación:
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
   inflater.inflate(R.menu.layout_menu, menu)

   val layoutButton = menu.findItem(R.id.action_switch_layout)
   setIcon(layoutButton)
}
  1. Mueve el código restante de chooseLayout(), setIcon() y onOptionsItemSelected() desde MainActivity tal como está. La única diferencia adicional que debes tener en cuenta es que, a diferencia de una actividad, un fragmento no es un Context. No puedes pasar this (si te refieres al objeto de fragmento) como el contexto del administrador de diseño. Sin embargo, los fragmentos proporcionan una propiedad context que puedes usar en su lugar. El resto del código es idéntico a MainActivity.
private fun chooseLayout() {
   when (isLinearLayoutManager) {
       true -> {
           recyclerView.layoutManager = LinearLayoutManager(context)
           recyclerView.adapter = LetterAdapter()
       }
       false -> {
           recyclerView.layoutManager = GridLayoutManager(context, 4)
           recyclerView.adapter = LetterAdapter()
       }
   }
}

private fun setIcon(menuItem: MenuItem?) {
   if (menuItem == null)
       return

   menuItem.icon =
       if (isLinearLayoutManager)
           ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
       else ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_linear_layout)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           isLinearLayoutManager = !isLinearLayoutManager
           chooseLayout()
           setIcon(item)

           return true
       }

       else -> super.onOptionsItemSelected(item)
   }
}
  1. Por último, copia sobre la propiedad isLinearLayoutManager de MainActivity. Escribe esto debajo de la declaración de la propiedad recyclerView.
private var isLinearLayoutManager = true
  1. Ahora que toda la funcionalidad se movió a LetterListFragment, resta que la clase MainActivity aumente el diseño de modo que el fragmento se muestre en la vista. Borra todo de MainActivity, excepto onCreate(). Después de los cambios, MainActivity deberá contener solo lo siguiente:
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

Tu turno

Eso es todo en cuanto a la migración de MainActivity a LettersListFragment. Migrar la DetailActivity es casi idéntico. Para migrar el código a WordListFragment, realiza los pasos siguientes:

  1. Copia el objeto complementario de DetailActivity a WordListFragment. Asegúrate de que la referencia a SEARCH_PREFIX en WordAdapter se haya actualizado de modo que haga referencia a WordListFragment.
  2. Agrega una variable _binding. Esta variable debe admitir valores nulos y tener null como valor inicial.
  3. Agrega una variable get llamada binding igual a la variable _binding.
  4. Aumenta el diseño en onCreateView() configurando el valor de _binding y mostrando la vista raíz.
  5. Realiza cualquier configuración restante en onViewCreated(): obtén una referencia a la vista de reciclador, establece su administrador de diseño y el adaptador, y agrega la decoración de su elemento. Deberás obtener la letra del intent. Los fragmentos no tienen una propiedad intent y, en general, no deberían acceder al intent de la actividad superior. Por ahora, consulta activity.intent (en lugar de intent en DetailActivity) para obtener los extras.
  6. Restablece _binding como nulo en onDestroyView.
  7. Borra el código restante de DetailActivity y deja solo el método onCreate().

Intenta seguir los pasos por tu cuenta antes de continuar. En el siguiente paso, se brinda una explicación detallada.

Esperamos que hayas disfrutado la oportunidad de migrar DetailActivity a WordListFragment. Esto es casi idéntico a migrar MainActivity a LetterListFragment. Por si en algún momento no pudiste avanzar, a continuación, encontrarás un resumen de los pasos.

  1. Primero, copia el objeto complementario a WordListFragment.
companion object {
   val LETTER = "letter"
   val SEARCH_PREFIX = "https://www.google.com/search?q="
}
  1. Luego, en LetterAdapter, en el objeto onClickListener() donde realizas el intent, debes actualizar la llamada a putExtra() y reemplazar DetailActivity.LETTER por WordListFragment.LETTER.
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
  1. Del mismo modo, en WordAdapter, debes actualizar el onClickListener() en el que navegas por los resultados de la búsqueda de la palabra y reemplazar DetailActivity.SEARCH_PREFIX por WordListFragment.SEARCH_PREFIX.
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")
  1. En WordListFragment, agrega una variable de vinculación del tipo FragmentWordListBinding?.
private var _binding: FragmentWordListBinding? = null
  1. Luego, crea una variable get para que puedas hacer referencia a vistas sin usar ?.
private val binding get() = _binding!!
  1. A continuación, amplía el diseño asignando la variable _binding y mostrando la vista raíz. Recuerda que, para los fragmentos, deberás hacerlo en onCreateView(), no en onCreate().
override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentWordListBinding.inflate(inflater, container, false)
   return binding.root
}
  1. A continuación, implementa onViewCreated(). Esto es casi idéntico a configurar la recyclerView de onCreateView() en DetailActivity. Sin embargo, debido a que los fragmentos no tienen acceso directo al intent, debes hacer referencia a él mediante activity.intent. Deberás hacerlo en onCreateView(), ya que no es seguro que la actividad exista antes en el ciclo de vida.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   val recyclerView = binding.recyclerView
   recyclerView.layoutManager = LinearLayoutManager(requireContext())
   recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())

   recyclerView.addItemDecoration(
       DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
   )
}
  1. Por último, puedes restablecer la variable _binding en onDestroyView().
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. Ahora que todas las funciones se movieron a WordListFragment, podrás borrar el código de DetailActivity. Lo único que debes dejar es el método onCreate().
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityDetailBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

Quita DetailActivity

Ahora que migraste de forma correcta la funcionalidad de DetailActivity a WordListFragment, ya no necesitas DetailActivity. Puedes borrar DetailActivity.kt y activity_detail.xml, y hacer un pequeño cambio en el manifiesto.

  1. Primero, borra DetailActivity.kt.

dd3b0bcf3ec81c9.png

  1. Comprueba que Safe Delete esté desmarcado y haz clic en OK.

f2f1ff137b0057a7.png

  1. A continuación, borra activity_detail.xml. Nuevamente, asegúrate de que Safe Delete esté desmarcado.

6090c1d640433e07.png

  1. Por último, como DetailActivity ya no existe, quita lo siguiente de AndroidManifest.xml.
<activity
   android:name=".DetailActivity"
   android:parentActivityName=".MainActivity" />

Después de borrar la actividad de detalles, te quedan dos fragmentos (LetterListFragment y WordListFragment) y una sola actividad (MainActivity). En la siguiente sección, aprenderás sobre el componente de Navigation de Jetpack y sobre cómo editar activity_main.xml de modo que pueda mostrar fragmentos y navegar entre ellos, en lugar de alojar un diseño estático.

Android Jetpack proporciona el componente de Navigation a fin de ayudarte a controlar cualquier implementación de navegación, ya sea simple o compleja, en tu app. El componente de Navigation tiene tres partes principales que utilizarás para implementar la navegación en la app de Words.

  • Navigation Graph: Es un archivo en formato XML que proporciona una representación visual de la navegación en tu app. El archivo consta de destinos que corresponden a actividades y fragmentos individuales, así como a acciones que pueden usarse en el código para navegar de un destino a otro. Al igual que los archivos de diseño, Android Studio proporciona un editor visual que permite agregar destinos y acciones al gráfico de navegación.
  • NavHost: Se usa NavHost para mostrar los destinos de un gráfico de navegación en una actividad. Cuando navegas entre fragmentos, se actualiza el destino que se muestra en NavHost. Usarás una implementación integrada, llamada NavHostFragment, en tu MainActivity.
  • NavController: El objeto NavController te permite controlar la navegación entre los destinos que se muestran en el NavHost. Cuando trabajas con intents, debes llamar a startActivity a fin de navegar a una pantalla nueva. Con el componente de Navigation, puedes llamar al método navigate() de NavController a efectos de intercambiar el fragmento que se muestra. El NavController también te ayuda a manejar tareas comunes, como responder al botón "arriba" del sistema para volver al fragmento que se mostró anteriormente.
  1. En el archivo build.gradle del nivel de proyecto, en buildscript > ext, debajo de material_version, establece la nav_version en 2.3.1.
buildscript {
    ext {
        appcompat_version = "1.2.0"
        constraintlayout_version = "2.0.2"
        core_ktx_version = "1.3.2"
        kotlin_version = "1.3.72"
        material_version = "1.2.1"
        nav_version = "2.3.1"
    }

    ...

}

  1. En el archivo build.gradle del nivel de la app, agrega lo siguiente al grupo de dependencias:
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

Complemento Safe Args

Cuando implementaste la navegación por primera vez en la app de Words, usaste un intent explícito entre las dos actividades. Para pasar datos entre las dos actividades, llamaste al método putExtra() y pasaste la letra seleccionada.

Antes de comenzar a implementar el componente de Navigation en la app de Words, también agregarás algo llamado Safe Args, un complemento de Gradle que te ayudará con la seguridad de tipo cuando pases datos entre fragmentos.

Realiza los siguientes pasos a fin de integrar SafeArgs en tu proyecto.

  1. En el archivo build.gradle de nivel superior, en buildscript > dependencies, agrega la siguiente Ruta de clase.
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
  1. En el archivo build.gradle del nivel de la app, dentro de plugins en la parte superior, agrega androidx.navigation.safeargs.kotlin.
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'androidx.navigation.safeargs.kotlin'
}
  1. Después de editar los archivos de Gradle, es posible que veas un banner amarillo en la parte superior que te solicite sincronizar el proyecto. Haz clic en "Sincronizar ahora" y espera uno o dos minutos mientras Gradle actualiza las dependencias de tu proyecto a fin de reflejar los cambios.

854d44a6f7c4c080.png

Una vez que se complete la sincronización, podrás continuar con el siguiente paso, en el que agregarás un gráfico de navegación.

Ahora que tienes los conocimientos básicos acerca de los fragmentos y su ciclo de vida, es hora de que las cosas se vuelvan un poco más interesantes. El siguiente paso es incorporar el componente de Navigation. El componente de Navigation se refiere simplemente a la colección de herramientas para implementar la navegación, en especial entre fragmentos. Trabajarás con un nuevo editor visual a efectos de implementar la navegación entre fragmentos: el Navigation Graph (gráfico de navegación, abreviado como NavGraph).

¿Qué es un gráfico de navegación?

El gráfico de navegación (o NavGraph) es un mapa virtual de la navegación de tu app. Cada pantalla (o, en tu caso, cada fragmento) se convierte en un posible "destino" hacia el que se puede navegar. Un NavGraph se puede representar con un archivo en formato XML que muestra cómo los destinos se relacionan entre sí.

En segundo plano, esto crea una nueva instancia de la clase NavGraph. Sin embargo, FragmentContainerView muestra los destinos del gráfico de navegación al usuario. Lo único que debes hacer es crear un archivo en formato XML y definir los posibles destinos. Luego, puedes usar el código generado para navegar entre fragmentos.

Usa FragmentContainerView en MainActivity

Dado que tus diseños ahora se incluyen en fragment_letter_list.xml y fragment_word_list.xml, tu archivo activity_main.xml ya no necesita contener el diseño de la primera pantalla de tu app. En cambio, reutilizarás MainActivity de modo que contenga una FragmentContainerView y actúe como el NavHost para tus fragmentos. A partir de este punto, toda la navegación de la app se realizará dentro de la FragmentContainerView.

  1. Reemplaza el contenido de FrameLayout en activity_main.xml que es androidx.recyclerview.widget.RecyclerView con una FragmentContainerView. Asígnale un ID de nav_host_fragment y establece su alto y su ancho en match_parent a fin de llenar todo el diseño del marco.

Reemplaza lo siguiente:

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        ...
        android:padding="16dp" />

Con lo siguiente:

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/nav_host_fragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. Debajo del atributo id, agrega un atributo name y establécelo en androidx.navigation.fragment.NavHostFragment. Si bien puedes especificar un fragmento en particular para este atributo, establecerlo en NavHostFragment permite que FragmentContainerView navegue entre fragmentos.
android:name="androidx.navigation.fragment.NavHostFragment"
  1. Debajo de los atributos layout_height y layout_width, agrega uno llamado app:navHost y establécelo en "true". Esto permitirá que el contenedor del fragmento interactúe con la jerarquía de navegación. Por ejemplo, si se presiona el botón Atrás del sistema, el contenedor volverá al fragmento mostrado con anterioridad, tal como sucede cuando se presenta una actividad nueva.
app:defaultNavHost="true"
  1. Agrega un atributo llamado app:navGraph y establécelo en "@navigation/nav_graph". Esto apunta a un archivo en formato XML que define cómo los fragmentos de tu app pueden navegar entre sí. Por ahora, Android Studio te mostrará un error de símbolo sin resolver. Abordarás esto en la siguiente tarea.
app:navGraph="@navigation/nav_graph"
  1. Por último, debido a que agregaste dos atributos con el espacio de nombres de la app, asegúrate de agregar el atributo xmlns:app al FrameLayout.
<xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

Esos son todos los cambios en activity_main.xml. A continuación, crearás el archivo nav_graph.

Configura el gráfico de navegación

Agrega un archivo de gráfico de navegación (File > New > Android Resource File) y completa los campos como se muestra a continuación.

  • File Name: nav_graph.xml. Es igual al nombre que estableciste para el atributo app:navGraph.
  • Resource Type: Navigation. El Directory Name debería cambiar automáticamente a navigation. Se creará una nueva carpeta de recursos llamada "navigation".

e26ed91764a5616e.png

Cuando crees el archivo en formato XML, verás un nuevo editor visual. Como ya hiciste referencia a nav_graph en la propiedad navGraph de FragmentContainerView, con el fin de agregar un destino nuevo, haz clic en el botón new en la esquina superior izquierda de la pantalla y crea un destino para cada fragmento (uno para fragment_letter_list y otro para fragment_word_list).

307d036fce790feb.gif

Una vez que se agreguen, estos fragmentos deberían aparecer en el gráfico de navegación, en el centro de la pantalla. También puedes seleccionar un destino específico con el árbol de componentes que aparece a la izquierda.

Crea una acción de navegación

A efectos de crear una acción de navegación entre el letterListFragment y los destinos de wordListFragment, coloca el cursor sobre el destino letterListFragment y arrastra desde el círculo que aparece a la derecha hasta el destino wordListFragment.

c9477af5828a83f4.gif

Ahora deberías ver que se creó una flecha que representa la acción entre los dos destinos. Haz clic en la flecha y podrás ver en el panel de atributos que esta acción tiene un nombre action_letterListFragment_to_wordListFragment al que se puede hacer referencia en el código.

Especifica argumentos para WordListFragment

Cuando navegaste entre actividades con un intent, especificaste un "extra" de modo que la letra seleccionada se pudiera pasar al wordListFragment. Navigation también admite el pase de parámetros entre destinos, y lo hace con seguridad de tipo.

Selecciona el destino wordListFragment y, en el panel de atributos, en Arguments, haz clic en el botón de signo más a fin de crear un argumento nuevo.

El argumento deberá llamarse letter y el tipo deberá ser String. Aquí es donde entra en juego el complemento Safe Args que agregaste antes. Especificar este argumento como una string garantiza que se esperará una String cuando tu acción de navegación se realice en el código.

b6bc3eaacd14bf50.png

Configura el destino de inicio

Si bien tu NavGraph reconoce todos los destinos necesarios, ¿cómo sabrá la FragmentContainerView qué fragmento mostrar primero? En el NavGraph, debes establecer la lista de letras como un destino de inicio.

Para establecer el destino de inicio, selecciona letterListFragment y haz clic en el botón Assign start destination.

99bb085e39dd7b4a.png

Eso es todo lo que tienes que hacer con el editor de NavGraph por ahora. Ahora, compila el proyecto. Esto generará algún código basado en tu gráfico de navegación de modo que puedas usar la acción de navegación que acabas de crear.

Realiza la acción de navegación

Abre LetterAdapter.kt a fin de realizar la acción de navegación. Esta acción solo requiere dos pasos.

  1. Borra el contenido de onClickListener() del botón. En su lugar, deberás recuperar la acción de navegación que acabas de crear. Agrega lo siguiente al onClickListener():
val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString())

Quizá no reconozcas algunos de estos nombres de clase y función, y esto se debe a que se generaron automáticamente después de que creaste el proyecto. Aquí es donde interviene el complemento Safe Args que agregaste en el primer paso: las acciones creadas en el NavGraph se convirtieron en código que puedes usar. Sin embargo, los nombres deberán ser bastante intuitivos. LetterListFragmentDirections te permite hacer referencia a todas las rutas de navegación posibles a partir del letterListFragment. La función actionLetterListFragmentToWordListFragment()

es la acción específica para navegar hasta el wordListFragment.

Una vez que tengas una referencia a tu acción de navegación, simplemente, obtén una referencia a tu NavController (un objeto que te permite realizar acciones de navegación) y llama a navigate() pasando la acción.

holder.view.findNavController().navigate(action)

Configura MainActivity

La última parte de la configuración está en MainActivity. Solo se necesitan unos pocos cambios en MainActivity para que todo funcione correctamente.

  1. Crea una propiedad navController. Esto está marcado como lateinit, ya que se establecerá en onCreate.
private lateinit var navController: NavController
  1. Luego, después de la llamada a setContentView() en onCreate(), obtén una referencia al nav_host_fragment (este es el ID de tu FragmentContainerView) y asígnalo a tu propiedad navController.
val navHostFragment = supportFragmentManager
    .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
  1. Luego, en onCreate(), llama a setupActionBarWithNavController() y pasa navController. Esto garantiza que se visualicen los botones de la barra de acciones (barra de la app), como la opción de menú de LetterListFragment.
setupActionBarWithNavController(navController)
  1. Por último, implementa onSupportNavigateUp(). Además de configurar defaultNavHost como true en el XML, este método te permitirá controlar el botón Arriba. No obstante, tu actividad debe proporcionar la implementación.
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}

En este punto, se establecieron todos los componentes de modo que la navegación funcione con fragmentos. Sin embargo, ahora que la navegación se realiza mediante fragmentos en lugar del intent, el intent adicional para la letra que usas en WordListFragment ya no funcionará. En el siguiente paso, actualizarás WordListFragment a fin de obtener el argumento letter.

Antes, hiciste referencia a activity?.intent en WordListFragment a fin de acceder a la letter adicional. Si bien esto funciona, no es una práctica recomendada, ya que los fragmentos pueden incorporarse en otros diseños y, en una app más grande, resulta mucho más difícil suponer a qué actividad pertenece el fragmento. Además, cuando se realiza la navegación con nav_graph y se usan argumentos seguros, no hay intents, por lo que tratar de acceder a los intents adicionales no funcionará.

Afortunadamente, acceder a argumentos seguros resulta bastante sencillo y no deberás esperar a que se llame a onViewCreated().

  1. En WordListFragment, crea una propiedad letterId. Puedes marcarla como lateinit de modo que no tengas que hacer que admita un valor nulo.
private lateinit var letterId: String
  1. Luego, anula onCreate() (no onCreateView() ni onViewCreated()) y agrega lo siguiente:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
        letterId = it.getString(LETTER).toString()
    }
}

Dado que es posible que arguments sea opcional, observa que llamas a let() y pasas una lambda. Si suponemos que arguments no es nulo, se ejecutará este código pasando los argumentos no nulos al parámetro it. Sin embargo, si arguments es null, no se ejecutará la lambda.

96a6a3253cea35b0.png

Si bien no forma parte del código, Android Studio te ofrece una sugerencia útil para que conozcas el parámetro it.

¿Qué es un Bundle? Considéralo un par clave-valor que se usa a fin de pasar datos entre clases, como actividades y fragmentos. En realidad, ya usaste un paquete cuando llamaste a intent?.extras?.getString() y realizaste un intent en la primera versión de esta app. Obtener la string de los argumentos cuando se trabaja con fragmentos funciona de la misma manera.

  1. Por último, puedes acceder a letterId cuando configuras el adaptador de la vista de reciclador. Reemplaza activity?.intent?.extras?.getString(LETTER).toString() en onViewCreated() por letterId.
recyclerView.adapter = WordAdapter(letterId, requireContext())

¡Excelente! Tómate un momento para ejecutar tu app. Ahora puede navegar entre dos pantallas, sin intents y con una única actividad.

Convertiste con éxito ambas pantallas de modo que usen fragmentos. Antes de que realizaras cualquier cambio, la barra de la app de cada fragmento tenía un título descriptivo para cada actividad incluida en esa barra. Sin embargo, después de la conversión a fin de usar fragmentos, este título ya no aparece en la actividad de detalle.

c385595994ba91b5.png

Los fragmentos tienen una propiedad llamada "label", en la que puedes establecer el título que la actividad superior usará en la barra de la app.

  1. En strings.xml, después del nombre de la app, agrega la siguiente constante:
<string name="word_list_fragment_label">Words That Start With {letter}</string>
  1. Puedes configurar la etiqueta de cada fragmento en el gráfico de navegación. Regresa a nav_graph.xml y selecciona letterListFragment en el árbol de componentes. En el panel de atributos, establece la etiqueta en la string app_name.

4568d78c606999d.png

  1. Selecciona wordListFragment y establece la etiqueta en word_list_fragment_label.

7e7e55ea2dfb65bb.png

¡Felicitaciones por haber llegado hasta aquí! Ejecuta tu app una vez más. Deberías ver todo como estaba al comienzo del codelab, solo que ahora toda tu navegación está alojada en una única actividad con un fragmento independiente para cada pantalla.

El código de solución para este codelab se encuentra en el proyecto que se muestra a continuación.

A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:

Obtén el código

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se implementó la app.
  • Los fragmentos son elementos de IU reutilizables que se pueden incorporar en actividades.
  • El ciclo de vida de un fragmento difiere del ciclo de vida de una actividad, y la configuración de la vista ocurre en onViewCreated() en lugar de hacerlo en onCreateView().
  • Se usa una FragmentContainerView a fin de incorporar fragmentos en otras actividades, y esta puede administrar la navegación entre fragmentos.

Cómo usar el componente de Navigation

  • Configurar el atributo navGraph de una FragmentContainerView te permite navegar entre fragmentos dentro de una actividad.
  • El editor NavGraph te permite agregar acciones de navegación y especificar argumentos entre diferentes destinos.
  • Si bien, cuando navegas con intents, debes pasar los extras, el componente de Navigation usa SafeArgs a fin de generar automáticamente las clases y los métodos para tus acciones de navegación, lo cual garantiza la seguridad de tipo con argumentos.

Casos de uso para fragmentos

  • Cuando se usa el componente de Navigation, muchas apps pueden administrar todo su diseño dentro de una sola actividad, así como toda la navegación que se produce entre fragmentos.
  • Los fragmentos hacen posibles los patrones de diseño comunes, como los diseños principales y de detalles en tablets, o el diseño de varias pestañas dentro de la misma actividad.