¡Hola! Bienvenido a este taller de Android creado por @AndreAndyP para Facebook Developer Circles Ciudad de México.
Última actualización: Agosto del 2021.
Antes de comenzar, vamos a hacer algo muy rápidamente: abre Android Studio y crea un nuevo proyecto. Selecciona New Project. 
Después selecciona empty activity. 
Dale un nombre a la app (yo escogí "Taller Dev Circles CDMX"), selecciona Kotlin como lenguaje y el nivel de API puede ser cualquiera, siempre y cuando sea 21 o mayor. El package name puede ser (y debería ser) diferente.

En lo que Android Studio crea la aplicación, hablemos un poco de desarrollo moderno en Android.
Comencemos por AndroidX, una mejora con respecto a la biblioteca de compatibilidad (support libraries). Todas las clases que formaban parte del espacio de nombres android.support.* ahora se encuentran en el espacio de nombres androix.*, por lo que se vuelve más fácil utilizarla.
Todas estas bibliotecas ahora se actualizan de forma independiente y utilizan versionamiento semántico para indicar la versión en la que están. A día de hoy, las bibliotecas se siguen actualizando, mientras que la bibilioteca de compatibilidad se quedó en la versión 28.0.0.
Nuestra app que acabamos de crear ya incluye AndroidX, no es necesario configurar nada más. De hecho, por eso no seleccionamos la casilla que dice "Use legacy android.support libraries". Incluso, aquellas apps creadas para Android 10 en adelante (nivel de API 29), ya no es posible usar la biblioteca de compatibilidad.

Si tienes un proyecto que aún utiliza las bibliotecas de soporte, lo mejor es que lo migres a AndroidX. Para hacerlo, solo tienes que hacer lo siguiente.
Android Jetpack es un conjunto de bibliotecas que ayudan a nosotros los desarrolladores a escribir código que cumpla con las mejores prácticas y reducir el código innecesario (boilerplate).
Gracias a Android Jetpack, hoy tenemos disponibles muchas bibliotecas que nos facilitan la vida con funcionalidades que antes nos hubiese tomado mucho tiempo implementar.
Entre las que destacan a primera vista, son las llamadas bibliotecas de arquitectura:
La razón de que destaquen es debido a que Google sugiere que la arquitectura de las aplicaciones se basen en el patrón de arquitectura Model-View-ViewModel (MVVM), ya que en este patrón, los datos son quienes manejan la interfaz de usuario.
Este es un diagrama de la arquitectura recomendada. Si quieres saber más, visita la guía de arquitectura de apps.

Android KTX son extensiones creadas en Kotlin para escribir código concreto y mucho más sencillo utilizando las capacidades de Kotlin al máximo.
Existen bibliotecas KTX para muchísimos componentes de Android, como activities, fragments, SQLite, SharedPreferences, Google Maps, etc. Para utilizar las bibliotecas KTX, no hace falta más que añadirlas a las dependencias de la app.
Veamos un ejemplo con Google Maps. Normalmente para incluir esta funcionalidad, hay que agregar la siguiente línea en el build.gradle del módulo:
implementation 'com.google.maps.android:maps:2.3.0'
Para usar KTX, hay que añadir el sufijo -ktx a la biblioteca. ¡Y es todo!
implementation 'com.google.maps.android:maps-ktx:2.3.0'
Sigamos con Google Maps. Para añadir un marcador a un mapa, normalmente haríamos esto:
val markerOptions = MarkerOptions().apply {
title("Ubicación actual")
snippet("Aquí es donde estas ahora")
position(latLng)
}
val marker = map.addMarker(markerOptions)
Gracias a la biblioteca KTX, podemos hacer esto:
val marker = map.addMarker {
title("Ubicación actual")
snippet("Aquí es donde estas ahora")
position(latLng)
}
¡Definitivamente mucho más sencillo! Más información de Android KTX aquí.
En este taller vamos a hacer la siguiente app: vamos a mostrar una lista de países junto con algunos detalles de cada país. Al tocar un país, podremos ver más detalles de cada país.

En caso de que no tengamos conexión a internet, y si ya accedimos a la app por primera vez, podremos navegar por la app gracias a una caché local.

