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.
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.
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
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- 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.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- 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
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- 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 estadoCREATED
. 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 estadoCREATED
.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 afindViewById()
.onStart()
: El fragmento pasa al estadoSTARTED
.onResume()
: El fragmento pasa al estadoRESUMED
y ahora tiene foco (puede responder a las entradas del usuario).onPause()
: El fragmento vuelve a pasar al estadoSTARTED
. El usuario puede ver la IU.onStop()
: El fragmento vuelve a pasar al estadoCREATED
. 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 estadoDESTROYED
. La vista ya se ha quitado de la memoria, pero el objeto del fragmento aún existe.onDestroy()
: El fragmento pasa al estadoDESTROYED
.
En el siguiente gráfico, se resume el ciclo de vida del fragmento y las transiciones entre estados.
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.
- 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 comofragment_letter_list
.
- Para el segundo fragmento, establece el Fragment Name en
WordListFragment
. El Fragment Layout Name deberá propagarse comofragment_word_list.xml
.
- 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
yWordListFragment
. 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() {
}
- Copia el contenido de
activity_main.xml
enfragment_letter_list.xml
y el deactivity_detail.xml
enfragment_word_list.xml
. Actualizatools:context
enfragment_letter_list.xml
a.LetterListFragment
ytools:context
enfragment_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()
.
- En
LetterListFragment.kt
, comienza por hacer referencia a laFragmentLetterListBinding
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 ?
.
- 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.
- Para implementar
onCreate()
, simplemente, llama asetHasOptionsMenu()
.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
- Recuerda que, cuando se trata de fragmentos, el diseño aumenta en
onCreateView()
. ImplementaonCreateView()
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
}
- Debajo de la propiedad
binding
, crea una propiedad para la vista de reciclador.
private lateinit var recyclerView: RecyclerView
- Luego, establece el valor de la propiedad
recyclerView
enonViewCreated()
y llama achooseLayout()
como lo hiciste enMainActivity
. Pronto moverás el métodochooseLayout()
aLetterListFragment
. 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.
- Por último, en
onDestroyView()
, restablece la propiedad_binding
anull
, dado que la vista ya no existe.
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- 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 claseActivity
tiene una propiedad global llamadamenuInflater
, los fragmentos no tienen esta propiedad. En su lugar, el amplificador de menú se pasa aonCreateOptionsMenu()
. Además, ten en cuenta que el métodoonCreateOptionsMenu()
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)
}
- Mueve el código restante de
chooseLayout()
,setIcon()
yonOptionsItemSelected()
desdeMainActivity
tal como está. La única diferencia adicional que debes tener en cuenta es que, a diferencia de una actividad, un fragmento no es unContext
. No puedes pasarthis
(si te refieres al objeto de fragmento) como el contexto del administrador de diseño. Sin embargo, los fragmentos proporcionan una propiedadcontext
que puedes usar en su lugar. El resto del código es idéntico aMainActivity
.
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)
}
}
- Por último, copia sobre la propiedad
isLinearLayoutManager
deMainActivity
. Escribe esto debajo de la declaración de la propiedadrecyclerView
.
private var isLinearLayoutManager = true
- Ahora que toda la funcionalidad se movió a
LetterListFragment
, resta que la claseMainActivity
aumente el diseño de modo que el fragmento se muestre en la vista. Borra todo deMainActivity
, exceptoonCreate()
. 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:
- Copia el objeto complementario de
DetailActivity
aWordListFragment
. Asegúrate de que la referencia aSEARCH_PREFIX
enWordAdapter
se haya actualizado de modo que haga referencia aWordListFragment
. - Agrega una variable
_binding
. Esta variable debe admitir valores nulos y tenernull
como valor inicial. - Agrega una variable get llamada binding igual a la variable
_binding
. - Aumenta el diseño en
onCreateView()
configurando el valor de_binding
y mostrando la vista raíz. - 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 propiedadintent
y, en general, no deberían acceder al intent de la actividad superior. Por ahora, consultaactivity.intent
(en lugar deintent
enDetailActivity
) para obtener los extras. - Restablece
_binding
como nulo enonDestroyView
. - Borra el código restante de
DetailActivity
y deja solo el métodoonCreate()
.
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.
- Primero, copia el objeto complementario a
WordListFragment
.
companion object {
val LETTER = "letter"
val SEARCH_PREFIX = "https://www.google.com/search?q="
}
- Luego, en
LetterAdapter
, en el objetoonClickListener()
donde realizas el intent, debes actualizar la llamada aputExtra()
y reemplazarDetailActivity.LETTER
porWordListFragment.LETTER
.
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
- Del mismo modo, en
WordAdapter
, debes actualizar elonClickListener()
en el que navegas por los resultados de la búsqueda de la palabra y reemplazarDetailActivity.SEARCH_PREFIX
porWordListFragment.SEARCH_PREFIX
.
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")
- En
WordListFragment
, agrega una variable de vinculación del tipoFragmentWordListBinding?
.
private var _binding: FragmentWordListBinding? = null
- Luego, crea una variable get para que puedas hacer referencia a vistas sin usar
?
.
private val binding get() = _binding!!
- 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 enonCreateView()
, no enonCreate()
.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWordListBinding.inflate(inflater, container, false)
return binding.root
}
- A continuación, implementa
onViewCreated()
. Esto es casi idéntico a configurar larecyclerView
deonCreateView()
enDetailActivity
. Sin embargo, debido a que los fragmentos no tienen acceso directo al intent, debes hacer referencia a él medianteactivity.intent
. Deberás hacerlo enonCreateView()
, 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)
)
}
- Por último, puedes restablecer la variable
_binding
enonDestroyView()
.
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- 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.
- Primero, borra
DetailActivity.kt
.
- Comprueba que Safe Delete esté desmarcado y haz clic en OK.
- A continuación, borra
activity_detail.xml
. Nuevamente, asegúrate de que Safe Delete esté desmarcado.
- Por último, como
DetailActivity
ya no existe, quita lo siguiente deAndroidManifest.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 usaNavHost
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 enNavHost
. Usarás una implementación integrada, llamadaNavHostFragment
, en tuMainActivity
.NavController
: El objetoNavController
te permite controlar la navegación entre los destinos que se muestran en elNavHost
. 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étodonavigate()
deNavController
a efectos de intercambiar el fragmento que se muestra. ElNavController
también te ayuda a manejar tareas comunes, como responder al botón "arriba" del sistema para volver al fragmento que se mostró anteriormente.
Dependencia de Navigation
- En el archivo
build.gradle
del nivel de proyecto, en buildscript > ext, debajo dematerial_version
, establece lanav_version
en2.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"
}
...
}
- 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.
- 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"
- En el archivo
build.gradle
del nivel de la app, dentro deplugins
en la parte superior, agregaandroidx.navigation.safeargs.kotlin
.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'androidx.navigation.safeargs.kotlin'
}
- 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.
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
.
- Reemplaza el contenido de
FrameLayout
en activity_main.xml que esandroidx.recyclerview.widget.RecyclerView
con unaFragmentContainerView
. Asígnale un ID denav_host_fragment
y establece su alto y su ancho enmatch_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" />
- Debajo del atributo id, agrega un atributo
name
y establécelo enandroidx.navigation.fragment.NavHostFragment
. Si bien puedes especificar un fragmento en particular para este atributo, establecerlo enNavHostFragment
permite queFragmentContainerView
navegue entre fragmentos.
android:name="androidx.navigation.fragment.NavHostFragment"
- 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"
- 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"
- 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 atributoapp:navGraph
. - Resource Type: Navigation. El Directory Name debería cambiar automáticamente a navigation. Se creará una nueva carpeta de recursos llamada "navigation".
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
).
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
.
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.
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.
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.
- 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 alonClickListener()
:
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.
- Crea una propiedad
navController
. Esto está marcado comolateinit
, ya que se establecerá enonCreate
.
private lateinit var navController: NavController
- Luego, después de la llamada a
setContentView()
enonCreate()
, obtén una referencia alnav_host_fragment
(este es el ID de tuFragmentContainerView
) y asígnalo a tu propiedadnavController
.
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
- Luego, en
onCreate()
, llama asetupActionBarWithNavController()
y pasanavController
. Esto garantiza que se visualicen los botones de la barra de acciones (barra de la app), como la opción de menú deLetterListFragment
.
setupActionBarWithNavController(navController)
- Por último, implementa
onSupportNavigateUp()
. Además de configurardefaultNavHost
comotrue
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()
.
- En
WordListFragment
, crea una propiedadletterId
. Puedes marcarla como lateinit de modo que no tengas que hacer que admita un valor nulo.
private lateinit var letterId: String
- Luego, anula
onCreate()
(noonCreateView()
nionViewCreated()
) 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.
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.
- Por último, puedes acceder a
letterId
cuando configuras el adaptador de la vista de reciclador. Reemplazaactivity?.intent?.extras?.getString(LETTER).toString()
enonViewCreated()
porletterId
.
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.
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.
- 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>
- Puedes configurar la etiqueta de cada fragmento en el gráfico de navegación. Regresa a
nav_graph.xml
y seleccionaletterListFragment
en el árbol de componentes. En el panel de atributos, establece la etiqueta en la stringapp_name
.
- Selecciona
wordListFragment
y establece la etiqueta enword_list_fragment_label
.
¡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
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- 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.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- 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
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- 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 enonCreateView()
. - 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 unaFragmentContainerView
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.