Construyendo una App cliente de noticias con el protocolo QUIC, MVVM y Data Binding [parte 1]

Daniel Mendoza
Huawei Developers Latinoamérica
6 min readOct 28, 2020
Blog writer Illustration by Delesign Graphics

La tecnología avanza cada vez más rápido, los teléfonos celulares pasaron de ser enormes bloques con botones a compuadoras miniatura más delgadas que un cuaderno, que podemos llevar en el bolsillo para realizar cualquiera de nuestras tareas (y también hacer llamadas). Este tipo de avances logran que las personas cada vez estén más y mejor conectadas, aunque esto por sí mismo no sería posible, sin los protocolos de comunicación que se encargan de llevar toda la información que compartimos.

El protocolo QUIC (también conocido como HTTP/3) fue inicialmente desarrollado por Google y posteriormente convertido en un proyecto de código abierto. Lo interesante de este protocolo es que la transmisión de datos se realiza sobre UDP (a diferencia de su antecesor que viaja sobre TCP), logrando que las conexiones tengan menor latencia (más velocidad).

Algunas empresas ya han comenzado a soportar QUIC en sus servidores, ahora del lado del cliente, nos toca preparar nuestras apps para adoptar este nuevo protocolo de transmisión de datos, afortunadamente hay formas de hacerlo sin perder compatibilidad con las versones 1 y 2 de HTTP. En este artículo les mostraré cómo crear un cliente de noticias compatible con QUIC usando MVVM y Data Binding.

Requerimientos:

Creando el proyecto

Lo primero que debemos hacer es crear un nuevo proyecto de Android Sudio con un Empty Activity, orientado a versiones de Android 6 o superior.

Nuevo proyecto en Android Studio

Agregando las dependencias

Para lograr nuestro objetivo, vamos a necesitar las siguientes dependencias:

  • RecyclerView: Para desplegar nuestras noticias en una lista
  • SwipeRefreshLayout: Para ofrecer la posibilidad de refrescar las noticias
  • HQUIC: Para conectarnos con el API REST mediante QUIC
  • kapt: Para usar Data Binding con Kotlin
  • Lifecycle: Para usar MVVM

Dependencias de UI

Primero agreguemos las dependencias de RecyclerView y SwipeRefreshLayout, añadiendo las siguientes lineas a nuestro archivo build.gradle a nuvel de app:

dependencies{
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}

Agregando QUIC

Para agregar el SDK de HQUIC debemos agregar el repositorio Maven de Huawei a nuestro archivo build.gradle a nivel de proyecto

