[Huawei Developers] Sherlock F:
QUIC is a new transport protocol based on UDP wich allows improve the speed of the network connecctions due to the low latency of UDP. QUIC means Quick UDP Internet Connections and is planned to become a standar.

The key features of QUIC are:

Dramatically reduced connection establishment time

Improved congestion control

Multiplexing without head of line blocking

Connection migration

Some companies are starting supporting QUIC on their servers, now from the client side, is time to be prepared and adding QUIC support to our apps. This time we will buid a News App with QUIC support using the HQUIC kit.

HQUIC allow us connect with web services over the QUIC protocol, if the remote does not supports QUIC, the kit will use HTTP 2 instead automatically.

Previous Requirements

An android studio project

A developer account on newsapi.org

Adding the required dependencies

This example will require the next dependencies:

RecyclerView: To show all the news in a list.

SwipeRefreshLayout: To allw the user refreshing the screen using a swipe gesture.=

HQUIC: The kit which allow us connect with services over QUIC

To use HQUIC you must add the Huawei's repository to your top level 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
}

Now add the required dependencies to your app level 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'

Designing the UI

We will show a list of items inside a RecyclerView, so you must design the item layout which will be displayed for each article. This basic view will only display the title, a brief introduction and the date of publication:

<?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>

For the Activity Layout, the design will contain a SwipeRefresLayout with the RecyclerView inside

<?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>

To show the articles on a recycler view you will need an adapter, the adapter will report the click event of any item to the given listener.

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

HQUIC Service

We will use the HQUICService class provided by the HQUIC demo project. This class abstracts the HQUIC kit capabilities and allows us to perform a request easily.

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
}

}

Downloading the news

We will define a helper class to handle the request and parses the response into an ArrayList. The HQUIC kit will read certain number of bytes per time, for big responses the onReadCompleted method will be called more than once.

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

Making the request

Define tue request properties

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

Call the getNews function to start the request, if everything goes well, the list of news will be delivered on the onSuccess callback. If an error ocurs, the onFailure callback will be triggered.

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

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/002/383/434/2640052000002383434.20200914043942.33797190367874703724908393735003:50510929012352:2800:688F8BF5B9AAA5404A644A46F1A1059B18F8794355C487E8F219E93BFDFAE23E.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/002/383/434/2640052000002383434.20200914043942.33797190367874703724908393735003:50510929012352:2800:688F8BF5B9AAA5404A644A46F1A1059B18F8794355C487E8F219E93BFDFAE23E.png[/IMG]

Conclusion

Is just a question of time for the QUIC protocol to become in the new standard of internet connections. For now you can get your apps ready to suppot it and provide the best user experience.

Test this demo: https://github.com/danms07/HQUICNews

Reference

Official Document

HQUIC sample code