API Calls

The majority of useful mobile applications rely on data obatined via the internet, often via an API accessed as a web service.

Many modern web services rely on standard HTTP requests, such as GET and POST to allow clients to interact with them. These are known as RESTful webservices, with REST being an acronym for REpresentational State Transfer. The data returned by such web services is often in JSON (JavaScript Object Notation) format, though it can be in XML and other formats.

A further concern of working with web APIs is the possibility for latency. A poor network connection or slow server could mean quite a few seconds of delays, so it is important that the application remains responsive.

The API for the application

This tutorial will build a simple application to retrieve quotes from a web services. Specifically quotes from Ron Swanson from the TV comedy 'Parks and Recreation' (well worth a watch if you've never seen it).

As APIs differ in the functionality they offer, and how they are used, they are usually documented so developers know how to use them. As APIs go, the Ron Swanson quote generator API has a very simple interface, documented on GitHub It is a RESTful API returning data in JSON format.

As requests to retrieve data from a RESTful web API use standard HTTP GET requests, they can be made in the browser. Click (or copy and paste) the following link into a web browser, and you should see a JSON representation of a quote (depending on your browser, you may need to choose to view it in 'raw' format): http://ron-swanson-quotes.herokuapp.com/v2/quotesAs per the documentation, a request to this URL returns a JSON array with a single quote, and example might appear as follows:

["Don't waste energy moving unless necessary."]

Building the application

  1. Start by creating a new application for phone and tablet targeting API 21 and above, using the empty application template.
  2. Open the module level build.gradle file and add the following code inside the android element so that ViewBinding is available:
    buildFeatures {
        viewBinding = true
    }
  3. Setup view binding in Main Activity by adding the following variable:
    private lateinit var binding: ActivityMainBinding
  4. Replace the call to setContentView() in the onCreate method with the following
    binding = ActivityMainBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
  5. Open activity_main.xml and add a button to the top of the UI, and below that add a TextView.
  6. In MainActivity.kt, add the following method stub:
    fun getQuote(){
    }
  7. Add an onClick listener to the button to call the getQuote method you created above
  8. In order for an app to make calls, we need to ensure it has permission to access the internet. Open the AndroidManifest.xml document, and add the following code directly inside the manifest element
    <uses-permission android:name="android.permission.INTERNET" />
  9. As you will have seen from trying out the API in the browser, the data that is returned is in JSON format. In this case it is relatively simple in that the response is an Array with one item, so in effect the quote is simply enclosed by square brackets and quotation marks. Whilst we could remove the JSON encoding with basic String manipulation techniques, we will get some basic familiarity with on of the classes in the org.json package: JSONArray. Add a the following method which creates a JSON array with data (which will eventually come from the web service), then return the first (and only) item in that array:
    private fun processQuoteJson(jsonString: String): String {
        val jsonArray = JSONArray(jsonString)
        return jsonArray[0].toString()
    }
    
    Add the import statement for org.json when prompted
  10. Most APIs provide more complex objects as a result of a request. As a developer you would need to study the documentation of any API you're working with, or failing that, inspect the output of the API, and then work with the JSONObject and JSONArray classes to access the data you need. Take some time to review the able on the JSON object documentation and JSON Array documentation
  11. As we do not want the possibly slow network request to cause our application to freeze while it executes, we can execute it concurrently using a Thread. To use a thread we need an instance of the Thread class which we can instantiate by passing an object that implements the Runnable interface (in a similar manner to the way onClickListeners are created). The "Runnable" interface has a run method which we then implement. The verbose version of the code to create code executed on a thread is as follows:
    //Don't add this code to your project
    val thread = Thread(object : Runnable {
        override fun run() {
            //some long running task logic here
        }
    })
    thread.start()
    Fortunately, we can make use of the same optimisations we use when creating onClickListeners, so the code can be simplified as follows:
    val thread = Thread{
        //some long running task logic here
    }
    thread.start()
    Create a private method inside the MainActivity class called fetchData(urlString: String) and add the code in the example above directly to this method. We will return to the logic that goes inside the run function shortly.
  12. One other problem that exists when working with Threads is the inability to update the UI from a thread we've created, to resolve this, we need to create another instance of the Runnable class with the code we want to execute, and pass it to the Activity's runOnUiThread method.

    Like the thread creation, Kotlin's optimisations allow us to simply pass an anonymous function as follows:
    runOnUiThread{
        //code to update elements within the UI
    }
    Add a method to the MainActivity class which will allow the textView to be updated from within the Thread as follows:
    private fun updateTextView(text: String) {
        runOnUiThread{
            binding.textView.text = text
        }
    }
  13. Next we will implement the code that does the hard work of requesting data from the API. in order to keep our code reusable, the method takes the URL as a parameter. One thing we must remember however, is that the code we are writing will go inside the (hidden) run method, and thus we cannot return a value in the usual way - instead we will call the updateTextViewmethod at the appropriate time
  14. Inside the fetchData(url:String String) method, and insiade the code that creates a Thread, add the following:
    try {
        val url = URL(urlString)
        val connection: HttpURLConnection = url.openConnection() as HttpURLConnection;
        connection.connectTimeout = 10000
        connection.readTimeout = 10000
        connection.requestMethod = "GET"
        connection.connect()
    
        val responseCode = connection.responseCode
        if (responseCode == HttpURLConnection.HTTP_OK) {
    
            //see https://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string#answer-5445161
            //for description of delimiter - basically this means that the next token the scanner reads is the entire stream
            val scanner = Scanner(connection.inputStream).useDelimiter("\\A")
            val text = if (scanner.hasNext()) scanner.next() else ""
    
            val quote = processQuoteJson(text)
            updateTextView(quote)
        } else {
            //bad response from server
            updateTextView("The server returned an error: $responseCode")
        }
    } catch (e: IOException) {
        //something went wrong
        updateTextView("An error occurred retrieving data from the server")
    }
    Working through the code we first create a HttpURLConnection object. Calling openConnection() on a URL object returns a URLConnection object, but as we are making a request to a HTTP address, the subtype returned should actually be a HttpURLConnection, and, we need access to some of the properties we will cast it as such.Next, we set various properties of the connection, such as how long we are prepared to wait for the connection to be made, how long we'll wait to read the result and the type of HTTP request we are making before connecting. We then verify the response code is okay (i.e. 200 and not something like 404). Assuming it is, we can use the scanner to read the entirety of the steam in one go then pass it to our method to parse the JSON and retrive our quotation. The remaining code handles errors with the server or the resonse.
  15. Finally, add the following code to the getQuote method:
    fetchData("https://ron-swanson-quotes.herokuapp.com/v2/quotes")
  16. Test the application and check that it displays various quotes when the button is pressed

Tasks

Further tasks

  1. Implement functionality so the user can search for Chuck Norris jokes and review the results in a ListView
  2. Allow the user to 'favourite' Chuck Norris jokes they like (hint: dont save the whole joke). Build functionality so the user can view a list of previously favorited jokes