Networking Basics with Retrofit

Retrofit is an open-source library created and maintained by Square (square.github.io/​retrofit). Under the hood, it uses the OkHttp library as its HTTP client (square.github.io/​okhttp).

Retrofit helps you build an HTTP gateway class. You write an interface with annotated instance methods, and Retrofit creates the implementation. Retrofit’s implementation handles making an HTTP request and parsing the HTTP response into an OkHttp.ResponseBody. But this is limiting: It would be better if you could work with your app’s data types. To support this, Retrofit lets you register a response converter, which Retrofit then uses to marshal your data types into the request and un-marshal your data types from the response.

Add the Retrofit dependency to your app module’s build.gradle file. Sync your Gradle file once you make the change.

Listing 24.6  Adding the Retrofit dependency (app/build.gradle)

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    ...
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
}

Before retrofitting (pun intended) the Flickr REST API, you will first configure Retrofit to fetch and log the contents of a web page URL – specifically, Flickr’s home page. Using Retrofit involves a bunch of moving parts. Starting simple will allow you to see the foundations. Later, you will build on this basic implementation to build your Flickr requests and deserialize the responses – meaning convert the linear, serialized data into non-serial pieces of data. That non-serial data will be your model objects.

Defining an API interface

It is time to define the API calls you want your app to be able to make. First, create a new package for your API-specific code. In the project tool window, right-click the com.bignerdranch.android.photogallery folder and choose NewPackage. Name your new package api.

Next, add a Retrofit API interface to your new package. A Retrofit API interface is a standard Kotlin interface that uses Retrofit annotations to define API calls. Right-click the api folder in the project tool window. Choose NewKotlin File/Class and name the file FlickrApi. Leave the Kind dropdown set to File. In the new file, define an interface named FlickrApi and add a single function representing a GET request.

Listing 24.7  Adding a Retrofit API interface (api/FlickrApi.kt)

interface FlickrApi {

    @GET("/")
    fun fetchContents(): Call<String>
}

If you are given a choice when importing Call, select retrofit2.Call.

Each function in the interface maps to a specific HTTP request and must be annotated with an HTTP request method annotation. An HTTP request method annotation tells Retrofit the HTTP request type (also known as an “HTTP verb”) that the function in your API interface maps to. The most common request types are @GET, @POST, @PUT, @DELETE, and @HEAD. (For an exhaustive list of available types, see the API docs at square.github.io/​retrofit/​2.x/​retrofit).

The @GET("/") annotation in the code above configures the Call returned by fetchContents() to perform a GET request. The "/" is the relative path – a path string representing the relative URL from the base URL of your API endpoint. Most HTTP request method annotations include a relative path. In this case, the relative path of "/" means the request will be sent to the base URL, which you will provide shortly.

By default, all Retrofit web requests return a retrofit2.Call object. A Call object represents a single web request that you can execute. Executing a Call produces one corresponding web response. (You can also configure Retrofit to return RxJava Observables instead, but that is outside the scope of this book.)

The type you use as Call’s generic type parameter specifies the data type you would like Retrofit to deserialize the HTTP response into. By default, Retrofit deserializes the response into an OkHttp.ResponseBody. Specifying Call<String> tells Retrofit that you want the response deserialized into a String object instead.

Building the Retrofit object and creating an API instance

The Retrofit instance is responsible for implementing and creating instances of your API interface. To make web requests based on the API interface you defined, you need Retrofit to implement and instantiate your FlickrApi interface.

First, build and configure a Retrofit instance. Open PhotoGalleryFragment.kt. In onCreate(…), build a Retrofit object and use it to create a concrete implementation of your FlickrApi interface.

Listing 24.8  Using the Retrofit object to create an instance of the API (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl("https://www.flickr.com/")
            .build()

        val flickrApi: FlickrApi = retrofit.create(FlickrApi::class.java)
    }
    ...
}

