Fetching JSON from Flickr

JSON stands for JavaScript Object Notation. It is a popular data format, particularly for web services. You can get more information about JSON as a format at json.org.

Flickr offers a fine JSON API. All the details you need are available in the documentation at flickr.com/​services/​api. Pull it up in your favorite web browser and find the list of Request Formats. You will be using the simplest – REST. The REST API endpoint is api.flickr.com/​services/​rest, and you can invoke the methods Flickr provides on this endpoint.

Back on the main page of the API documentation, find the list of API Methods. Scroll down to the interestingness section and click on flickr.interestingness.getList. The documentation will report that this method returns the list of interesting photos for the most recent day or a user-specified date. That is exactly what you want for PhotoGallery.

The only required parameter for the getList method is an API key. To get an API key, return to flickr.com/​services/​api and follow the link for API Keys. You will need a Yahoo ID to log in. Once you are logged in, request a new, noncommercial API key. This usually only takes a moment. Your API key will look something like 4f721bgafa75bf6d2cb9af54f937bb70. (You do not need the “Secret,” which is only used when an app will access user-specific information or images.)

Once you have a key, you have all you need to make a request to the Flickr web service. Your GET request URL will look something like this:

    https://api.flickr.com/services/rest/?method=flickr.interestingness.getList
      &api_key=yourApiKeyHere&format=json&nojsoncallback=1&extras=url_s

The Flickr response is in XML format by default. To get a valid JSON response, you need to specify values for both the format and nojsoncallback parameters. Setting nojsoncallback to 1 tells Flickr to exclude the enclosing method name and parentheses from the response it sends back. This lets your Kotlin code more easily parse the response.

Specifying the parameter called extras with the value url_s tells Flickr to include the URL for the small version of the picture if it is available.

Copy the example URL into your browser, replacing yourApiKeyHere with your actual API key. This will allow you to see an example of what the response data will look like, as shown in Figure 24.4.

Figure 24.4  Sample JSON output

Sample JSON output

Time to update your existing networking code to request recent interesting photos data from the Flickr REST API instead of requesting the contents of Flickr’s home page. First, add a function to your FlickrApi API interface. Again, replace yourApiKeyHere with your API key. For now, hardcode the URL query parameters in the relative path string. (Later, you will abstract these query parameters out and add them in programmatically.)

Listing 24.18  Defining the “fetch recent interesting photos” request (api/FlickrApi.kt)

interface FlickrApi {

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

    @GET(
        "services/rest/?method=flickr.interestingness.getList" +
                "&api_key=yourApiKeyHere" +
                "&format=json" +
                "&nojsoncallback=1" +
                "&extras=url_s"
    )
    fun fetchPhotos(): Call<String>
}

Notice that you added values for the method, api_key, format, nojsoncallback, and extras parameters.

Next, update the Retrofit instance configuration code in FlickrFetchr. Change the base URL from Flickr’s home page to the base API endpoint. Rename the fetchContents() function to fetchPhotos() and call through to the new fetchPhotos() function on the API interface.

Listing 24.19  Updating the base URL (FlickrFetchr.kt)

class FlickrFetchr {

    private val flickrApi: FlickrApi

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

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

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

Note that the base URL you set is api.flickr.com, but the endpoints you want to hit are at api.flickr.com/services/rest. This is because you specified the services and rest parts of the path in your @GET annotation in FlickrApi. The path and other information you included in the @GET annotation will be appended onto the URL by Retrofit before it issues the web request.

Finally, update PhotoGalleryFragment to execute the web request so that it fetches recent interesting photos instead of the contents of Flickr’s home page. Replace the fetchContents() call with a call to the new fetchPhotos() function. For now, serialize the response into a string, as you did previously.

Listing 24.20  Executing the “fetch recent interesting photos” request (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

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

        val flickrLiveData: LiveData<String> = FlickrFetchr().fetchContentsPhotos()
        ...
    }
    ...
}

