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 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 Flickr 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 4f721bgafa75bf6d2cb9be54f937bb71. (That is a fake key we made up as an example – you will need to obtain your own Flickr API key.) You do not need the “Secret,” which is only used when an app will access user-specific information or images.

With your shiny new key, you can 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 20.5. (Your results may be formatted differently, depending on your browser. But however it is laid out, you should see text like “photos,” “page,” “pages,” and so on.)

Figure 20.5  Sample JSON output

Sample JSON output

Time to update your existing networking code to request data for recent interesting photos 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 20.16  Defining the “fetch recent interesting photos” request (api/FlickrApi.kt)

private const val API_KEY = "yourApiKeyHere"

interface FlickrApi {
    @GET("/")
    suspend fun fetchContents() : String
    @GET(
        "services/rest/?method=flickr.interestingness.getList" +
            "&api_key=$API_KEY" +
            "&format=json" +
            "&nojsoncallback=1" +
            "&extras=url_s"
    )
    suspend fun fetchPhotos(): String
}

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

Next, update the Retrofit instance configuration code in PhotoRepository. 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 20.17  Updating the base URL (PhotoRepository.kt)

class PhotoRepository {
    private val flickrApi: FlickrApi

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

    suspend fun fetchContent() = flickrApi.fetchContent()
    suspend fun fetchPhotos() = flickrApi.fetchPhotos()
}

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 20.18  Executing the “fetch recent interesting photos” request (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
          val response = PhotoRepository().fetchContent()
          val response = PhotoRepository().fetchPhotos()
          Log.d(TAG, "Response received: $response")
        }
    }
    ...
}

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 20.6. (It will help to search for PhotoGalleryFragment in the Logcat search box.)

Figure 20.6  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 automatically wrap the output the way Figure 20.6 shows. Scroll to the right to see more of the extremely long JSON response string. Or wrap the Logcat contents by clicking the Flickr JSON in Logcat button on Logcat’s left side, shown in Figure 20.6.

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 within the api subpackage and add the following code:

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

data class GalleryItem(
    val title: String,
    val id: String,
    val 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 20.7.

Figure 20.7  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, [ ]. And JSON objects can be nested within each other, resulting in a hierarchy like the one in the middle column of Figure 20.7. (The right side of Figure 20.7 shows the GalleryItem and the other model objects you will create shortly to represent this data.)

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 Kotlin objects and back again.

One such library is Moshi (github.com/​square/​moshi). Another library from Square, Moshi 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 Moshi do the rest.

Using Moshi is similar to using the Room database library’s @Entity data classes. Moshi uses code generation to map JSON to Kotlin classes for you. You will annotate your relevant code, and Moshi will generate code that adapts JSON strings into instances of Kotlin classes. Moshi also has the functionality to parse strings into Kotlin classes dynamically at runtime, but the code generation approach is more performant and easier to set up.

To configure Moshi to do all those things for you, first enable the same kapt plugin you used with Room. It is defined at the project level, so add the following line to the build.gradle file labeled (Project: PhotoGallery):

Listing 20.20  Enabling kapt (build.gradle)

plugins {
    id 'com.android.application' version '7.1.2' apply false
    id 'com.android.library' version '7.1.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
    id 'org.jetbrains.kotlin.kapt' version '1.6.10' apply false
}
...

Once you have enabled the plugin, apply it to your app’s build process in app/build.gradle. Include the core library as well as the library that performs the code generation in your dependencies. Finally, Square created a Moshi converter for Retrofit that makes it easy to plug Moshi into your Retrofit implementation. Add the Retrofit Moshi converter library dependencies as well. As always, be sure to sync the file when you are done.

Listing 20.21  Adding Moshi dependencies (app/build.gradle)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'org.jetbrains.kotlin.kapt'
}

android {
    ...
}

dependencies {
    ...
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
    implementation 'com.squareup.moshi:moshi:1.13.0'
    kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.13.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
    ...
}

With your dependencies in place, 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, Moshi maps JSON object names to property names. If your property names match the JSON object names, 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 @Json annotation to the property to tell Moshi which JSON field the property maps to.

To generate the code to adapt the JSON string into a GalleryItem, you need to annotate the class with the @JsonClass annotation. This will tell Moshi to perform its code generation work during compilation. Update GalleryItem with these annotations now.