Retrofit.Builder() is a fluent interface that makes it easy to configure and build your Retrofit instance. You provide a base URL for your endpoint using the baseUrl(…) function. Here, you provide the Flickr home page: "https://www.flickr.com/". Make sure to include the appropriate protocol with the URL (here, https://). Also, always include a trailing / to ensure Retrofit correctly appends the relative paths you provide in your API interface onto the base URL.

Calling build() returns a Retrofit instance, configured based on the settings you specified using the builder object. Once you have a Retrofit object, you use it to create an instance of your API interface. Note that Retrofit does not generate any code at compile time – instead, it does all the work at runtime. When you call retrofit.create(…), Retrofit uses the information in the API interface you specify, along with the information you specified when building the Retrofit instance, to create and instantiate an anonymous class that implements the interface on the fly.

Adding a String converter

By default, Retrofit deserializes web responses into okhttp3.ResponseBody objects. But for logging the contents of a web page, it is much easier to work with a plain ol’ String. To get Retrofit to deserialize the response into strings instead, you will specify a converter when building your Retrofit object.

A converter knows how to decode a ResponseBody object into some other object type. You could create a custom converter, but you do not have to. Lucky for you, Square created an open-source converter, called the scalars converter, that can convert the response into a string. You will use it to deserialize Flickr responses into string objects.

To use the scalars converter, first add the dependency to your app module’s build.gradle file. Do not forget to sync the file after you add the dependency.

Listing 24.9  Adding the scalar converter dependency (app/build.gradle)

dependencies {
    ...
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.5.0'
}

Now, create an instance of the scalar converter factory and add it to your Retrofit object.

Listing 24.10  Adding the converter to the Retrofit object (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl("https://www.flickr.com/")
            .addConverterFactory(ScalarsConverterFactory.create())
            .build()

        val flickrApi: FlickrApi = retrofit.create(FlickrApi::class.java)
    }
    ...
}

Retrofit.Builder’s addConverterFactory(…) function expects an instance of Converter.Factory. A converter factory knows how to create and return instances of a particular converter. ScalarsConverterFactory.create() returns an instance of the scalars converter factory (retrofit2.converter.scalars.ScalarsConverterFactory), which in turn will provide instances of a scalar converter when Retrofit needs it.

More specifically, since you specified Call<String> as the return type for FlickrApi.fetchContents(), the scalar converter factory will provide an instance of the string converter (retrofit2.converter.scalars.StringResponseBodyConverter). In turn, your Retrofit object will use the string converter to convert the ResponseBody object into a String before returning the Call result.

Square provides other handy open-source converters for Retrofit. Later in this chapter, you will use the Gson converter. You can see the other available converters, and information on creating your own custom converter, at square.github.io/​retrofit.

Executing a web request

Up to this point, you have been writing code to configure your network request. Now is the moment you have been waiting for: executing a web request and logging the result. First, call fetchContents() to generate a retrofit2.Call object representing an executable web request.

Listing 24.11  Getting a Call representing a request (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val flickrApi: FlickrApi = retrofit.create(FlickrApi::class.java)

        val flickrHomePageRequest: Call<String> = flickrApi.fetchContents()
    }
    ...
}

Note that calling fetchContents() on the FlickrApi instance does not cause any networking to happen. Instead, fetchContents() returns a Call<String> object representing a web request. You can execute the Call at any time in the future. Retrofit determines the details of the call object based on the API interface you provided (FlickrApi) and the Retrofit object you created.

To execute the web request represented by the Call object, call the enqueue(…) function in onCreate(savedInstanceState: Bundle?) and pass an instance of retrofit2.Callback. While you are at it, add a TAG constant.

Listing 24.12  Executing the request asynchronously (PhotoGalleryFragment.kt)

private const val TAG = "PhotoGalleryFragment"

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val flickrHomePageRequest : Call<String> = bnrInterface.fetchContents()

        flickrHomePageRequest.enqueue(object : Callback<String> {
            override fun onFailure(call: Call<String>, t: Throwable) {
                Log.e(TAG, "Failed to fetch photos", t)
            }

            override fun onResponse(
                call: Call<String>,
                response: Response<String>
            ) {
                Log.d(TAG, "Response received: ${response.body()}")
            }
        })
    }
}

