[Huawei Developers] ask011:
Most android applications download it's content from the cloud (commonly a REST API) getting ready to parse and display that information with lists and menus in order to display dynamic content or provide a personalized experience. There are some third party libraries designed to consume a REST API (like Retrofit) or to download media content (like Glide and Picasso). This time, let me introduce you the new Huawei Network Kit.

What is nework kit?

Network kit is the new Huawei's System SDK designed to simplify the communications with web services by providing 2 main connection modes:

Rest Client

HTTP Client

Network kit supports QUIC connections automatically, that means if the Web service supports QUIC or migrates to QUIC, your app will keep working without require any change. In addition, this kit is pretty similar to the well known Retrofit, so, if you have previous experience with Retrofit, you will be able to integrate Network Kit withount complications.

Previously, we made a News client by using the HQUIC kit. In this article we are going to develop a news client application by using the new Huawei Network Kit.

Previous requirements

A developer account in newsapi.org

Android Studio 4.0 or later and the kotlin plugin

Setting up the project

Network kit doesn't require to setup a project in AGC, but you still need to add the Huawei Maven repositories to your project-level build.gradle:

buildscript {
ext.kotlin_version = "1.4.31"
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
...
maven {url 'https://developer.huawei.com/repo/'}
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

Go to the official documents and look for the Network kit latest version under version change history. Once you have found the latest version available, add it to yout app-level build.gradle as follows

implementation 'com.huawei.hms:network-embedded:5.0.1.301'

We will use Moshi to parse the response from the web service, let's add the related dependencies and the kapt plugin to proccess the annotations.

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}

android{
...
}

dependencies {

...

implementation 'com.huawei.hms:network-embedded:5.0.1.301'

implementation 'com.squareup.moshi:moshi:1.11.0'
implementation "com.squareup.moshi:moshi-kotlin:1.11.0"
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.11.0'

...
}

To display the news in a list, we must add RecyclerView and CardView to our project and must enable the DataBinding library to make our job easier.

android {
...
//Enabling DataBinding and ViewBinding
buildFeatures{
viewBinding true
dataBinding true
}

...
}

dependencies {

...
//MVVM dependencies
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
//DataBinding dependency
kapt "com.android.databinding:compiler:3.1.4"
//Layout dependencies
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
...
}

We are ready to start the project.

Building the request

First of all, Network kit must be initialized, let's create an Application class to do this job

NetworkApplication.kt

class NetworkApplication: Application() {
companion object {
const val TAG="Network Application"
}
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
private fun initNetworkKit() {
// Initialize the object only once, upon the first call.
NetworkKit.init(this ,object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
if (result) {
Log.i(TAG, "Networkkit init success")
} else {
Log.i(TAG, "Networkkit init failed")
}
}
})
}
}

To make sure this code will be excecuted upon each startup, we must specify this class inside the application element in our AndroidManifest.xml. Let's add the required permissions too.

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name=".NetworkApplication"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NetworkKitDemo"
android:usesCleartextTraffic="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

Now, we must create the data models wich Moshi will use to parse the response

NewsResponse.kt

@JsonClass(generateAdapter = true)
data class NewsResponse(
@Json(name = "status") val status: String?,
@Json(name = "totalResults") val totalResults: Int?,
@Json(name = "articles") val articles: List<Article>
)

@JsonClass(generateAdapter = true)
data class Article(
@Json(name = "source") val source: Source?,
@Json(name = "author") val author: String?,
@Json(name = "title") val title: String?,
@Json(name = "description") val description: String?,
@Json(name = "url") val url: String?,
@Json(name = "urlToImage") val urlToImage: String?,
@Json(name = "publishedAt") val publishedAt: String?,
@Json(name = "content") val content: String?
)

@JsonClass(generateAdapter = true)
data class Source(
@Json(name = "id") val id: String?,
@Json(name = "name") val name: String?
)

Network kit porvides 2 operation modes, we will use the REST Client to get the Top headlines in the user's country and the HTTP Client mode to download the picture of each Article. We will create a singleton class called NetworkKitHelper.

Let's take a look to the REST Client mode:

NetworkKitHelper.kt

