DEV Community

Cover image for Creación de una aplicación de noticias utilizando el protocolo QUIC [Kotlin]
HuaweiDevsLATAM
HuaweiDevsLATAM

Posted on

Creación de una aplicación de noticias utilizando el protocolo QUIC [Kotlin]

QUIC es un nuevo protocolo de transporte basado en UDP que permite mejorar la velocidad de las conexiones de red debido a la baja latencia de UDP. QUIC significa Quick UDP Internet Connections y está previsto que se convierta en un estándar.
Las características clave de QUIC son:

  • Tiempo de establecimiento de conexión drásticamente reducido
  • Control de congestión mejorado
  • Multiplexación sin bloqueo de cabecera de línea
  • Migración de conexión

Algunas empresas están comenzando a admitir QUIC en sus servidores, ahora desde el lado del cliente, es hora de estar preparados y agregar soporte QUIC a nuestras aplicaciones. Esta vez construiremos una aplicación de noticias con soporte QUIC usando el kit HQUIC.
HQUIC nos permite conectarnos con servicios web a través del protocolo QUIC, si el control remoto no es compatible con QUIC, el kit usará HTTP 2 en su lugar automáticamente.

Requisitos previos

  • Un proyecto de Android Studio
  • Una cuenta de desarrollador en newsapi.org

Agregar las dependencias requeridas

Este ejemplo requerirá las siguientes dependencias:

  • RecyclerView: Para mostrar todas las novedades en una lista.
  • SwipeRefreshLayout: Para permitir que el usuario actualice la pantalla con un gesto de deslizar.
  • HQUIC:El kit que nos permite conectarnos con servicios a través de QUIC

Para usar HQUIC debes agregar el repositorio de Huawei a su nivel superior build.gradle.

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

Ahora agregue las dependencias requeridas a nivel de aplicación build.gradle

implementation 'com.huawei.hms:hquic-provider:5.0.0.300'
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
Enter fullscreen mode Exit fullscreen mode

Diseñando la UI

Mostraremos una lista de elementos dentro de un RecyclerView, por lo que debes preparar el diseño del elemento que se mostrará para cada artículo. Esta vista básica solo mostrará el título, una breve introducción y la fecha de publicación:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
    android:padding="5dp">
<TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:text="TextView"
        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="Content"
        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="TextView"
        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>
Enter fullscreen mode Exit fullscreen mode

Para el diseño de la actividad, el diseño contendrá un SwipeRefresLayout con el RecyclerView dentro

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".MainActivity">
    <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>
Enter fullscreen mode Exit fullscreen mode

Para mostrar los artículos en una vista de reciclador, necesitarás un adaptador, el adaptador informará el evento de clic de cualquier elemento al oyente dado.

class NewsAdapter(val news:ArrayList<Article>,var listener: NewsViewHolder.onNewsClickListener?): RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
class NewsViewHolder(itemView: View, var listener:onNewsClickListener?) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
        public fun init(article: Article){
            itemView.title.text=article.title
            itemView.content.text=article.description
            val date= SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()).parse(article.time)
            itemView.time.text=date?.toString()
            itemView.setOnClickListener(this)
        }
interface onNewsClickListener{
            fun onClickedArticle(position: Int)
        }
override fun onClick(v: View?) {
            listener?.onClickedArticle(adapterPosition)
        }
    }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
        val view=LayoutInflater.from(parent.context)
            .inflate(R.layout.item_view,parent,false)
        return NewsViewHolder(view,listener)
    }
override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
        holder.init(news[position])
    }
override fun getItemCount(): Int {
        return news.size
    }
}
Enter fullscreen mode Exit fullscreen mode

HQUIC Service

Usaremos la clase HQUICService proporcionada por el proyecto de demostración HQUIC. Esta clase abstrae las capacidades del kit HQUIC y nos permite realizar una solicitud fácilmente.

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.
     * @return UrlRequest urlrequest instance.
     */
    private fun builRequest(url: String, method: String): UrlRequest? {
        val cronetEngine: CronetEngine? = createCronetEngine(url)
        val requestBuilder= cronetEngine?.newUrlRequestBuilder(url, callback, executor)
        requestBuilder?.apply {
            setHttpMethod(method)
            return build()
        }
        return null
    }
/**
     * Send a request to the URL.
     *
     * @param url Request URL.
     * @param method Request method type.
     */
    fun sendRequest(url: String, method: String) {
        Log.i(TAG, "callURL: url is " + url + "and method is " + method)
        val urlRequest: UrlRequest? = builRequest(url, method)
        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
    }
}
Enter fullscreen mode Exit fullscreen mode

Descargando las noticias

Definiremos una clase auxiliar para manejar la solicitud y analiza la respuesta en una ArrayList. El kit HQUIC leerá una cierta cantidad de bytes por vez, para respuestas grandes, el método onReadCompleted se llamará más de una vez.

data class Article(val author:String,
                   val title:String,
                   val description:String,
                   val url:String,
                   val time:String)
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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Haciendo la solicitud

Definir propiedades de solicitud.

private val API_KEY="YOUR_API_KEY"
private val URL = "https://newsapi.org/v2/top-headlines?apiKey=$API_KEY"
private val METHOD = "GET"
Enter fullscreen mode Exit fullscreen mode

Llama a la función getNews para iniciar la solicitud, si todo va bien, la lista de noticias se entregará en la devolución de llamada onSuccess. Si ocurre un error, se activará la devolución de llamada onFailure.

private fun getNews() {
    val country=Locale.getDefault().country
    val url= "$URL&country=$country"
    Log.e("URL",url)
    val downloader=NewsClient(this)
    downloader.apply {
        listener=this@MainActivity
        getNews(url,METHOD)
    }
}
override fun onSuccess(news: ArrayList<Article>) {
    this.news.apply {
        clear()
        addAll(news)
    }
    runOnUiThread{
        swipeRefreshLayout.isRefreshing=false
        loadingDialog?.dismiss()
        adapter?.notifyDataSetChanged()
    }
}
override fun onFailure(error: String) {
    val errorDialog=AlertDialog.Builder(this).apply {
        setTitle(R.string.error_title)
        val message="${getString(R.string.error_message)} \n $error"
        setMessage(message)
        setPositiveButton(R.string.ok){ dialog, _ ->
            dialog.dismiss()
        }
        setCancelable(false)
        create()
        show()
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusión

Es solo cuestión de tiempo para que el protocolo QUIC se convierta en el nuevo estándar de conexiones a Internet. Por ahora, puedes preparar tus aplicaciones para admitirlo y brindar la mejor experiencia de usuario.
Prueba el demo: https://github.com/danms07/HQUICNews

Alt Text

Les compartimos el link al articulo original
https://forums.developer.huawei.com/forumPortal/en/topicview?tid=0202352465220930189&fid=0101187876626530001

Top comments (0)