Listing 20.22  Integrating Moshi (GalleryItem.kt)

@JsonClass(generateAdapter = true)
data class GalleryItem(
    val title: String,
    val id: String,
    @Json(name = "url_s") val 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 as well.

Include a property called galleryItems to store a list of gallery items and annotate it with @Json(name = "photo"). Moshi will automatically create a list and populate it with gallery item objects based on the JSON array named "photo".

Listing 20.23  Adding PhotoResponse (PhotoResponse.kt)

@JsonClass(generateAdapter = true)
data class PhotoResponse(
    @Json(name = "photo") val galleryItems: List<GalleryItem>
)

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.

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 20.24  Adding FlickrResponse (FlickrResponse.kt)

@JsonClass(generateAdapter = true)
data class FlickrResponse(
    val photos: PhotoResponse
)

Take another look at the diagram comparing the JSON text to model objects (copied below in Figure 20.8) and notice how the objects you created map to the JSON data.

Figure 20.8  PhotoGallery data and model objects

PhotoGallery data and model objects

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

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

interface FlickrApi {
    @GET(...)
    fun fetchPhotos(): StringFlickrResponse
}

Next, update PhotoRepository. Swap out the scalars converter factory for a Moshi converter factory and update fetchPhotos() to return the list of gallery items.

Listing 20.26  Updating PhotoRepository for Moshi (PhotoRepository.kt)

class PhotoRepository {
    private val flickrApi: FlickrApi

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

    suspend fun fetchPhotos() = flickrApi.fetchPhotos()
    suspend fun fetchPhotos(): List<GalleryItem> =
        flickrApi.fetchPhotos().photos.galleryItems
}

Now that you are no longer using the scalars converter factory, you do not need the retrofit2.converter.scalars imports in PhotoRepository.kt and PhotoGalleryFragment.kt. They might disappear on their own, but if not you should delete them, because they may cause errors.

You do not need to make any changes to the PhotoGalleryFragment class, since you are only logging out the result. Run PhotoGallery to test your JSON parsing code. You should see the logging output of the gallery item list printed to Logcat. If you want explore the results further, set a breakpoint on the logging line in the lambda and use the debugger to drill down through galleryItems (Figure 20.9).

Figure 20.9  Exploring the Flickr response

Exploring the Flickr response

If you run into any issues, 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 an error response and Moshi will fail to initialize your models. In the next section, you will handle situations where things do not go according to plan.

Handling errors

There are hundreds of ways in which a network request can go wrong. The device might not be connected to the internet. The server might be down and fail to respond to the request. There might be an issue with the contents of your request or the server’s response.

In those cases, you will not have an easy-to-use FlickrResponse to mess with in your code. You will need to handle errors related to these situations yourself.

To model a common network issue, turn off internet access on your device or emulator: Swipe down from the top of the screen to open Quick Settings. Press the Internet icon, and toggle off the mobile data and WiFi options (Figure 20.10). Press Done.

Figure 20.10  Turning off the internet

Turning off the internet

(The steps to disable internet access might vary, depending on the version of Android. For example, you might instead need to look for separate WiFi and mobile data settings to disable.)

Next, navigate to the overview screen and kill PhotoGallery, if it is running. Finally, try relaunching PhotoGallery. You will see it display briefly – and then crash when it makes a network request that cannot be successfully completed.

There are many ways to handle network request errors through Retrofit, depending on the source of the error and the experience you want to provide to users. But at a minimum, your app should avoid crashing when there is an error with networking.

To prevent a crash in PhotoGallery, you will use Kotlin’s try/catch syntax. In PhotoGalleryFragment, wrap your network request in a try/catch block and log out the error if an exception is thrown.

Listing 20.27  Handling network errors (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
          try {
              val response = PhotoRepository().fetchPhotos()
              Log.d(TAG, "Response received: $response")
          } catch (ex: Exception) {
              Log.e(TAG, "Failed to fetch gallery items", ex)
          }
        }
    }
    ...
}

Compile and run your app again. If you look at Logcat, you will see an error logged, but the app will keep running.

Re-enable internet access on your device or emulator before moving on.

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

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