Retrofit makes it easy to respect the two most important Android threading rules:

  1. Execute long-running operations on a background thread, never on the main thread.

  2. Update the UI from the main thread only, never from a background thread.

Call.enqueue(…) executes the web request represented by the Call object. Most importantly, it executes the request on a background thread. Retrofit manages the background thread for you, so you do not have to worry about it.

The thread maintains a queue, or list, of work to do. When you call Call.enqueue(…), Retrofit adds your request to its queue of work. You can enqueue multiple requests, and Retrofit will process them one by one until the queue is empty. (You will learn more about creating and managing background threads in Chapter 25.)

The Callback object you pass to enqueue(…) is where you define what you want to happen after the request completes and the response comes back. When the request running on the background thread is complete, Retrofit calls one of the callback functions you provided on the main (UI) thread: If a response is received from the server, it calls Callback.onResponse(…); if not, it calls Callback.onFailure(…).

The Response Retrofit passes to onResponse() contains the result contents in its body. The type of the result will match the return type you specified on the corresponding function in the API interface. In this case, fetchContents() returns Call<String>, so response.body() returns a String.

The Call object passed to onResponse() and onFailure() is the original call object used to initiate the request.

You can force a request to execute synchronously by calling Call.execute(). Just make sure the execution happens on a background thread and not on the main UI thread. As you learned in Chapter 11, Android disallows all networking on the main thread. If you try to do it, Android will throw a NetworkOnMainThreadException.

Asking permission to network

One more thing is required to get networking up and running: You have to ask permission. Just as users would not want you secretly taking their pictures, they also do not want you secretly downloading ASCII pictures of farm animals.

To ask permission to network, add the following permission to your manifests/AndroidManifest.xml file.

Listing 24.13  Adding networking permission to the manifest (manifests/AndroidManifest.xml)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.bignerdranch.android.photogallery" >

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

    <application>
        ...
    </application>

</manifest>

Android treats the INTERNET permission as “normal,” since so many apps require it. As a result, all you need to do to use this permission is declare it in your manifest. More dangerous permissions (like the one allowing you to know the device’s location) also require a runtime request.

Run your code, and you should see the amazing Flickr home page HTML pop up in Logcat, as shown in Figure 24.3. (Finding your log statements within the Logcat window can be tricky. It helps to search for something specific. In this case, enter PhotoGalleryFragment in the Logcat search box, as shown.)

Figure 24.3  Flickr.com HTML in Logcat

Flickr.com HTML in Logcat

Moving toward the repository pattern

Right now, your networking code is embedded in your fragment. Before you move on, move the Retrofit configuration code and API direct access to a new class.

Create a new Kotlin file named FlickrFetchr.kt. Add a property to stash a FlickrApi instance. Cut the Retrofit configuration code and API interface instantiation code from PhotoGalleryFragment and paste it into an init block in the new class (these are the two lines that start with val retrofit: Retrofit = ... and flickrApi = ... in Listing 24.14). Split the flickrApi declaration and assignment into two separate lines to declare flickrApi as a private property on FlickrFetchr. This is so that you can access it elsewhere in the class (outside of the init block) – but not outside of the class.

When you are done, FlickrFetchr should match Listing 24.14.

Listing 24.14  Creating FlickrFetchr (FlickrFetchr.kt)

private const val TAG = "FlickrFetchr"

class FlickrFetchr {

    private val flickrApi: FlickrApi

    init {
        val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl("https://www.flickr.com/")
            .addConverterFactory(ScalarsConverterFactory.create())
            .build()

        flickrApi = retrofit.create(FlickrApi::class.java)
    }
}

If you have not already, cut the redundant Retrofit configuration code from PhotoGalleryFragment (Listing 24.15). This will cause an error, which you will fix once you finish fleshing out FlickrFetchr.

