DEV Community

Cover image for Android-Kotlin,Get data from restful API having multiple JSON objects.
Benard Ngoda
Benard Ngoda

Posted on

Android-Kotlin,Get data from restful API having multiple JSON objects.

Hello There, i am delighted that you landed onto my article.
In this Article i will teaching you how to fetch data from restful API having multiple data objects.
In this scope, we will be using Android Kotlin to achieve this. If you have never been to Android Before please don't worry i will explain it in a way you will understand better.
In this tutorial we will create a Covid19 Logging app that will be displaying a list of counties affected with the pandemic.
This is what we will have at the end:
Alt Text

Part 1

This is the first part of the series we will go into,so stay tuned for even more exiting updates on this project.
Here are some of the things you will need to get things ready :

  1. Android Studio
  2. Familiarity with Kotlin.
  3. Retrofit Basic concepts.
  4. Json Basic Knowledge.
  5. https://disease.sh/v2/countries

So lets get into it.

Create Android Studio Project

Open Android Studio -> Create a new Project ->Give it any name
->Tick Kotlin as the Language.-> Select Empty Activity.

Once your project is synced correctly.

We will start by adding a number of dependancies we will need for our project.
Head to gradle scripts (Module:app)
add the following

//for material themes
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'com.squareup.picasso:picasso:2.71828'
    //retrofit requisites retro+gson
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'

We will the head to our activity_main.xml and add a recycler view to it.This will be used to display a list of countries.
It should be like this:

<?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=".activities.MainActivity">

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

You should have something similar to this.
Alt Text

Since we will be using material components (To make our app look better), you will have to change your theme to inherit from material components. So edit your existing styles.xml (in styles/styles.xl)
to the following:

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar.Bridge">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

Notice that the parent theme is now
Theme.MaterialComponents.Light.DarkActionBar.Bridge
You may chose any, but should have Material components.

Next we need to allow our app access Resources from the Internet, so we will add permisions to Access Internet. Open your manifest.xml file and add the following at the top of application tags.

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

Once all is set,
we are now ready to start implementing functionality.

Our scope in this tutorial will be to fetch data from an API which has several data objects,
By This i mean a json response such as this:

{
"updated":1592127440420,
"country":"Afghanistan",
"countryInfo"
{
"_id":4,
"iso2":"AF",
"iso3":"AFG",
"lat":33,"long":65,
"flag":"https://disease.sh/assets/img/flags/af.png"
},
"cases":24766,
"todayCases":664,
"deaths":471,
"todayDeaths":20,
"recovered":4725,
"todayRecovered":524,
"active":19570,
"critical":19,
}

The above response has two json objects which are :

  1. Country Info
  2. Country Finer Details

If you have worked with HTTP requests and responses, you will attest how this is usually a hectic step to map your data.
We will be using Retrofit which was earlier on imported.

Java package.

Since we are done with any other things to do with XML (Layouts,styles,manifests) we now head to our java package folder.
By default your project now has MainActivity.kt, nothing to worry about.

We will create four packages, they will be responsible for making a better architecture for our app. This will aid to make code more reusable and easier to understand.
So lets do it.

On your package, which contains MainActivity.kt,
->Rightclick ->New->Package ...
The package names will be

  1. activities(Paste in MainActivity.kt)
  2. models
  3. services
  4. helpers

It should look similar to this.
Alt Text

First things first:

In order to receive the data into our app, we need to define what kind of data we are going to receive.This is usually the hardest task to accomplish, for simple data it may look simple, but for a whole production app, it will give you a hard time.
This definition will go into a data class in Kotlin.

I will show you the easiest way to structure your data class using the data from our api.
On your android studio :Head to :

  1. File
  2. Settings.
  3. Plugins.
  4. Search for (Json To Kotlin -Seal).
  5. Install it

It will look like this
Alt Text

This plugin will help us to create sealed classes from any response.
(Restart IDE) if requested

In your models package;
Right click on models -> new ->(You will see 'Kotlin data class from json') click it.

Paste in the response we earlier on had in this tutorial.
Give the class a name; for this case call it MyCountry
Click on Advanced -> You will see Four tabs: Select Other, then tick
Enable Inner class Model.

By this you're telling the plugin to create all the json data objects into one single class that we can call to retrieve data at once. (Click okey and apply)
A new class will be created with all the properties as shown:

package dev.bensalcie.retrofitest.models


import com.google.gson.annotations.SerializedName