Iremos en este orden:
item_country.fragment_country.CountryAdapter.La ventana debe quedar así: (recuerda que el package name puede ser y debería ser diferente)
.
Todo lo demás déjalo como te apareció, solo asegúrate de que el lenguaje seleccionado sea Kotlin. Selecciona "Finalizar".
Muchos archivos han sido creados. Veamos uno por uno:
CountryFragment.kt: El fragment encargado de preparar la vista para mostrar la lista de países.fragment_country.xml: El archivo XML donde crearemos la interfaz de usuario de la lista.CountryAdapter.kt: La clase encargada de "convertir" la lista de países a una lista visible para el usuario.item_country.xml: El archivo XML donde crearmos la interfaz de usuario que mostrará un elemento en la lista.placeholder.PlaceholderContent: Un archivo que contiene datos de muestra temporales. No lo usaremos mucho.Para lanzar la app con el código que hasta ahorita llevamos, abre el archivo activity_main.xml y reemplaza la etiqueta TextView por lo siguiente:
<androidx.fragment.app.FragmentContainerView
android:id="@+id/navigation_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.andreandyp.tallerdevcirclescdmx.CountryFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
La etiqueta androidx.fragment.app.FragmentContainerView es la que se usa ahora para mostrar fragments en pantalla. El ID que usamos tiene un propósito específico del que hablaremos más adelante. El atributo android:name indica cuál es el fragmento que queremos incrustar y mostrar en pantalla, en este caso, mostraremos CountryFragment.
Tras ejecutar la app y si todo salió bien, deberías ver lo siguiente en tu dispositivo físico o en tu dispositivo virtual Android o AVD (Android Virtual Device) en adelante.

Como puedes ver, ya tenemos una lista que se puede mostrar en pantalla. Prueba a jugar con la lista: ¡se puede desplazar sobre ella!
Ahora vamos a diseñar la UI. Vamos a darle a la pantalla la funcionalidad de actualizar el contenido gracias a SwipeRefreshLayout. Abre el archivo fragment_country.xml y cambia la etiqueta RecyclerView por SwipeRefreshLayout. Debe quedar así:
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/countries_swipe_refresh"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CountryFragment">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Adentro de la etiqueta del SwipeRefreshLayout, vamos a añadir una etiqueta ConstraintLayout. Esta etiqueta debe quedar así:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
Adentro de la etiqueta del ConstraintLayout, vamos a añadir 2 elementos: el RecyclerView que nos ayudará a mostrar la lista y un TextView que nos ayudará a avisar al usuario cuando no hay internet en la aplicación.
La etiqueta RecyclerView debe quedar así:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/countries_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_country" />
Nota el último atributo del elemento. Ese atributo que utiliza el espacio de nombres tools es muy útil durante el desarrollo porque nos permite darles valores de prueba a distintos atributos de una vista, atributos que se mostrarán en la vista previa del IDE.
En este caso, tools:listitem le indica a Android Studio que renderice el layout del archivo item_country.xml. Haz click en el botón que dice "Split" o "Design" para ver cómo ese archivo que indicamos en tools:listitem se muestra en la vista previa.

Añade también el elemento TextView. Debe quedar así:
<TextView
android:id="@+id/no_internet_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="No tienes conexión a internet"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
Nota cómo estamos utilizando el espacio de nombres tools de nuevo. Al momento de ejecutar la app, la vista va a desaparecer, pero en la vista previa del IDE veremos que el elemento sigue visible. Por esto es tan útil este espacio de nombres.
Si presionas Alt (Options en Mac) + Enter cuando el mouse está sobre el texto, te aparecerán varias opciones. Selecciona "Extract string resource" y luego dale el nombre no_internet_connection. Guárdalo únicamente en "values". El atributo debe quedar así:
android:text="@string/no_internet_connection"
Finalmente, añade el siguiente atributo al elemento RecyclerView:
app:layout_constraintBottom_toTopOf="@+id/no_internet_text_view"
Y modifica el atributo android:layout_height, dale el valor 0dp (También conocido como match_constraint), para que utilice todo el espacio disponible sin tapar al TextView.
La etiqueta RecyclerView debe quedar así:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/countries_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/no_internet_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_country" />
La lista debería verse así en la vista previa.