Listing 24.15  Cutting Retrofit setup from the fragment (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl("https://www.flickr.com/")
            .addConverterFactory(ScalarsConverterFactory.create())
            .build()

        val flickrApi: FlickrApi = retrofit.create(FlickrApi::class.java)

        val flickrHomePageRequest : Call<String> = flickrApi.fetchContents()

        ...
    }
    ...
}

Next, add a function named fetchContents() to FlickrFetchr to wrap the Retrofit API function you defined for fetching the Flickr home page. (You can copy most of the code below from PhotoGalleryFragment, but make sure to adjust the pasted result to match Listing 24.16.)

Listing 24.16  Adding fetchContents() to FlickrFetchr (FlickrFetchr.kt)

private const val TAG = "FlickrFetchr"

class FlickrFetchr {

    private val flickrApi: FlickrApi

    init {
        ...
    }

    fun fetchContents(): LiveData<String> {
        val responseLiveData: MutableLiveData<String> = MutableLiveData()
        val flickrRequest: Call<String> = flickrApi.fetchContents()

        flickrRequest.enqueue(object : Callback<String> {

            override fun onFailure(call: Call<String>, t: Throwable) {
                Log.e(TAG, "Failed to fetch photos", t)
            }

            override fun onResponse(
                call: Call<String>,
                response: Response<String>
            ) {
                Log.d(TAG, "Response received")
                responseLiveData.value = response.body()
            }
        })

        return responseLiveData
    }
}

In fetchContents(), you instantiate responseLiveData to an empty MutableLiveData<String> object. You then enqueue a web request to fetch the Flickr page and return responseLiveData immediately (before the request completes). When the request successfully completes, you publish the result by setting responseLiveData.value. This way other components, such as PhotoGalleryFragment, can observe the LiveData returned from fetchContents() to eventually receive the web request results.

Note that the return type for fetchContents() is a non-mutable LiveData<String>. You should avoid exposing mutable live data objects when at all possible to limit other components from changing the live data’s contents. The data should flow in one direction through the LiveData.

FlickrFetchr will wrap most of the networking code in PhotoGallery (right now it is small and simple, but it will grow over the next several chapters). fetchContents() enqueues the network request and wraps the result in LiveData. Now other components in your app, such as PhotoGalleryFragment (or some ViewModel or activity, etc.), can create an instance of FlickrFetchr and request photo data without having to know about the Retrofit or the source the data is coming from.

Update PhotoGalleryFragment to use FlickrFetchr to see this magic in action (Listing 24.17).

Listing 24.17  Using FlickrFetchr in PhotoGalleryFragment (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val flickrHomePageRequest : Call<String> = flickrApi.fetchContents()

        flickrHomePageRequest.enqueue(object : Callback<String> {
            override fun onFailure(call: Call<String>, t: Throwable) {
                Log.e(TAG, "Failed to fetch photos", t)
            }

            override fun onResponse(
                call: Call<String>,
                response: Response<String>
            ) {
                Log.d(TAG, "Response received: ${response.body()}")
            }
        })

        val flickrLiveData: LiveData<String> = FlickrFetchr().fetchContents()
        flickrLiveData.observe(
            this,
            Observer { responseString ->
                Log.d(TAG, "Response received: $responseString")
            })
    }
    ...
}

This refactor moves your app closer to following the repository pattern recommended by Google in its Guide to App Architecture (developer.android.com/​jetpack/​docs/​guide). FlickrFetchr serves as a very basic repository. A repository class encapsulates the logic for accessing data from a single source or a set of sources. It determines how to fetch and store a particular set of data, whether locally in a database or from a remote server. Your UI code will request all of the data from the repository, because the UI does not care how the data is actually stored or fetched. Those are implementation details of the repository itself.

Right now, all of your app’s data comes directly from the Flickr web server. However, in the future you might decide to cache that data in a local database. In that case, the repository would manage getting the data from the right place. Other components in your app can use the repository to get data without having to know where the data is coming from.

Take a moment to run your app and verify that it still works correctly. You should see the contents of the Flickr home page print to Logcat, as shown in Figure 24.3.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.221.113