Last Updated: 2021-31-05

Interfaces gráficas en Android

La interfaz de usuario de una aplicación es todo aquello que el usuario puede ver y con lo que puede interactuar. Para diseñar interfaces gráficas de aplicaciones móviles se requieren distintos tipos de vistas que brinden una experiencia agradable y familiar al usuario. Además de las vistas, es necesario definir un flujo de aplicación que maximice la usabilidad y la facilidad de interacción con el usuario. Para manejar estos flujos Android ofrece una estructura de navegación sólida con base en componentes denominados Actividades y Fragmentos. Estos componentes representan una pantalla de la aplicación, y los cuales se comunican por medio de Intents, manejadores y de la navegación de Jetpack.

Dada la naturaleza del sistema operativo de Android, estos componentes de interfaz gráfica manejan ciclos de vida complejos donde se definen eventos de creación, pausa, reinicio y finalización de la aplicación para asegurar un correcto manejo de los recursos del dispositivo móvil en cuanto a la carga de información y renderizado de la interfaz. Por este motivo, es muy importante conocer y comprender el funcionamiento de dichos componentes en detalle.

¿Qué construirá?

Al final de este tutorial usted tendrá:

¿Qué aprenderá?

Al desarrollar este tutorial aprenderá:

¿Qué necesita?

Fragmentos de una actividad

Existen muchos escenarios donde las actividades deben ser fragmentadas, dado que su comportamiento no puede ser representado de una forma suficientemente adecuada en una sola actividad o en actividades separadas. Para esto, Android ofrece los Fragments, los cuales pueden ser combinados como secciones modulares de una actividad, las cuales manejan ciclos de vida y eventos propios.

Para incluir fragmentos dentro de una actividad, no es necesario modificar el archivo de manifiesto, sino el archivo de recursos del layout correspondiente a la actividad. Existe la posibilidad de definir cada uno de los fragmentos, con sus respectivos elementos gráficos como parte del layout de la actividad, como se muestra en el siguiente ejemplo:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment android:name="com.example.news.ArticleListFragment"
            android:id="@+id/list"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
    <fragment android:name="com.example.news.ArticleReaderFragment"
            android:id="@+id/viewer"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
</LinearLayout>

No obstante, también existe la posibilidad de agregar los fragmentos de forma programática por medio de FragmentTransactions administradas por un FragmentManager. El código en Kotlin para realizar esta operación es el siguiente:

val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val fragment = ExampleFragment()
fragmentTransaction.add(R.id.fragment_container, fragment)
fragmentTransaction.commit()

Finalmente, es importante recordar que a las clases correspondientes a los fragmentos es necesario declararles Fragment como superclase y, en caso de ser necesario, configurar el comportamiento que se desea que tengan en el ciclo de vida. El ciclo de vida de los fragmentos es distinto al de las actividades, y se ve fuertemente influenciado por el de su actividad contenedora. A continuación se muestra un diagrama, tomado de la documentación oficial de Android Developers (y disponible en este enlace: https://developer.android.com/guide/components/fragments?hl=es#Lifecycle), que explica las distintas fases del ciclo de vida:

Secuencia de callbacks que existen en el ciclo de vida. Cuando la actividad está en estado created, se llaman los métodos onAttach, onCreate, onCreateView, onActivityCreated. Cuando la actividad está en estado started, se llama el método onStart. Cuando la actividad está en estado resumed, se llama el método onResume. Cuando la actividad está en estado paused, se llama el método onPaused. Cuando la actividad está en estado stopped, se llama el método onStop. Cuando la actividad está en estado destroyed, se llaman los métodos onDestroyView, onDestroy y onDetach

Imagen 1. Diagrama del ciclo de vida de un fragmento

En la primera parte del tutorial usted desarrolló la primera versión de la aplicación utilizando únicamente Activities de Android. Ahora usted partirá del proyecto que desarrolló allí, reemplazando las actividades de listado y de detalles por una sola actividad vacía que contiene un fragmento correspondiente al listado y uno correspondiente a la vista de detalle. Recuerde que, cuenta con la versión desarrollada por los tutores del curso, del código fuente de la aplicación que debió desarrollar para el tutorial anterior. Esta versión está en el repositorio de los tutoriales del curso, disponible en el siguiente enlace: https://github.com/TheSoftwareDesignLab/MISW4104-Ejemplos. Así mismo, recuerde que este tutorial está desarrollado con base en el codelab Fragments and Navigation component de Android Developers (disponible en el siguiente enlace: https://developer.android.google.cn/codelabs/basic-android-kotlin-training-fragments-navigation-component#1)

Crear los nuevos fragmentos

Desde Android Studio, seleccione en el menú superior, la opción File > New > Fragment > Fragment (Blank). Esto abrirá una ventana como la siguiente, donde usted debe introducir la información de identificación de los nuevos fragmentos:

El cuadro muestra que se va a crear un fragmento vacío. Tiene un campo de texto para el nombre del fragmento, uno para el nombre del recurso de layout, y otro para el lenguaje (Java o Kotlin)

Imagen 2. Cuadro de diálogo para crear un nuevo fragmento

Cree un fragmento con el nombre LetterListFragment y otro fragmento con el nombre WordListFragment. Podrá ver cómo se crea, para cada uno, un archivo en formato .xml, y un archivo .kt con el código para manejar el fragmento. Una vez se hayan creado los archivos, modifique el código fuente de ambos fragmentos de forma que la declaración de las clases esté vacía.

Ahora, modifique el XML de los archivos fragment_letter_list.xml y fragment_word_list.xml agregando las etiquetas que existían en el archivo activity_main.xml, con la única diferencia de que el valor de tools:context será .LetterListFragment en el caso del primer archivo y .WordListFragment en el caso del segundo.

Implementar el fragmento del listado de letras

En primer lugar, abra el archivo LetterListFragment.kt para modificarlo y agregar el binding necesario. El View Binding facilita la interacción del código con las vistas, ya que contiene referencias directas a todos los componentes de una vista. Para lograr esto, inicialmente defina una propiedad de la clase de la siguiente forma:

private var _binding: FragmentLetterListBinding? = null

Esta se va a referir al View Binding de su fragmento y será nuleable según la etapa del ciclo de vida de su fragmento. Como ésta obtendrá su valor en el llamado al método onCreateView(), defina allí una nueva variable que contenga el valor asignado de la siguiente forma:

private val binding get() = _binding!!

Además de estas propiedades, agregue una que representará al componente RecyclerView de la siguiente forma:

private lateinit var recyclerView: RecyclerView

Además de esto, como el proceso de layout inflation ocurre en el método onCreateView() de los fragmentos, es necesario que se asegure de inflar la vista y de asignar el valor del View Binding. Inflar el layout significa crear los objetos de tipo View para cada uno de los ítems definidos en el archivo XML del layout. Esto se logra de la siguiente forma:

override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentLetterListBinding.inflate(inflater, container, false)
   val view = binding.root
   return view
}