Abre el archivo item_country.xml. Sustituye el elemento LinearLayout por un elemento ConstraintLayout. Debe quedar así:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<!-- Resto del código -->
</androidx.constraintlayout.widget.ConstraintLayout>
Sustituye el primer elemento TextView por un elemento ImageView. Debe quedar de la siguiente manera:
<ImageView
android:id="@+id/country_flag_image_view"
android:layout_width="64dp"
android:layout_height="64dp"
android:contentDescription="Bandera del país."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
Cambia el primer elemento TextView para que quede de la siguiente manera:
<TextView
android:id="@+id/country_name_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem"
app:layout_constraintStart_toEndOf="@+id/country_flag_image_view"
app:layout_constraintTop_toTopOf="@+id/country_flag_image_view"
tools:text="México" />
Añade un TextView abajo, de la siguiente manera:
<TextView
android:id="@+id/country_capital_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceListItemSecondary"
android:layout_marginStart="@dimen/text_margin"
app:layout_constraintStart_toEndOf="@+id/country_flag_image_view"
app:layout_constraintTop_toBottomOf="@+id/country_name_text_view"
tools:text="Ciudad de México" />
Tras realizar estos cambios, el archivo item_country.xml debería verse así en el panel de vista previa:

Y el archivo fragment_country.xml debería verse así en el panel de vista previa:

Si lanzas ahora mismo la aplicación, verás que no compila por las siguientes líneas del archivo CountryAdapter.kt
val idView: TextView = binding.itemNumber
val contentView: TextView = binding.content
Probablemente más cosas salgan a la vista, como la clase ViewHolder o el parámetro de tipo ItemCountryBinding del constructor, además de la propia clase CountryAdapter si es que nunca has utilizado `RecyclerView. Vamos por partes:
Esta clase, que como su nombre indica sigue el patrón Adapter, se encarga de transformar la lista de elementos (el parámetro values) a una interfaz de usuario. Una clase Adapter nos brinda muchos métodos que podemos sobreescribir para trabajar; pero de todos estos, solamente necesitamos sobreescribir 3 para que se muestre la lista:
onCreateViewHolder: infla el XML que se utilizará para mostrar cada elemento e instancia un ViewHolder.onBindViewHolder: nos da acceso al ViewHolder para que podamos asignarle a las vistas los datos que vienen en nuestra lista (values, en este caso).getItemCount: útil para que el RecyclerView sepa cuántos elementos se tienen que crear.Esta clase nos permite acceder rápidamente a las vistas del elemento que se mostrará en la lista. Un ViewHolder recibe como parámetro el elemento que se mostrará en la lista y dentro del cuerpo de la clase añadimos como campos de esta las vistas a las que necesitamos acceder rápidamente. Esta clase hereda de la clase RecyclerView.ViewHolder y esta última recibe como parámetro la vista padre, es decir, el elemento que se mostrará en la lista.
ItemCountryBinding)Antes en Android se utilizaba el método findViewById(int id) para acceder a una vista. Este método presenta ciertos problemas:
NullPointerException.Para solucionar estos problemas, se creó una solución llamada Data binding. Esta solución fue muy buena pero a veces era muy pesada, ya que añadía mucho tiempo al proceso de build. Esta alternativa es muy buena y se sigue usando, pero a veces es demasiado para aplicaciones sencillas.
Así que, basada en Data binding, llegó otra solución más sencilla: View binding. Esta solución nos permite obtener los beneficios de traer vistas como en Data binding sin añadir tiempo al proceso de build de la app.
Para usar view binding necesitamos una cosa: ir al archivo app/build.gradle y añadir las siguientes líneas:
android {
// Resto del código...
buildFeatures {
viewBinding true
}
}
¡Listo! Ahora cada vez que añadas un nuevo layout XML, Android Studio generará una nueva clase cuyo nombre será igual al archivo XML pero en notación Camel Case y con el sufijo -Binding. Por ejemplo:
activity_main.xml -> ActivityMainBinding.ktfragment_country.xml -> FragmentCountryBinding.ktitem_country.xml -> ItemCountryBinding.ktAsí al usar el método inflate de una clase creada por View binding, este método nos devolverá una variable (normalmente llamada binding) del mismo tipo de la clase creada. Mediante esta variable podremos acceder a las vista definidas en el XML mediante su ID, que serán propiedades de la misma clase. Por ejemplo, para acceder al elemento country_flag_image_view lo podemos hacer mediante binding.countryFlagImageView.
Fácil, rápido y sin peligro de un NullPointerException. Si quieres saber más de View binding, entra aquí.
Ahora que sabemos cómo funciona View binding, pongámoslo a prueba. Dentro de la clase ViewHolder, crea 3 propiedades para acceder al ImageView y a ambos TextView. Deben quedar de la siguiente manera:
inner class ViewHolder(binding: ItemCountryBinding) : RecyclerView.ViewHolder(binding.root) {
val countryFlag: ImageView = binding.countryFlagImageView
val countryName: TextView = binding.countryNameTextView
val capitalName: TextView = binding.countryCapitalNameTextView
}
Puedes deshacerte del método toString(). No lo utilizaremos.
En el método onBindViewHolder, llama a las propiedades countryFlag, countryName y capitalName y asíganles el ícono del launcher y los datos de prueba que Android Studio creó de la clase PlaceholderContent. Debe quedar de la siguiente manera:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
holder.countryFlag.setImageResource(R.mipmap.ic_launcher)
holder.countryName.text = item.id
holder.capitalName.text = item.content
}
Ahora ve a la clase CountryFragment y crea un campo llamado binding de tipo FragmentCountryBinding, debe quedar así:
private lateinit var binding: FragmentCountryBinding
Ahora ve al método onCreateView y crea utilizando View binding la vista de la siguiente manera:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCountryBinding.inflate(layoutInflater, container, false)
return binding.root
}
Borra el método onCreate del fragmento, la propiedad columnCount y el companion object, no necesitamos ese código autogenerado. Abajo del método onCreateView, sobreescribe el método onViewCreated. En este método añadiremos el adapter al RecyclerView, utiliza View binding. Debe quedar así:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.countriesRecyclerView.adapter = CountryAdapter(PlaceholderContent.ITEMS)
binding.countriesSwipeRefresh.isEnabled = false
}
La última línea es para deshabilitar la función del SwipeRefreshLayout, solo será temporalmente. Prueba a lanzar la aplicación, debería verse así:

Ahora vamos a seguir con la pantalla de detalles, donde se mostrará la información de cada país. Para eso, crea un nuevo fragmento haciendo click derecho sobre el módulo app y selecciona New > Fragment > Fragment (Blank). El nombre del fragmento debe ser CountryDetailsFragment y en el cuadro de texto que dice Fragment layout name debe decir fragment_country_details.

Para comenzar, abre el archivo CountryDetailsFragment.kt y elimina lo siguiente:
param1 y param2.onCreate.companion object.De tal manera que solo quede el método onCreateView. El código debe verse así:
class CountryDetailsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_country_details, container, false)
}
}
Ahora abre el archivo fragment_country_details.xml y cambia el elemento FrameLayout por ConstraintLayout. Elimna el TextView que aparece ahí.
Primero, crea un elemento ImageView para mostrar la imagen. Debe ser así:
<ImageView
android:id="@+id/details_country_flag_image_view"
android:layout_width="match_parent"
android:layout_height="128dp"
android:layout_margin="16dp"
android:contentDescription="@string/content_description_flag"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
Luego, crea 2 TextView de la siguiente manera:
<TextView
android:id="@+id/details_country_name_text_view"
style="@style/TextAppearance.MaterialComponents.Headline4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details_country_flag_image_view"
tools:text="Mexico" />
<TextView
android:id="@+id/details_country_capital_name_text_view"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details_country_name_text_view"
tools:text="Mexico City" />
El primero será para mostrar el nombre del país y el segundo para mostrar la capital de dicho país. Ahora crea los textos que servirán como etiquetas para mostrar la población y el área del país. Los valores de @string/details_label_population y @string/details_label_area son "Population" y "Area", respectivamente.
<TextView
android:id="@+id/details_label_population_text_view"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:text="@string/details_label_population"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details_country_capital_name_text_view" />
<TextView
android:id="@+id/details_label_area_text_view"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:text="@string/details_label_area"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details_label_population_text_view" />
Finalmente, añade los TextView para los datos de estas 2 estadísticas:
<TextView
android:id="@+id/details_population_text_view"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details_country_capital_name_text_view"
tools:text="120,000,000" />
<TextView
android:id="@+id/details_area_text_view"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/details_population_text_view"
tools:text="1,900,000 km²" />
Si todo salió bien, deberías ver la pantalla en la vista previa así:
.
Debido a que aún no tenemos los datos necesarios, no podemos probar aún la pantalla. Puedes añadir el fragment al elemento FragmentContainerView del archivo activity_main.xml, pero solamente se verán las etiquetas de "Population" y "Area".
Hemos llegado al fin de la primera parte de este taller de Android. Nos falta mucho por agregar a la app, esto se pondrá cada vez mejor. Si quieres ver todo el código de este taller, ve a este repositorio de GitHub y navega hacia la rama modernas-1.
¡Nos vemos en la siguiente parte!