How to Consume APIS with Retrofit 2

From WikiHTP

If we look at most of the applications we use, we can see that they have a dynamic content, that is, it changes when someone adds information, photos or whatever. This is because the vast majority of applications access the network to obtain the data that is subsequently displayed in the app. In this chapter, we will learn to understand how it works and apply it.

What is a REST API?[edit]

A REST API is a service that provides us with the necessary functions to obtain information from an external client (a database hosted in another part of the world, for example) within our own application.

For example, let's think about Facebook, an application with millions of users. It would be unfeasible to have the information of each user within the application, right? Well, to solve the problem they use API REST services. The first thing we do when entering the app is a login, this would be the first of the services because we send the server the user and password and this would return the information that we should show in the app.

We have four different types of requests.

  • Get : They are the simplest, they only return information to us. If we need to pass a parameter to the request will be through the URL. That is if, for example, we have to make a request that depends on an id (eg the identification of the user) the URL would be formed like this https://example.com/information/1, being 1 the parameter that we passed. The problem with this is that it is unsafe to pass sensitive information.
  • Post : Similar to Get but the parameters are not passed through URL, so it is safer to send information.
  • Put : It is usually used to create the entity, that is, if we think of a service such as access to a database, this would create the user for example.
  • Delete : It would be the last of the four that would allow us to delete the records in the database.

The information usually comes in two different formats, XML or JSON. To avoid getting too involved in the topic, we will only talk about the JSON, which is the most common format and with which we are going to work.

JSON format[edit]

JSON is a simple text format, it is the acronym for JavaScript Object Notation. It is one of the standards for the transfer of information between platforms, it has a very readable form that allows us to understand its contents without a problem. A simple example would be this.

{
  "employees": {
    "employee": [
      {
        "id": "1",
        "firstName": "Tom",
        "lastName": "Cruise",
        "photo": "https://jsonformatter.org/img/tom-cruise.jpg"
      },
      {
        "id": "2",
        "firstName": "Maria",
        "lastName": "Sharapova",
        "photo": "https://jsonformatter.org/img/Maria-Sharapova.jpg"
      },
      {
        "id": "3",
        "firstName": "Robert",
        "lastName": "Downey Jr.",
        "photo": "https://jsonformatter.org/img/Robert-Downey-Jr.jpg"
      }
    ]
  }
}

All JSON format starts and ends with keys and has a key-value. The employees key also contains an employee list (note that brackets instead of brackets), which stores id, firstName, lastName and photo. So we can pass a lot of information from one platform to another with standards that help us simplify the process.

If you still get complicated reading these files at the beginning, we can make use of many websites that simplify the way you see it, such as JsonEditOnline.

We start[edit]

In this chapter, we will make a small app that looks for images of dogs. We will have a search engine and a list. The user will add a dog breed and at that moment we will make a REST request that will return an array of images of that breed and we will update the list.

We will use a public API called Dog API that we can find here.

It is a GET service, to which we will pass the name of the race and we will return a JSON similar to this one.

{
  "status": "success",
  "message": [
    "https://images.dog.ceo/breeds/akita/512px-Ainu-Dog.jpg",
    "https://images.dog.ceo/breeds/akita/512px-Akita_inu.jpeg",
    "https://images.dog.ceo/breeds/akita/Akina_Inu_in_Riga_1.JPG",
    "https://images.dog.ceo/breeds/akita/Akita_Dog.jpg",
    "https://images.dog.ceo/breeds/akita/Akita_Inu_dog.jpg",
    "https://images.dog.ceo/breeds/akita/Akita_hiking_in_Shpella_e_Pëllumbasit.JPG",
    "https://images.dog.ceo/breeds/akita/Akita_inu_blanc.jpg",
    "https://images.dog.ceo/breeds/akita/An_Akita_Inu_resting.jpg",
    "https://images.dog.ceo/breeds/akita/Japaneseakita.jpg"
  ]
}

We must notice that it contains two attributes:

  • Status: This is a String that will return success or error depending on whether it finds the race or not.
  • Message: An array of String that contains all the images that we will show.

You will ask how we can make the call and retrieve the values, because for this we will use two libraries, Retrofit , which will be responsible for making the requests to network and Gson who will be responsible for parsing the response, that is, will try the JSON to convert it in an object of which we work in Kotlin.

Accessing the data[edit]

We will start by adding the libraries mentioned above. We will go to the build.gradle file of the app module to be able to do it.