Making these few tweaks to your existing code renders your app ready to fetch and log Flickr data. Run PhotoGallery, and you should see rich, fertile Flickr JSON in Logcat, like Figure 24.5. (It will help to search for PhotoGalleryFragment in the Logcat search box.)

Figure 24.5  Flickr JSON in Logcat

Flickr JSON in Logcat

(Logcat can be finicky. Do not panic if you do not get results like ours. Sometimes the connection to the emulator is not quite right and the log messages do not get printed out. Usually it clears up over time, but sometimes you have to rerun your application or even restart your emulator.)

As of this writing, the Android Studio Logcat window does not wrap the output automatically. Scroll to the right to see more of the extremely long JSON response string. Or wrap the Logcat contents by clicking the Use Soft Wraps button shown in Figure 24.5.

Now that you have such fine JSON data from Flickr, what should you do with it? You will do what you do with all data – put it in one or more model objects. The model class you are going to create for PhotoGallery is called GalleryItem. A gallery item holds meta information for a single photo, including the title, the ID, and the URL to download the image from.

Create the GalleryItem data class and add the following code:

Listing 24.21  Creating a model object class (GalleryItem.kt)

data class GalleryItem(
    var title: String = "",
    var id: String = "",
    var url: String = ""
)

Now that you have defined a model object, it is time to create and populate instances of that object with data from the JSON output you got from Flickr.

Deserializing JSON text into model objects

The JSON response displayed in your browser and Logcat window is hard to read. If you pretty print the response (format it with white space), it looks something like the text on the left in Figure 24.6.

Figure 24.6  JSON text, JSON hierarchy, and corresponding model objects

JSON text, JSON hierarchy, and corresponding model objects

A JSON object is a set of name-value pairs enclosed between curly braces, { }. A JSON array is a comma-separated list of JSON objects enclosed in square brackets, [ ]. You can have objects nested within each other, resulting in a hierarchy.

Android includes the standard org.json package, which has classes that provide access to creating and parsing JSON text (such as JSONObject and JSONArray). However, lots of smart people have created libraries to simplify the process of converting JSON text to Java objects and back again.

One such library is Gson (github.com/​google/​gson). Gson maps JSON data to Kotlin objects for you automatically. This means you do not need to write any parsing code. Instead, you define Kotlin classes that map to the JSON hierarchy of objects and let Gson do the rest.

Square created a Gson converter for Retrofit that makes it easy to plug Gson into your Retrofit implementation. First, add the Gson and Retrofit Gson converter library dependencies to your app module’s Gradle file. As always, be sure to sync the file when you are done.

Listing 24.22  Adding Gson dependencies (app/build.gradle)

dependencies {
    ...
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.5.0'
    implementation 'com.google.code.gson:gson:2.8.5'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
}

Next, create model objects that map to the JSON data in the Flickr response. You already have GalleryItem, which maps almost directly to an individual object in the "photo" JSON array. By default, Gson maps JSON object names to property names. If your property names match the JSON object names directly, you can leave them as is.

But your property names do not need to match the JSON object names. Take your GalleryItem.url property, versus the "url_s" field in the JSON data. GalleryItem.url is more meaningful in the context of your codebase, so it is better to keep it. In this case, you can add a @SerializedName annotation to the property to tell Gson which JSON field the property maps to.

Update GalleryItem now to do just that.

Listing 24.23  Overriding default name-property mapping (GalleryItem.kt)

data class GalleryItem(
    var title: String = "",
    var id: String = "",
    @SerializedName("url_s") var url: String = ""
)

Now, create a PhotoResponse class to map to the "photos" object in the JSON data. Place the new class in the api package, since this class is a side effect of how you implement deserialization of the Flickr API, rather than a model object the rest of your app cares about.