data class MyCountry(
    val active: Int,
    val activePerOneMillion: Double,
    val cases: Int,
    val casesPerOneMillion: Double,
    val continent: String,
    val country: String,
    val countryInfo: CountryInfo,
    val critical: Int,
    val criticalPerOneMillion: Double,
    val deaths: Int,
    val deathsPerOneMillion: Double,
    val oneCasePerPeople: Int,
    val oneDeathPerPeople: Int,
    val oneTestPerPeople: Int,
    val population: Int,
    val recovered: Int,
    val recoveredPerOneMillion: Double,
    val tests: Int,
    val testsPerOneMillion: Double,
    val todayCases: Int,
    val todayDeaths: Int,
    val todayRecovered: Int,
    val updated: Long
) {
    data class CountryInfo(
        val flag: String,
        @SerializedName("_id")
        val id: Int
    )
}

This will be enough for a Model to be used in Retrofit.
In ther service package, create a new interface class an name it
CountryService.kt, this interface clas will contain our function to get all countries into a list using a Call function from retrofit.
This is how it will look like:

package dev.bensalcie.retrofitest.services

import dev.bensalcie.retrofitest.models.MyCountry
import retrofit2.Call
import retrofit2.http.GET

interface CountryService {

    @GET("countries")
    fun getAffectedCountryList () : Call<List<MyCountry>>
}

We will create an object class which will help us to build our services with retrofit and the interface, name it ServiceBuilder.kt
and it will look like this:

package dev.bensalcie.retrofitest.services

import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ServiceBuilder {
    private const val URL ="https://disease.sh/v2/"
    //CREATE HTTP CLIENT
    private val okHttp =OkHttpClient.Builder()

    //retrofit builder
    private val builder =Retrofit.Builder().baseUrl(URL)
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttp.build())

    //create retrofit Instance
    private val retrofit = builder.build()

    //we will use this class to create an anonymous inner class function that
    //implements Country service Interface


    fun <T> buildService (serviceType :Class<T>):T{
        return retrofit.create(serviceType)
    }

}

What it does basically is to instantiate retrofit with OkHttp client which will be responsible to get our interface working.
In here we have provided the 'base url' on which we will append to our interface to get our data.
Notice the function 'buidService', this will help us to create an anonymous inner class function that invokes our interface class.

In our helper package, we are going to create an Adapter, which will be responsible in adding our country items to our Recycler view.

package dev.bensalcie.retrofitest.helpers

import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso
import dev.bensalcie.retrofitest.R
import dev.bensalcie.retrofitest.models.MyCountry

class CountriesAdapter(private val countriesList: List<MyCountry>) :RecyclerView.Adapter<CountriesAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {

        val view  = LayoutInflater.from(parent.context).inflate(R.layout.country_item,parent,false)
        return ViewHolder(view)
    }


    override fun getItemCount(): Int {

        return countriesList.size
    }


    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        Log.d("Response", "List Count :${countriesList.size} ")


        return holder.bind(countriesList[position])

    }
    class ViewHolder(itemView : View) :RecyclerView.ViewHolder(itemView) {


        var imageView = itemView.findViewById<ImageView>(R.id.ivFlag)
        var tvTitle = itemView.findViewById<TextView>(R.id.tvTitle)
        var tvCases = itemView.findViewById<TextView>(R.id.tvCases)
        fun bind(country: MyCountry) {

            val name ="Cases :${country.cases.toString()}"
       tvTitle.text = country.country
        tvCases.text = name
        Picasso.get().load(country.countryInfo.flag).into(imageView)
        }

    }
}

Notice this function

 fun bind(country: MyCountry) {

            val name ="Cases :${country.cases.toString()}"
       tvTitle.text = country.country
        tvCases.text = name
        Picasso.get().load(country.countryInfo.flag).into(imageView)
        }

This function binds the data to the respective elements and also loads the image using picasso.
Earlier on we saw that, in order to get the country flag, we needed to get back to the 'CountryInfo' object for us to extract the flag,and also if we needed to get the number of cases, we needed to get into the 'CountryDetails' object, but with the Model class we created, we are able to access both objects at ago.

In this case, we access the flag attribute through :

country.countryInfo.flag

as in the case of getting the flag image.

Picasso.get().load(country.countryInfo.flag).into(imageView)

Finally.

In order to get our data displaying onto our recylcer view, we need a way to call our service , to invoke our interface.

In your MainActivity, we will add a function 'loadCountries()',
this will be responsible for fetching our data asynchronously using enqueue in retrofit and the help of our service binder.

This is how it will look.


