[Huawei Developers] muraliameakula:
Introduction

Hi everyone, In this article, we’ll take a look at the Huawei Network Kit and how to use it with Rest APIs. Then, we will develop a demo app using Kotlin in the Android Studio. Finally, we’ll talk about the most common types of errors when making network operations on Android and how you can avoid them.

Huawei Network Kit

Network Kit is a service suite that allows us to perform our network operations quickly and safely. It provides a powerful interacting with Rest APIs and sending synchronous and asynchronous network requests with annotated parameters. Also, it allows us to quickly and easily upload or download files with additional features such as multitasking, multithreading, resumable uploads, and downloads. Lastly, we can use it with other Huawei kits such as hQUIC Kit and Wireless Kit to get faster network traffic.

Our Sample Project

In this application, we'll get a user list from a Rest Service and show the user information on the list. When we are developing the app, we'll use these libraries:

RecyclerView

DiffUtil

Kotlinx Serialization

ViewBinding

To make it simple, we don't use an application architecture like MVVM and a progress bar to show the loading status of the data.

The file structure of our sample app:

Website for Rest API

JsonPlaceHolder is a free online Rest API that we can use whenever we need some fake data. We’ll use the fake user data from the below link. And, it gives us the user list as Json, click Here.

Why we are going to use Kotlin Serialization instead of Gson ?

Firstly, we need a serialization library to convert JSON data to objects in our app. Gson is a very popular library for serializing and deserializing Java objects and JSON. But, we are using the Kotlin language and Gson is not suitable for Kotlin. Because Gson doesn’t respect non-null types in Kotlin.

If we try to parse such as a string with GSON, we’ll find out that it doesn’t know anything about Kotlin default values, so we’ll get the NullPointerExceptions as an error. Instead of Kotlinx Serialization, you can also use serialization libraries that offer Kotlin-support, like Jackson or Moshi. We will go into more detail on the implementation of the Kotlinx Serialization.

Setup the Project

We are not going to go into the details of integrating Huawei HMS Core into a project. You can follow the instructions to integrate HMS Core into your project via official docs or codelab. After integrating HMS Core, let’s add the necessary dependencies.

Add the necessary dependencies to build.gradle (app level).

plugins {
id 'com.huawei.agconnect' // HUAWEI agconnect Gradle plugin'
id 'org.jetbrains.kotlin.plugin.serialization' // Kotlinx Serialization
}

android {
buildFeatures {
// Enable ViewBinding
viewBinding true
}
}

dependencies {
// HMS Network Kit
implementation 'com.huawei.hms:network-embedded:5.0.1.301'
// Kotlinx Serialization
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1'
}

We’ll use viewBinding instead of findViewById. It generates a binding class for each XML layout file present in that module. With the instance of a binding class, we can access the view hierarchy with type and null safety.

We used the kotlinx-servialization-json:1.01 version instead of the latest version 1.1.0 in our project. If you use version 1.1.0 and your Kotlin version is smaller than 1.4.30-M1, you will get an error like this:

Your current Kotlin version is 1.4.10, while kotlinx.serialization core runtime 1.1.0 requires at least Kotlin 1.4.30-M1.

Therefore, if you want to use the latest version of Kotlinx Serialization, please make sure that your Kotlin version is higher than 1.4.30-M1.

Add the necessary dependencies to build.gradle (project level)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
dependencies {
classpath 'com.huawei.agconnect:agcp:1.4.1.300' // HUAWEI Agcp plugin
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" // Kotlinx Serialization
}
}

Declaring Required Network Permissions

To use functions of Network Kit, we need to declare required permissions in the AndroidManifest.xml file.

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

Initialize the Network Kit

Let’s create an Application class and initialize the Network Kit here.

class App : Application() {

private val TAG = "Application"

override fun onCreate() {
super.onCreate()

initNetworkKit()
}

private fun initNetworkKit() {
NetworkKit.init(applicationContext, object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
if (result) {
Log.i(TAG, "NetworkKit init success")
} else {
Log.i(TAG, "NetworkKit init failed")
}
}
})
}
}

Note: Don’t forget to add the App class to the Android Manifest file.

<manifest ...>
...
<application
android:name=".App"
...
</application>
</manifest>

ApiClient

getApiClient(): It returns the RestClient instance as a Singleton. We can set the connection time out value here. Also, we specified the base URL.

const val BASE_URL = "https://jsonplaceholder.typicode.com/"

class ApiClient {

companion object {
private var restClient: RestClient? = null

fun getApiClient(): RestClient {

val httpClient = HttpClient.Builder()
.callTimeout(1000)
.connectTimeout(10000)
.build()

if (restClient == null) {
restClient = RestClient.Builder()
.baseUrl(BASE_URL)
.httpClient(httpClient)
.build()
}
return restClient!!
}
}

}

ApiInterface

We specified the request type as GET and pass the relative URL as “users”. And, it returns us the results as String.

interface ApiInterface {

@GET("users")
fun fetchUsers(): Submit<String>
}

User  - Model Class

As I mentioned earlier, we get the data as a string. Then, we’ll convert data to User object help of the Kotlinx Serialization library. To perform this process, we have to add some annotations to our data class.

@serializable -> We can make a class serializable by annotating it.

@SerialName() -> The variable name in our data must be the same as we use in the data class. If we want to set different variable names, we should use @SerialName annotation.

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class User(
@SerialName("id")
val Id: Int = 0,
val name: String = "",
val username: String = "",
val email: String = "",
)

UserDiffUtil