buildscript {repositories {maven { url 'https://developer.huawei.com/repo/' }// This linegoogle()jcenter()}dependencies {classpath 'com.android.tools.build:gradle:3.3.2'}}}allprojects {repositories {maven { url 'https://developer.huawei.com/repo/' }// and this onegoogle()jcenter()}}task clean(type: Delete) {delete rootProject.buildDir}

Ahora agreguemos la dependencia de HQUIC a nuestro build.gradle a nivel de app.

implementation 'com.huawei.hms:hquic-provider:5.0.0.300'

Dependencias de arquitectura

Para usar Data Binding con Kotlin necesitamos agregar el plugin de kapt en nuestro archivo build.gradle a nivel de app.

// asegurate de agregar el plugin de kapt después del plugin de kotlin
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

Ahora debemos agregar las dependencias de kapt y MVVM

dependencies {
...
implementation "android.arch.lifecycle:extensions:1.1.1"
kapt 'com.android.databinding:compiler:3.1.4'

}

Por último hay que habilitar el soporte para View Binding y Data Binding dentro de buildFeatures

android {
...

buildFeatures{
dataBinding true
viewBinding true
}
}

Construyendo el cliente QUIC

Vamos a crear un cliente llamado HQUICService, basado en el ejemplo proporcionado por Huawei, esta clase abstrae las funcionalidades de QUIC y nos permite hacer peticiones REST de forma sencilla.

Nota: Si el servidor no soporta QUIC, HQUIC realizará una petición HTTP/2, por lo que implementar este SDK no supone ningún problema.

class HQUICService(val context: Context) {

private val TAG = "HQUICService"

private val DEFAULT_PORT = 443

private val DEFAULT_ALTERNATEPORT = 443

private val executor: Executor = Executors.newSingleThreadExecutor()

private var cronetEngine: CronetEngine? = null

private var callback: UrlRequest.Callback? = null


/**
* Asynchronous initialization.
*/
init {
HQUICManager.asyncInit(
context,
object : HQUICManager.HQUICInitCallback {
override fun onSuccess() {
Log.i(TAG, "HQUICManager asyncInit success")
}

override fun onFail(e: Exception?) {
Log.w(TAG, "HQUICManager asyncInit fail")
}
})
}

/**
* Create a Cronet engine.
*
*
@param url URL.
*
@return cronetEngine Cronet engine.
*/
private fun createCronetEngine(url: String): CronetEngine? {
if (cronetEngine != null) {
return cronetEngine
}
val builder = CronetEngine.Builder(context)
builder.enableQuic(true)
builder.addQuicHint(getHost(url), DEFAULT_PORT, DEFAULT_ALTERNATEPORT)
cronetEngine = builder.build()
return cronetEngine
}

/**
* Construct a request
*
*
@param url Request URL.
*
@param method method Method type.
*
@param headers Request Headers
*
@param body Request body
*
@return UrlRequest urlrequest instance.
*/
private fun builRequest(
url: String,
method: String,
headers: HashMap<String, String>?,
body:ByteArray?
): UrlRequest? {
val cronetEngine: CronetEngine? = createCronetEngine(url)
val requestBuilder = cronetEngine?.newUrlRequestBuilder(url, callback, executor)
requestBuilder?.apply {
setHttpMethod(method)
if(method=="POST"){
body?.let {
setUploadDataProvider(UploadDataProviders.create(ByteBuffer.wrap(it)), executor) }
}
headers?.let{
for (key in it.keys) {
addHeader(key, headers[key])
}
}
return build()
}
return null
}

/**
* Send a request to the URL.
*
*
@param url Request URL.
*
@param method Request method type.
*
@param headers Request Headers
*
@param body Request body
*/
fun sendRequest(url: String, method: String, headers: HashMap<String, String>?=null,body:ByteArray?=null) {
Log.i(TAG, "callURL: url is " + url + "and method is " + method)
val urlRequest: UrlRequest? = builRequest(url, method, headers,body)
urlRequest?.apply { urlRequest.start() }
}

/**
* Parse the domain name to obtain the host name.
*
*
@param url Request URL.
*
@return host Host name.
*/
private fun getHost(url: String): String? {
var host: String? = null
try {
val url1 = URL(url)
host = url1.host
} catch (e: MalformedURLException) {
Log.e(TAG, "getHost: ", e)
}
return host
}

fun setCallback(mCallback: UrlRequest.Callback?) {
callback = mCallback
}
}

El siguiente paso es definir el modelo de datos. Afortunadamente, las Data Clases de Kotlin hacen este proceso bastante sencillo.

data class Article(val author:String,
val title:String,
val description:String,
val url:String,
val _time:String){
//El campo time viene en formato UTC, por lo que tendremos que formatearlo antes de mostrarlo.
val time:String
get() {
val date= SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()).parse(_time)
return date?.toString() ?: ""
}
}

Ahora construyamos el cliente de noticias que se encargará de realizar la solicitud a newsapi.org. Esta clase hereda de UrlRequest.Callback, por lo que escuchará todos los eventos de la petición.

class NewsClient(context: Context): UrlRequest.Callback() {
var hquicService: HQUICService? = null
val CAPACITY = 10240
val TAG="NewsDownloader"
var response:StringBuilder=java.lang.StringBuilder()
var listener:NewsClientListener?=null

init {
hquicService = HQUICService(context)
hquicService?.setCallback(this)
}


fun getNews(url: String, method:String){
hquicService?.sendRequest(url,method)
}

override fun onRedirectReceived(
request: UrlRequest,
info: UrlResponseInfo,
newLocationUrl: String
) {
request.followRedirect()
}

override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
Log.i(TAG, "onResponseStarted: ")
val byteBuffer = ByteBuffer.allocateDirect(CAPACITY)
request.read(byteBuffer)

}

override fun onReadCompleted(
request: UrlRequest,
info: UrlResponseInfo,
byteBuffer: ByteBuffer
) {
Log.i(TAG, "onReadCompleted: method is called")
val readed=String(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.position())
response.append(readed)
request.read(ByteBuffer.allocateDirect(CAPACITY))
}

override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {
//If everything is ok you can read the response body
val json=JSONObject(response.toString())
val array=json.getJSONArray("articles")
val list=ArrayList<Article>()
for (i in 0 until array.length()){
val article=array.getJSONObject(i)
val author=article.getString("author")
val title=article.getString("title")
val description=article.getString("description")
val time=article.getString("publishedAt")
val url=article.getString("url")
list.add(Article(author, title, description, url, time))
}
listener?.onSuccess(list)
}

override fun onFailed(request: UrlRequest, info: UrlResponseInfo, error: CronetException) {
//If someting fails you must report the error
listener?.onFailure(error.toString())
}

public interface NewsClientListener{
fun onSuccess(news:ArrayList<Article>)
fun onFailure(error: String)
}

Como puedes ver, en el método onReadCompleted se agregan los datos obtenidos a un StringBuilder.

response.append(readed)

El SDK de HQUIC sólo lee cierta cantidad de bytes a la vez, por lo que el método onReadCompleted será llamado varias veces hasta leer la respuesta completa.

Una vez que se hayan obtenido todos los datos de forma satisfactoria, se llamará al método onSucceeded. De lo conrario, se llamará a onFailed. En ambos casos, el resultado será reportado a la instancia de NewsClientListener.

Eso es todo por ahora, nos vemos en la Parte 2

--

--