object NetworkKitHelper {
const val TAG: String = "HTTPClient"

//Your API key from newsapi.org
val apiKey = Keys.readApiKey()

fun createNewsClient(): NewsService {
val restClient = RestClient.Builder()
.httpClient(HttpClient.Builder().build())
.baseUrl("https://newsapi.org/v2/")//Specify the API base URL, this is useful if you will consume multiple paths of the same API
.build()
return restClient.create(NewsService::class.java)
}

//Declare a Request API
interface NewsService {
//Use the GET annotation to specify the path
@GET("top-headlines/")
fun getTopHeadlines(/* use the Query annotation to specify a query parameter in the request*/
@Query("apiKey") apiKey: String? = "",
@Query("country") country: String
): Submit<String?>?
}

fun loadTopHeadlines(sampleService: NewsService, listener: NewsClientListener?,country:String=Locale.getDefault().country) {
sampleService.getTopHeadlines(apiKey,country)?.enqueue(object : Callback<String?>() {
@Throws(IOException::class)
override fun onResponse(submit: Submit<String?>?, response: Response<String?>) {
// Obtain the response. This method will be called if the request is successful.
val body = response.body
body?.let {
try {
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter(NewsResponse::class.java)
val news = adapter.fromJson(it)
news?.let { myNews ->
listener?.onNewsDownloaded(myNews.articles)
}
} catch (e: JSONException) {
Log.e("excepion", e.toString())
}
}
}

override fun onFailure(submit: Submit<String?>?, exception: Throwable?) {
// Obtain the response. This method will be called if the request fails.
Log.e("LoadTopHeadlines", "response onFailure = " + exception?.message)
}
})
}

interface NewsClientListener {
fun onNewsDownloaded(news: List<Article>)
}
}

Put special attention to the loadTopHeadlines function. As you can see, there aren't coroutines or threads defined, we are using the enqueue API instead. By this way Network Kit will handle the request in asynchronous mode for us.

If the API call is successfull, we will use Moshi to parse the response into data objects. By other way, we will be notified about the error in the onFailure callback. Once the response has been parsed, NetworkKitHelper will repor the news to the specified NewsClientListener.

Let's add the code to download the preview pics:

NetworkKitHelper.kt (Adding)

object NetworkKitHelper {

private val httpClient: HttpClient = createClient()

private fun createClient(): HttpClient {
return HttpClient.Builder()
.callTimeout(1000)
.connectTimeout(10000)
.build()
}

fun createRequest(url: String): Request {
return httpClient.newRequest()
.url(url)
.method("GET")
.build()
}

fun httpClientEnqueue(request: Request, listener: HttpClientListener? = null) {
httpClient.newSubmit(request).enqueue(object : Callback<ResponseBody?>() {
@Throws(IOException::class)
override fun onResponse(
submit: Submit<ResponseBody?>?,
response: Response<ResponseBody?>
) {
// Process the response if the request is successful.
Log.i(TAG, "response code:" + response.code)
response.body?.let {
listener?.onSuccess(it.bytes())
}

}

override fun onFailure(submit: Submit<ResponseBody?>?, throwable: Throwable?) {
// Process the exception if the request fails.
Log.w(TAG, "response onFailure = ${throwable?.message}")
}
})
}

interface HttpClientListener {
fun onSuccess(body: ByteArray)
}
}

As well as with the REST Client mode, we are able to enqueue HTTP Requests and define a callback for each one. In this case, we are receiving a byte array which will be used to create and display a bitmap.

Here we will face a complication. If we try to store the bitmap in the same data class as the Article, Moshi will cause a reflection error at compilation time. To solve this, we will define a new class to store the article and be responsible to load the bitmap, by doing so, we will be able to load the news as soon as we get them and then using the observer pattern, the bitmap will be added to the view as soon as it's ready.

ArticleModel.kt

class ArticleModel(val article: Article) : NetworkKitHelper.HttpClientListener {
private val _bitmap= MutableLiveData<Bitmap?>().apply{postValue(null)}
val bitmap: LiveData<Bitmap?> =_bitmap

init {
loadBitmap()
}

fun loadBitmap() {
article.urlToImage?.let{
val request=NetworkKitHelper.createRequest(it)
NetworkKitHelper.httpClientEnqueue(request, this)
}
}

override fun onSuccess(body: ByteArray) {
val bitmap= BitmapFactory.decodeByteArray(body, 0, body.size)
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, 1000, 600, true)
_bitmap.postValue(resizedBitmap)
}
}

As soon as any instance of ArticleModel is created, it will enqueue an HTTP request async for the preview pic. If the call is successfull, we will receive a ByteArray in the onSuccess callback to create our bitmap from it and let the observer know the bitmap is ready to be displayed.

Sending the request

Let's create a ViewModel which will be responsible to invoke the API and store the data. Here we will use the observer pattern to let the observer know the Articles are ready to be displayed.

MainViewModel.kt

class MainViewModel : ViewModel(), NetworkKitHelper.NewsClientListener {
private val _articles = MutableLiveData<ArrayList<ArticleModel>>().apply { value = ArrayList() }
val articles: LiveData<ArrayList<ArticleModel>> = _articles

fun loadTopHeadlines(){
articles.value?.let{
if(it.isEmpty()) getTopHeadlines()
else return
}
}

private fun getTopHeadlines() {
NetworkKitHelper.loadTopHeadlines(NetworkKitHelper.createNewsClient(),this)
}

override fun onNewsDownloaded(news: List<Article>) {
val list=ArrayList<ArticleModel>()
for (article: Article in news) {
list.add(ArticleModel(article))
}
_articles.postValue(list)
}

}

To avoid downloading the news again when the user rotates the screen, we are defining the loadTopHeadlines function. It will only make the request if the list of articles is empty.

Displaying the Articles