To tell the RecyclerView that an item in the list has changed, we’ll use the DiffUtil instead of the notifyDataSetChanged().

DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one. And, it uses The Myers Difference Algorithm to do this calculation.

What makes notifyDataSetChanged() inefficient is that it forces to recreate all visible views as opposed to just the items that have changed. So, it is an expensive operation.

class UserDiffUtil(
private val oldList: List<User>,
private val newList: List<User>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}

override fun getNewListSize(): Int {
return newList.size
}

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].Id == newList[newItemPosition].Id
}

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}

}

row_user.xml

We have two TextView to show userId and the userName. We’ll use this layout in the RecylerView.

<?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="60dp">

<TextView
android:id="@+id/tv_userId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1" />

<View
android:id="@+id/divider_vertical"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_userId"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tv_userName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/divider_vertical"
app:layout_constraintTop_toTopOf="parent"
tools:text="Antonio Vivaldi" />

<View
android:id="@+id/divider_horizontal"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

UserAdapter

It contains the adapter and the ViewHolder class.

class UserAdapter : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {

private var oldUserList = emptyList<User>()

class UserViewHolder(val binding: RowUserBinding) : RecyclerView.ViewHolder(binding.root)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(
RowUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}

override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.binding.tvUserId.text = oldUserList[position].Id.toString()
holder.binding.tvUserName.text = oldUserList[position].name
}

override fun getItemCount(): Int = oldUserList.size

fun setData(newUserList: List<User>) {
val diffUtil = UserDiffUtil(oldUserList, newUserList)
val diffResults = DiffUtil.calculateDiff(diffUtil)
oldUserList = newUserList
diffResults.dispatchUpdatesTo(this)
}
}

activity_main.xml

It contains only a recyclerview to show the user list.

<?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="match_parent"
tools:context=".ui.MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity

userAdapter: We create a adapter for the RecyclerView.

apiClient: We create a request API object using the RestClient object (ApiClient).

Network Kit provides two ways to send network request: synchronous and asynchronous.

Synchronous requests block the client until the operation completes. We can only get data after it finishes its task.

An asynchronous request doesn’t block the client and we can receive a callback when the data has been received.

getUsersAsSynchronous(): We use synchronous requests here. Firstly, we get the response from RestApi. Then, we need to convert the JSON data to User objects. We use the decodeFromString function to do this. Also, we set ignoreUnknownKeys = true, because we don’t use all user information inside the JSON file. We just get the id, name, username, and email. If you don’t put all information inside your Model Class (User), you have to set this parameter as true. Otherwise, you will get an error like:

Use ‘ignoreUnknownKeys = true’ in ‘Json {}’ builder to ignore unknown keys.

We call this function inside the onCreate. But, we are in the main thread, and we cannot call this function directly from the main thread. If we try to do this, it will crash and give an error like:

Caused by: android.os.NetworkOnMainThreadException

We should change our thread. So, we call getUsersAsSynchronous() function inside the tread. Then, we get the data successfully. But, there is still one problem. We changed our thread and we cannot change any view without switching to the main thread. If we try to change a view before switching the main thread, it will give an error:

D/MainActivity: onFailure: Only the original thread that created a view hierarchy can touch its views.

So, we use the runOnUiThread function to run our code in the main thread. Finally, we send our data to the recyclerview adapter to show on the screen as a list.

getUsersAsAsynchronous() - We use asynchronous requests here. We send a network request and wait for the response without blocking the thread. When we get the response, we can show the user list on the screen. Also, we don’t need to call our asynchronous function inside a different thread. But, if we want to use any view, we should switch to the main thread. So, we use the runOnUiThread function to run our code in the main thread again.

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private val TAG = "MainActivity"

private val userAdapter by lazy { UserAdapter() }

private val apiClient by lazy {
ApiClient.getApiClient().create(ApiInterface::class.java)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)

binding.recyclerView.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = userAdapter
}

getUsersAsAsynchronous()

/*
thread(start = true) {
getUsersAsSynchronous()
}
*/
}

private fun getUsersAsSynchronous() {
val response = apiClient.fetchUsers().execute()
if (response.isSuccessful) {
val userList =
Json { ignoreUnknownKeys = true }.decodeFromString<List<User>>(response.body)
runOnUiThread {
userAdapter.setData(userList)
}
}
}

private fun getUsersAsAsynchronous() {
apiClient.fetchUsers().enqueue(object : Callback<String>() {
override fun onResponse(p0: Submit<String>?, response: Response<String>?) {
if (response?.isSuccessful == true) {
val userList = Json {
ignoreUnknownKeys = true
}.decodeFromString<List<User>>(response.body)

runOnUiThread {
userAdapter.setData(userList)
}
}
}

override fun onFailure(p0: Submit<String>?, p1: Throwable?) {
Log.d(TAG, "onFailure: ${p1?.message.toString()}")
}

})
}

}

Tips and Tricks

You can use Coroutines to manage your thread operations and perform your asynchronous operations easily.

You can use Sealed Result Class to handle the network response result based on whether it was a success or failure.

Before sending network requests, you can check that you’re connected to the internet using the ConnectivityManager.

Conclusion

In this article, we have learned how to use Network Kit in your network operations. And, we’ve developed a sample app that lists user information obtained from the REST Server. In addition to sending requests using either an HttpClient object or a RestClient object, Network Kit offers file upload and download featuring. Please do not hesitate to ask your questions as a comment.

Thank you for your time and dedication. I hope it was helpful. See you in other articles.

References

Huawei Network Kit Official Documentation

Huawei Network Kit Official Codelab

Original Source