Prosiguiendo con los métodos para definir el ciclo de vida del fragmento, defina ahora el método onViewCreated() para manipular los elementos de la interfaz que desea incluir de forma dinámica, que en este caso únicamente es el RecyclerView. El código correspondiente se muestra a continuación:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   recyclerView.layoutManager = LinearLayoutManager(context)
   recyclerView.adapter = LetterAdapter()
}

Asegúrese de cerrar todos los recursos al final del ciclo de vida en el método onDestroyView(), esto es, reiniciar el valor de la propiedad _binding a null, cuando la aplicación cierre el fragmento o su actividad padre. Para esto, incluya el siguiente código:

override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}

Por último, para terminar de utilizar View Binding en esta versión de la aplicación, modifique el archivo que contiene el código correspondiente al segundo fragmento.

Implementar el fragmento del listado de palabras

Ahora, proceda a cambiar el archivo WordListFragment.kt, donde ejercerá cambios similares a los que hizo en la anterior sección para el otro fragmento. En primer lugar, debe crear nuevamente dos propiedades de la clase que representen el View Binding y su referencia para evitar nulidad, de la siguiente forma:

private var _binding: FragmentWordListBinding? = null
private val binding get() = _binding!!

Luego debe, nuevamente, configurar los métodos del ciclo de vida del fragmento, partiendo por el método onCreateView(), para que quede de la siguiente forma:

override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentWordListBinding.inflate(inflater, container, false)
   return binding.root
}

Luego de esto, agregue el método onViewCreated() para configurar el RecyclerView, asegurándose de que el adapter incluya la letra adecuada para cada uno de los botones. Esto lo logrará con el siguiente código:

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)
   )
}

Finalmente, recuerde cerrar los recursos utilizados con el método onDestroyView(), cuyo código fuente corresponde al siguiente:

override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}

Reemplazar el flujo de navegación por medio de Intents

Dado que migró las dos actividades anteriores a una sola actividad general con dos fragmentos, en este momento le debe estar sobrando la actividad DetailActivity. Para asegurarse de eliminar todos sus contenidos y referencias, seleccione el archivo DetailActivity.kt en el panel lateral izquierdo de Android Studio y elija la opción "Delete". Esto le mostrará un cuadro de diálogo con la opción de ejecutar un "Safe delete", la cual, en este caso, es irrelevante, ya que no habrá referencias faltantes. Luego de eliminar este archivo, elimine, de la misma forma, el archivo activity_detail.xml. Finalmente, elimine la referencia a dicho archivo en el archivo de manifiesto.

En este momento, la aplicación sólo le mostrará el primer fragmento y no habrá forma de que acceda al fragmento del listado de palabras de una letra. Para crear esta conexión, debe utilizar los componentes de navegación de Jetpack. Primero, agregue las dependencias necesarias para utilizar los componentes de navegación, lo cual implica que en el archivo build.gradle a nivel de proyecto, incluya la pareja llave-valor nav_version = "2.3.1"

en el apartado buildscript > ext, y la línea

 classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"

en el apartado buildscript > dependencies. En el archivo build.gradle, a nivel del directorio app, contenga las siguientes dependencias:

implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

Además, en el archivo build.gradle a nivel del directorio app incluya, en el apartado plugins, la siguiente línea:

id 'androidx.navigation.safeargs.kotlin'

Luego tendrá que ejecutar un Gradle Sync para asegurar que las dependencias del proyecto queden bien construidas.

Una vez su proyecto cuente con las dependencias necesarias, agregue el grafo de navegación. Este consiste en un archivo XML que le permite definir las posibles rutas entre las vistas que se definieron para su aplicación. Lo primero que debe hacer es modificar el archivo para que tenga el siguiente contenido dentro del FrameLayout:

<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">
   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/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/nav_graph"/>
</FrameLayout>

Luego de esto, cree un archivo llamado nav_graph.xml por medio del botón File > New > Android Resource File, y asígnele el tipo de recurso "Navigation". Al seleccionar este nuevo archivo, se le mostrará un editor gráfico donde podrá definir acciones de navegación entre diferentes destinos. En la esquina superior izquierda podrá ver el botón New, con el cual podrá agregar fragmentos, y luego podrá establecer conexiones arrastrando el cursor desde los círculos de los bordes. Para establecer la comunicación entre los dos fragmentos defina la línea como se muestra a continuación:

La imagen muestra la vista de Android Studio al abrir el archivo xml de navegación del proyecto. Esta vista es gráfica y tiene un panel central con elementos que representan cada vista, y un panel lateral con las propiedades\n

Imagen 3. Gráfico de navegación de los fragmentos

Luego de establecer la línea, haga clic en cada fragmento y asigne, respectivamente, el origen y el destino de la navegación.

Cuando seleccione el fragmento wordListFragment, podrá ubicar a la derecha una pestaña referente a los argumentos. Agregue, desde allí, un argumento llamado letter, de tipo string, que reemplazará el Intent que antes comunicaba al listado de letras con el listado de palabras. Luego de esto, modifique el archivo LetterAdapter.kt de forma que el método onClickListener del botón tenga únicamente las siguientes instrucciones:

override fun onBindViewHolder(holder: LetterViewHolder, position: Int) {
   val item = list.get(position)
   holder.button.text = item.toString()

   // Assigns a [OnClickListener] to the button contained in the [ViewHolder]
   holder.button.setOnClickListener {
       // Create an action from WordList to DetailList
       // using the required arguments
       val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString())
       // Navigate using that action
       holder.view.findNavController().navigate(action)
   }
}

Así mismo, debe modificar el archivo MainActivity.kt para que incluya la siguiente propiedad:

private lateinit var navController: NavController

En cuanto al método onCreate(), debe agregar las siguientes líneas de código posterior al llamado de la instrucción setContentView(), para poder vincular adecuadamente los controladores de navegación:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

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

   // Get the navigation host fragment from this Activity
   val navHostFragment = supportFragmentManager
       .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
   // Instantiate the navController using the NavHostFragment
   navController = navHostFragment.navController
   // Make sure actions in the ActionBar get propagated to the NavController
   setupActionBarWithNavController(navController)
}

Así mismo, debe agregar un método para configurar la navegación hacia arriba con las siguientes líneas de código:

override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}

Después, en el archivo WordListFragment.kt, debe reemplazar el código encargado de recibir y procesar el parámetro correspondiente a la letra seleccionada para buscar palabras del diccionario de la siguiente forma. En primer lugar, defina una propiedad para representar la letra con la siguiente línea:

private lateinit var letterId: String

Luego, al final del método onCreate, incluya las siguientes líneas, que capturan los argumentos recibidos por el fragmento y los asignan a la variable instanciada anteriormente, revisando cuidadosamente que no haya errores causados por nulidad en los argumentos:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   // Retrieve the LETTER from the Fragment arguments
   arguments?.let {
       letterId = it.getString(LETTER).toString()
   }
}

Por último, en la instanciación del adaptador del RecyclerView, reemplace el parámetro de la letra que obtenía el valor desde los extras del intent por la variable, de forma que se quedará con el siguiente código:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   val recyclerView = binding.recyclerView
   recyclerView.layoutManager = LinearLayoutManager(requireContext())
   recyclerView.adapter = WordAdapter(letterId, requireContext())

   // Adds a [DividerItemDecoration] between items
   recyclerView.addItemDecoration(
       DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
   )
}

Ahora podrá ejecutar la nueva versión de la aplicación, la cual hace uso de fragmentos. Interactúe con ella para validar que el comportamiento funcional es exactamente igual al de la aplicación con la que comenzó este tutorial, la cual fue desarrollada utilizando solo Activities.

¡Felicidades!

Al finalizar este tutorial, pudo familiarizarse con los conceptos de Fragmentos en Android, los ciclos de vida de los mismos y el flujo de información por medio de la navegación de Android.

Ahora podrá implementar sus propios fragmentos en una Actividad de una aplicación de Android que contenga varias pantallas, establecer una navegación adecuada y ahondar en las particularidades del desarrollo con Kotlin para crear aplicaciones modernas y cada vez más complejas.

Créditos

Versión 1.1 - Mayo 31, 2021

Juan Sebastián Espitia Acero

Autor

Norma Rocio Héndez Puerto

Revisora

Mario Linares Vásquez

Revisor