private fun loadCountries() {
        //initiate the service
        val destinationService  = ServiceBuilder.buildService(CountryService::class.java)
        val requestCall =destinationService.getAffectedCountryList()
        //make network call asynchronously
        requestCall.enqueue(object : Callback<List<MyCountry>>{
            override fun onResponse(call: Call<List<MyCountry>>, response: Response<List<MyCountry>>) {
                Log.d("Response", "onResponse: ${response.body()}")
               if (response.isSuccessful){
                   val countryList  = response.body()!!
                   Log.d("Response", "countrylist size : ${countryList.size}")
                   country_recycler.apply {
                       setHasFixedSize(true)
                       layoutManager = GridLayoutManager(this@MainActivity,2)
                       adapter = CountriesAdapter(response.body()!!)
                   }
               }else{
                   Toast.makeText(this@MainActivity, "Something went wrong ${response.message()}", Toast.LENGTH_SHORT).show()
               }
            }
            override fun onFailure(call: Call<List<MyCountry>>, t: Throwable) {
                Toast.makeText(this@MainActivity, "Something went wrong $t", Toast.LENGTH_SHORT).show()
            }
        })
    }

Notice in here, we have created an instance of the binding service,used it to access our interface function of getting countrylist,then we enqueue to get the 'onResponse' listener and an 'onError' listener.

We then check if the response was success. If it is, we add the response body inform of a list to our adapter.

Our recycler view will contain a grid of two collumns, so we will use a gridManager.
The we assign our adapter to our recycler view.

You may now call this function whenever you desire within MainActivity,kt . For me, i called it on create, and this is how the final code looks like.


package dev.bensalcie.retrofitest.activities

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import dev.bensalcie.retrofitest.R
import dev.bensalcie.retrofitest.helpers.CountriesAdapter
import dev.bensalcie.retrofitest.models.MyCountry
import dev.bensalcie.retrofitest.services.CountryService
import dev.bensalcie.retrofitest.services.ServiceBuilder
import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        loadCountries()
    }


    private fun loadCountries() {
        //initiate the service
        val destinationService  = ServiceBuilder.buildService(CountryService::class.java)
        val requestCall =destinationService.getAffectedCountryList()
        //make network call asynchronously
        requestCall.enqueue(object : Callback<List<MyCountry>>{
            override fun onResponse(call: Call<List<MyCountry>>, response: Response<List<MyCountry>>) {
                Log.d("Response", "onResponse: ${response.body()}")
               if (response.isSuccessful){
                   val countryList  = response.body()!!
                   Log.d("Response", "countrylist size : ${countryList.size}")
                   country_recycler.apply {
                       setHasFixedSize(true)
                       layoutManager = GridLayoutManager(this@MainActivity,2)
                       adapter = CountriesAdapter(response.body()!!)
                   }
               }else{
                   Toast.makeText(this@MainActivity, "Something went wrong ${response.message()}", Toast.LENGTH_SHORT).show()
               }
            }
            override fun onFailure(call: Call<List<MyCountry>>, t: Throwable) {
                Toast.makeText(this@MainActivity, "Something went wrong $t", Toast.LENGTH_SHORT).show()
            }
        })
    }
}

You should now have one like this:
Alt Text

That is it for this tutorial. If you managed to reach this far!!!
Congratulations.
Alt Text

Please share, comment and suggets more Interesting stuffs.
Dont Miss out on part two,coming very soon.

Top comments (8)

Collapse
 
chinghsuanwei profile image
chinghsuanwei • Edited

I think the author just forget to past code of country_item.xml, so I just want to make the project completed by adding the missing file.

Here is the my github link: github.com/chinghsuanwei/CovidLog

Feel free to tell me to delete repo, if you think it's not OK.

Collapse
 
raymond6289 profile image
Raymond6289

Thank you very much for this guidance. I have a query. I tried to use a variable in the @GET expression but get the following compiler error:

“Only ‘const val’ can be used in constant expressions.”

Can you advise me how to get around this problem please? Thank you.

Collapse
 
ndiritumichael profile image
Michael Ndiritu

Nice article but I have found some issues with the JSON contents you posted.
also could you please post the code on Github for references.

Collapse
 
bensalcie profile image
Benard Ngoda

Hello,sorry for this...i will post the code on github and clarify as well

Collapse
 
pkbiswal321 profile image
pkbiswal321

where this code in github?

Collapse
 
michelp3 profile image
michelp3

Hello android studio 4 : convert Json to kotlin
data class CountryInfo(
val _id: Int,
val flag: String,
val iso2: String,
val iso3: String,
val lat: Int,
val long: Int
)
error nothing to screen
Explication ???
My english is bad
Best regards

Collapse
 
michelp3 profile image
michelp3

Explication
MyCountry
val criticalPerOneMillion: Double,

CountryInfo
val lat: Double
val long: Double

With Modifications : OK but android studio 4 : convert Json to kotlin is not correct ??

Collapse
 
alos1895 profile image
alos1895

Hi, I think just forget to put the code for country_item.xml. But Works excellent