Blog writer Illustration by Delesign Graphics

En la Parte 1 configuramos todas las dependencias de nuestro proyecto y construimos el cliente de noticias que se encargará de conectarse con el servicio de newsapi.org. Llegó la hora de definir la parte visual de nuestra aplicación y el ViewModel que la conectará con la fuente de datos.

Definiendo las vistas

Nuestra app sólo requiere 2 layouts:

  • activity_news: Será la vista principal de nuestra app. Para generar este archivo hice un Refactor -> Rename al archivo activity_main.xml
  • item_view: Será la vista de cada una de nuestras noticias.

activity_news.xml

En este caso el Parent es un FrameLayout, pero cómo sólo tiene un Children que ocupa todo el espacio en pantalla, podría reemplazarse con otro Layout como LinearLayour o ConstraintLayout.

Agregaremos un SwipeRefreshLayout a nuestra vista, y dentro de este colocaremos nuestro RecyclerView.

<?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=".NewsActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/swipeRefreshLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerNews"
android:layout_width="match_parent"
android:layout_height="match_parent">

</androidx.recyclerview.widget.RecyclerView>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>

item_view.xml

Es una vista realmente sencilla, consta de un ConstraintLayout con 3 TextViews para desplegar el título de la noticia, la descripción o resumen y la fecha de publicación. Estos 3 datos serán añadidos a la vista mediante DataBinding con una instancia de nuestra Data Class: Article.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data class="ArticleBinding">

<variable
name="article"
type="com.hms.demo.hquicnews.Article" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:background="@color/gray"
android:clickable="true"
android:elevation="5dp"
android:padding="5dp">

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@{article.title}"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@{article.description}"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />

<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@{article.time}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Como puedes ver, la propiedad Text de cada TextView está asociada con una propiedad de nuestra instancia de Article definida dentro de la etiqueta <data>.

Contruyendo el ViewModel

Definiremos una clase llamada NewsViewModel que extienda de ViewModel. Este se encargará de comunicar nuestro Activity con la fuente de datos, además de que mantendrá en memoria nuestra lista de noticias, haiendo que esta no se vea afectada por la rotación de pantalla.

class NewsViewModel : ViewModel(), NewsClient.NewsClientListener {
private val API_KEY="YOUR_API_KEY" //get your API key here: https://newsapi.org/register
private val URL = "https://newsapi.org/v2/top-headlines?apiKey=$API_KEY"
private val METHOD = "GET"
var newsClient:NewsClient?=null

private val _news=MutableLiveData<ArrayList<Article>>().apply{
value= ArrayList()
}
val news: LiveData<ArrayList<Article>> = _news
}

Nota: Para efectos demostrativos dejaré el API key de newsapi.org dentro de la clase NewsViewModel, sin embargo, para mantener tus keys a salvo es altamente recomendable que sigas esta guía.

Comunicandonos con el Activity

Debemos notificar al Activity cuando la descarga de noticias se complete, para que esta pueda cambiar el estado del SwipeRefreshLayout. Definamos entonces una interfáz para indicar al Activity que la descarga se completó o si el usuario hizo clic en alguna de las noticias.

interface NewsViewModelListener{
fun onNewsDownloadComplete()
fun onItemClick(article: Article)
}

Descargando las noticias

Nuestro ViewModel se encargará de crear una instancia de NewsClient para descargar las noticias según el país que esté configurado en el sistema.

public fun getNews(context: Context,country:String=Locale.getDefault().country) {
if(newsClient==null){
newsClient=NewsClient(context)
}
val url= "$URL&country=$country"
newsClient?.apply {
listener=this@NewsViewModel
getNews(url,METHOD)
}
}

Escuchando la respuesta

Sobreescribiremos los métodos de la interfáz NewsClientListener para escuchar el resultado de la petición. Si las noticias se obtuvieron satisfactoriamente, se actualizará el MutableLiveData que contiene la lista de noticias

override fun onSuccess(news: ArrayList<Article>) {
_news.postValue(news)
listener?.onNewsDownloadComplete()
}

override fun onFailure(error: String) {
listener?.onNewsDownloadComplete()
}

Manejando la rotación de pantalla

Si el usuario gira su dispositivo para pasar de Portrait a Landscape o viceversa, el ciclo de vida de nuestro activity volverá a comenzar. Si le indicamos al ViewModel que descargue las noticias automáticamente al iniciar nuestro Activity, también hará la petición al girar la pantalla, aún si ya hay noticias mostrandose. Es por esto que debemos crear un método que impida la descarga si ya se tiene la lista de noticias.

public fun loadNews(context: Context){
news.value?.apply {
if(isEmpty()) getNews(context)
else listener?.onNewsDownloadComplete()
}
}

De esta forma llamaremos a loadNews desde el ciclo de vide de nuestro Activity y llamaremos a getNews sólo cuando el usuario use el SwipeRefreshLayout para refrescar las noticias.

Escuchando el clic de las noticias

Preparemos nuestro ViewModel para escuchar los clics que el usuario haga sobre alguna de las noticias de la lista.

public fun onItemClick(article:Article){
listener?.onItemClick(article)
}

Modificaremos un poco el archivo item_view.xml para que notifique los eventos onClick al ViewModel. Primero debemos añadir una variable en el elemento <data>

<data class="ArticleBinding">

<variable
name="article"
type="com.hms.demo.hquicnews.Article" />

<variable
name="mainVM"
type="com.hms.demo.hquicnews.NewsViewModel" />
</data>

Ahora sólo queda agregar el evento onClick a nuestro ConstraintLayout para reportarle al ViewModel cada vez que el usuario haga clic en una noticia.

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:background="@color/gray"
android:clickable="true"
android:elevation="5dp"
android:onClick="@{()->mainVM.onItemClick(article)}" android:padding="5dp">

Eso es todo por ahora. En la Parte 3 crearemos el adapter de nuestro RecyclerView y agregaremos la funcionalidad del Activity para completar nuestra app.

--

--