ext.retrofit_version = '2.3.0'
ext.cardView_version = '27.1.0'
ext.support_version = '27.1.0'
ext.picasso_version = '2.71828'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'

    //Picasso
    implementation "com.squareup.picasso:picasso:$picasso_version"

    //Suport
    implementation "com.android.support:appcompat-v7:$support_version"
    implementation "com.android.support:recyclerview-v7:$support_version"

    //Retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

    //Anko
    implementation 'org.jetbrains.anko:anko-common:0.9'

    //Cardview
    implementation "com.android.support:cardview-v7:$cardView_version"


    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

This time, I added the versions of the libraries in variables using the reserved word ext.Name We want. I do this because the code is then more readable and easier to play when it comes to updating.

Let's look at the implemented libraries:

  • Picasso: We will use it to upload the images from the internet.
  • Retrofit: As we mentioned before we will use it for the REST request.
  • GSon: It will convert the JSON into an easy data model to be able to work with it.
  • Anko: We have already used it several times, we will use it to simplify the practice as much as possible.
  • CardView: It is a component of Android, which allows us to create a kind of cards that visually look great, it will be the container of our images.

The rest are libraries that come by default so I will not enter them.

Next, we will create the data model, that is, a class that allows Gson to extract the JSON values and create an object of the class with those values.

It will be a data class called DogsResponse and it will contain two fields, the same ones that returned the response of the request.

data class DogsResponse (@SerializedName("status") var status:String, @SerializedName("message") var images: List<String>)

As you can see, it is a very common class that we have worked with, the only difference is the @SerializedName attribute, which we will use to add the exact name of the value that the API returns. This attribute can be removed if the name of our field is exact to the one that returns the response, but I recommend leaving it.

In this example, we will create a single request, which will be the list of images, but we can have many more functions with different answers, all of them will go in an interface (not a class) that we will create called APIService.

interface APIService {
    @GET
    fun getCharacterByName(@Url url:String): Call<DogsResponse>
}

Although it is very short we must understand certain aspects. Before each function, we must say what type of REST call will be, GET, POST, etc. Then we have added @URL to the parameter that it will receive since it will be the name of the race and since GET will be in the URL. To finish we will return an object of the DogsResponse class with all the information. Notice that it goes inside a Call <> Retrofit object.

Now we will go to the MainActivity to add the retrofit implementation.