We will use DataBinding to quicly display our news in a RecyclerView on the MainActivity, let's take a look to the main layout

activity_main.xml

<?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"
xmlns:tools="http://schemas.android.com/tools"
>

<data class="MainBinding"/>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_height="match_parent"
android:layout_width="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>

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

Now we must define the card wich will be rendered for each article

article_card.xml

<?xml version="1.0" encoding="utf-8"?>

<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="ArticleBinding">
<variable
name="item"
type="com.hms.demo.networkkitdemo.ArticleModel" />
</data>

<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="5dp"
android:layout_marginVertical="5dp"
card_view:cardCornerRadius="15dp"
card_view:cardElevation="20dp"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">

<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/pic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:contentDescription="@string/desc"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="1.0"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/articleTitle"
card_view:shapeAppearanceOverlay="@style/roundedImageView"
tools:srcCompat="@tools:sample/avatars" />

<TextView
android:id="@+id/articleTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.article.title}"
android:textAlignment="viewStart"
android:textSize="24sp"
android:textStyle="bold"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintHorizontal_bias="0.498"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@{item.article.description}"
android:textSize="20sp"
card_view:layout_constraintEnd_toEndOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/pic" />

<TextView
android:id="@+id/source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@{item.article.source.name}"
card_view:layout_constraintBottom_toBottomOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
card_view:layout_constraintTop_toBottomOf="@+id/desc"
card_view:layout_constraintVertical_bias="0.09" />

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

The ArticleBinding class will be responsible to fill the view with the values in it's ArticleModel instance for us. That's the magic of DataBinding.

As you may know, to display elements in a RecyclerView we need an Adapter, so let's define it

NewsAdapter.kt

class NewsAdapter: RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
var articles:List<ArticleModel> =ArrayList()

class NewsViewHolder(private val binding:ArticleBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(item:ArticleModel){
binding.item=item
item.bitmap.observe(binding.root.context as LifecycleOwner){
it?.let{
binding.pic.setImageBitmap(it)
}
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
val inflater=LayoutInflater.from(parent.context)
val binding=ArticleBinding.inflate(inflater,parent,false)
return NewsViewHolder(binding)
}

override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {

holder.bind(articles[position])

}

override fun getItemCount(): Int {
return articles.size
}
}

Put special attention to the bind function of the NewsViewHolder class, from here we are telling to the ArticleBinding instance what is the information we want to display in the view. Also, we are using the observer pattern to update the ImageView once the article's preview pic has been downloaded.

Finally, is time to join everything through the MainActivity

MainActivity.kt

class MainActivity : AppCompatActivity() {
companion object {
const val TAG="Main"
}
private lateinit var binding:MainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= MainBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
setContentView(binding.root)
val viewModel:MainViewModel=ViewModelProvider(this).get(MainViewModel::class.java)
val adapter=NewsAdapter()

viewModel.articles.observe(this){

adapter.articles=it
adapter.notifyDataSetChanged()
}
binding.recycler.adapter=adapter
viewModel.loadTopHeadlines()
}
}

Final result

Tips & Tricks

If your app will consume a REST API with Kotlin, is better to use Moshi instead of gson because Moshi can understand the kotlin's not-nullable types.

If you will use API keys to authenticate your client with the server, is better to use the NDK to hide your KEY and prevent it from being obtained by using reverse engineering. Let's use the Rahul Sharma's hidding method. (Make sure to download the Android NDK from the SDK Manager)

1. Swithch to the Project view and create a jni directory under the main directory.

2. Under the jni directory add the next 3 files:

Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := keys
LOCAL_SRC_FILES := keys.c

include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_ABI := all

Keys.c (Put here your API key)

#include <jni.h>

JNIEXPORT jstring JNICALL
Java_com_hms_demo_networkkitdemo_Keys_getApiKey(JNIEnv *env, jclass instance) {
return (*env)->NewStringUTF(env, "PUT_HERE_YOUR_API_KEY");
}

3. Switch back to the Android View and create a Keys kotlin object

Keys.kt

object Keys {
init {
System.loadLibrary("keys")
}
private external fun getApiKey(): String?

public fun readApiKey(): String? { //use this method for String
return getApiKey()
}
}

4. Tell gradle you will use NDK by adding the next code inside android

build.gradle (app-level)

plugins {
...
}

android {
...

externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}

dependencies {

...
}

Finally, modify the NetworkKitHelper object to read the API key from the native library.

NetworkKitHelper.kt (Modifying)

object NetworkKitHelper {

val apiKey = Keys.readApiKey()

}

Conclusion

By using Network kit your app will be ready to perform requests over QUIC or HTTP/2 without writting extra code. The REST Client mode and it's annotations are helpful to to quickly consume a REST API without taking care about Threads or Coroutines. And finally, the HTTP Client mode is useful to download preview images or any other stuff which is not a JSON.

References

Read In Forum

Network Kit official Docs

Hiding Secret/Api key from reverse engineering in Android using NDK

Moshi