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.
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."]
android
element so that ViewBinding is available:buildFeatures {
viewBinding = true
}
private lateinit var binding: ActivityMainBinding
setContentView()
in the onCreate method with the following binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
fun getQuote(){
}
<uses-permission android:name="android.permission.INTERNET" />
private fun processQuoteJson(jsonString: String): String {
val jsonArray = JSONArray(jsonString)
return jsonArray[0].toString()
}
Add the import statement for org.json when promptedJSONObject
and JSONArray
classes to access the data you need. Take some time to review the able on the JSON object documentation and JSON Array documentationrun
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.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.
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
}
}
run
method, and thus we cannot return a value in the usual way - instead we will call the updateTextView
method at the appropriate timefetchData(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.getQuote
method:
fetchData("https://ron-swanson-quotes.herokuapp.com/v2/quotes")