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:
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 :
- Android Studio
- Familiarity with Kotlin.
- Retrofit Basic concepts.
- Json Basic Knowledge.
- 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.
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 :
- Country Info
- 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
- activities(Paste in MainActivity.kt)
- models
- services
- helpers
It should look similar to this.
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 :
- File
- Settings.
- Plugins.
- Search for (Json To Kotlin -Seal).
- Install it
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:
That is it for this tutorial. If you managed to reach this far!!!
Congratulations.
Please share, comment and suggets more Interesting stuffs.
Dont Miss out on part two,coming very soon.
Top comments (8)
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.
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.
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.
Hello,sorry for this...i will post the code on github and clarify as well
where this code in github?
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
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 ??
Hi, I think just forget to put the code for country_item.xml. But Works excellent