Include a property called galleryItems to store a list of gallery items and annotate it with @SerializedName("photo"). Gson will automatically create a list and populate it with gallery item objects based on the JSON array named "photo". (Right now, the only data you care about in this particular object is the array of photo data in the "photo" JSON object. Later in this chapter, you will want to capture the paging data if you choose to do the challenge in the section called Challenge: Paging.)

Listing 24.24  Adding PhotoResponse (PhotoResponse.kt)

class PhotoResponse {
    @SerializedName("photo")
    lateinit var galleryItems: List<GalleryItem>
}

Finally, add a class named FlickrResponse to the api package. This class will map to the outermost object in the JSON data (the one at the top of the JSON object hierarchy, denoted by the outermost { }). Add a property to map to the "photos" field.

Listing 24.25  Adding FlickrResponse (FlickrResponse.kt)

class FlickrResponse {
    lateinit var photos: PhotoResponse
}

The diagram in Figure 24.6 shows how the objects you created map to the JSON data.

Now it is time to make the magic happen: to configure Retrofit to use Gson to deserialize your data into the model objects you just defined. First, update the return type specified in the Retrofit API interface to match the model object you defined to map to the outermost JSON object. This indicates to Gson that it should use the FlickrResponse to deserialize the JSON response data.

Listing 24.26  Updating fetchPhoto()’s return type (FlickrApi.kt)

interface FlickrApi {

    @GET(...)
    fun fetchPhotos(): Call<StringFlickrResponse>
}

Next, update FlickrFetchr. Swap out the scalar converter factory for a Gson converter factory. Update fetchPhotos() to return LiveData that wraps a list of gallery items. Change the LiveData and MutableLiveData type specifiers from String to List<GalleryItem>. Change the Call and Callback type specifiers from String to FlickrResponse. Finally, update onResponse(…) to dig the gallery item list out of the response and update the live data object with the list.

Listing 24.27  Updating FlickrFetchr for Gson (FlickrFetchr.kt)

class FlickrFetchr {

    private val flickrApi: FlickrApi

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

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

    fun fetchPhotos(): LiveData<StringList<GalleryItem>> {
        val responseLiveData: MutableLiveData<String> = MutableLiveData()
        val responseLiveData: MutableLiveData<List<GalleryItem>> = MutableLiveData()
        val flickrRequest: Call<StringFlickrResponse> = flickrApi.fetchPhotos()

        flickrRequest.enqueue(object : Callback<StringFlickrResponse> {

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

            override fun onResponse(
                call: Call<StringFlickrResponse>,
                response: Response<StringFlickrResponse>
            ) {
                Log.d(TAG, "Response received")
                responseLiveData.value = response.body()
                val flickrResponse: FlickrResponse? = response.body()
                val photoResponse: PhotoResponse? = flickrResponse?.photos
                var galleryItems: List<GalleryItem> = photoResponse?.galleryItems
                    ?: mutableListOf()
                galleryItems = galleryItems.filterNot {
                    it.url.isBlank()
                }
                responseLiveData.value = galleryItems
            }
        })

        return responseLiveData
    }
}

Note that Flickr does not always return a valid url_s value for each image. The code above filters out gallery items with blank URL values using filterNot{…}.

Last, update the LiveData type specifier in PhotoGalleryFragment.

Listing 24.28  Updating type specifiers in PhotoGalleryFragment (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {

    private lateinit var photoRecyclerView: RecyclerView

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

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

Run PhotoGallery to test your JSON parsing code. You should see the toString() output of the gallery item list printed to Logcat. If you want explore the results further, set a breakpoint on the logging line in Observer and use the debugger to drill down through galleryItems (Figure 24.7).

Figure 24.7  Exploring the Flickr response

Exploring the Flickr response

If you run into an UninitializedPropertyAccessException, make sure that your web request is properly formatted. In some cases (such as when the API key is invalid), the Flickr API will return a response with a good response code (200) but an empty response body, and Gson will fail to initialize your late-initialized models.

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

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