We will create a method that returns a Retrofit object, which will be responsible for the request.

    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
                .baseUrl("https://dog.ceo/api/breed/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }

In order to create it, we will call the Builder () of the retrofit library, in which the baseUrl must be passed, which is the URL where we will make the request, but only the base, ie https://dog.ceo/api/breed/ (important the / final or will give us error).

The URL of the complete petition will be https://dog.ceo/api/breed/akita/images, but as the final part is dynamic (it depends on the race we want to look for) we do not add it to baseUrl.

Then, before the build, we added the factory that converts the JSON into our DogsResponse class, which is the addConverterFactory line.

We must understand that a request is made asynchronously, that is to say, we start the petition and as we do not know what it may take, once we have the information, it will notify us. This process could block the application because we use the threads. The threads allow us to create subprocesses within the app without blocking the main thread, which is the one that manages the interface. Therefore we must create a thread that manages the call. More information about the subprocesses on the official page of Android developers.

   private fun searchByName(query: String) {
        doAsync {
            val call = getRetrofit().create(APIService::class.java).getCharacterByName("$query/images").execute()
            val puppies = call.body() as DogsResponse
            uiThread {
                if(puppies.status == "success") {
                    initCharacter(puppies)
                }else{
                    showErrorDialog()
                }
                hideKeyboard()
            }
        }
    }

The creation of a thread is very simple, thanks to Anko, we will simply call the function doAsync {} and everything we do inside it will be managed in another thread.

We proceed to understand what we have done. The first thing is to create a variable called call that will call the function getRetrofit () that we created previously, followed by the interface that contains the call we want and we end up passing the query (which will be the dog breed we have put in the seeker) and then call the function to execute () .

The content of that variable will be the response of our API, but Retrofit will return a generic object with more content than we are looking for, so we create variable puppies that will call the previous variable call followed by .body () that allows extracting only the information of our interest. Next, as it is a generic object, we must say that it is of the DogsResponse type.

With this we would have the information we are interested in, the following will be to modify the view to add the new information, but as I said previously, the visual part is worked on the main thread so we will call inside the doAsync {} to the uiThread function {} that allows us to execute part of the code in the main thread.

Now we will check if the status is a success, which means that the API has returned the correct values ​​and if so we will start the view or we will show an error dialog and finish, independently of the result, we will hide the keyboard. We have not developed this part yet.

Design[edit]

The design will be very simple, our activity_main will contain a SearchView and a RecyclerView, both will go inside a ConstraintLayout.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    android:id="@+id/viewRoot"
    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="com.cursokotlin.retrofitkotlinexample.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvDogs"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/searchBreed"/>

    <android.support.v7.widget.SearchView
        android:id="@+id/searchBreed"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="7dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0"
        app:queryHint="Ej: Akita" />

</android.support.constraint.ConstraintLayout>

In the RecyclerView we have said that it takes the total width of the screen and the height will be the same but starting below the search engine. The SearchView has a new property, queryHint that allows us to add a description so that the user can see an example of what to look for.

As the SearchView we have not seen it, it will be the first thing we implement in the MainActivity. For it to work we must make our activity implement the necessary methods.

class MainActivity : AppCompatActivity(), android.support.v7.widget.SearchView.OnQueryTextListener {

We must also add its listener in the onCreate () function.

searchBreed.setOnQueryTextListener(this)

Once the extension is added, it will ask us to implement the necessary methods.

The first method onQueryTextChange will warn us every time the user adds a character, but this will not be used because we will only search for the race once the user finishes writing. We will focus on OnQueryTextSubmit, which will be launched once the user clicks on the action that he has finished writing, so he returns a String, which will be the text entered.

We will take advantage of the OnQueryTextSubmit function to launch the request once the user finishes writing in the search engine, so we will modify the function to call searchByName ().

   override fun onQueryTextSubmit(query: String): Boolean {
        searchByName(query.toLowerCase())
        return true
    }

Before sending the search, we have called the ToLowerCase () function that transforms all the text into lowercase.

Once we send it out and the request is made, we will show the images, we will use our recyclerView, so the next step will be to create the adapter DogsAdapter and view, we start with the latter, which call item_dog.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="320dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_margin="16dp"
    android:elevation="7dp"
    app:cardCornerRadius="8dp">

    <ImageView
        android:id="@+id/ivDog"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.v7.widget.CardView>
As we said at the beginning of the chapter, these views will go to a cardView. The only new attribute will be cardCornerRadius which will be the level of the rounded edge, the more we put it, the closer the corners will be.

Our adapter will be very simple.

class DogsAdapter (val images: List<String>) : RecyclerView.Adapter<DogsAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = images[position]
        holder.bind(item)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return ViewHolder(layoutInflater.inflate(R.layout.item_dog, parent, false))
    }

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

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        fun bind(image: String) {
            itemView.ivDog.fromUrl(image)
        }
    }
}

In the bind () function of the ViewHolder, we have taken the image of the view and called an extension function we have created called fromUrl (). This extension will be created in a new file (I usually call extensions eg ImageExtensions, DateExtensions).

fun ImageView.fromUrl(url:String){
    Picasso.get().load(url).into(this)
}

The extension uses the Picasso library to convert the URL of the image into a visual element.

With this, we have finished the adapter part and can return to the MainActivity.

Recall that once the request responds we can make two options, the first load the items obtained in the recyclerView.

     private fun initCharacter(puppies: DogsResponse) {
        if(puppies.status == "success"){
            imagesPuppies = puppies.images
        }
        dogsAdapter = DogsAdapter(imagesPuppies)
        rvDogs.setHasFixedSize(true)
        rvDogs.layoutManager = LinearLayoutManager(this)
        rvDogs.adapter = dogsAdapter
    }

First, we make sure that the state is a success if we make the mistake of calling the method from another side if so, we will assign the result of the response in a generic array that we have created at the top of the class.

    lateinit var imagesPuppies:List<String>

Then we will start the recyclerView with this list.

Now we will have two methods to finish the project. The first will show an error if the service has failed.

    lateinit var imagesPuppies:List<String>
private fun showErrorDialog() {
        alert("An error has occurred, try again.") {
            yesButton { }
        }.show()
    }

The second one will lower the keyboard once the user searches for a race.

 private fun hideKeyboard(){
        val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(viewRoot.windowToken, 0)
    }

With this, we would have everything. The last step will be to go to AndroidManifest and add internet permissions to make the call.

 uses-permission android:name="android.permission.INTERNET" ;

This chapter is a bit more complicated than the previous ones, but I wanted to show it as soon as possible because it is essential for any application.

About This Tutorial

This page was last edited on 30 May 2019, at 00:08.