teaching machines

SENG 440: Lecture 8 – AsyncTask

March 14, 2019 by . Filed under lectures, semester1-2019, seng440.

Dear students,

Today we explore how to fetch JSON data from a web service, which we will eventually show in a list. Our app, which we’ll call Lately, will grab headlines from News API. In this lecture, we’ll be grabbing headlines from the BBC. Helping us today is Android’s AsyncTask. We’ll continue this example in the next two lectures, where we’ll add a RecyclerView to show a custom listen and use fragments to show multiple “screens” at a time.

Before we forget, here’s your TODO list for next time:

Let’s start making our app.

NewsAPI

We’ll start by examining the News API web service. We fetch the top headlines from a news source by issuing an HTTP GET request of this URL:

https://newsapi.org/v2/top-headlines?sources=bbc-news&apiKey=KEY

KEY must be replaced by the string that News API gives you when you register. The JSON we get from this URL looks something like this:

{
  "status": "ok",
  "totalResults": 10,
  "articles": [
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "Facebook services suffer widespread outages",
      "description": "Social network acknowledges problems with \"Facebook family of apps\", though the cause is unknown.",
      "url": "http:\/\/www.bbc.co.uk\/news\/technology-47562281",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/3CAB\/production\/_106013551_whbranded_newsubject.jpg",
      "publishedAt": "2019-03-13T20:50:53Z",
      "content": "Image copyrightGetty ImagesImage caption\r\n The Facebook \"family\" of apps was suffering issues, the company confirmed\r\nSeveral of Facebooks products are suffering partial outages, affecting users globally.\r\nFacebooks main product, its Messenger service, image-\u2026 [+3448 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC Sport",
      "title": "Sky Brown: Meet the 10-year-old GB skateboarding sensation - BBC Sport",
      "description": "Skateboarder Sky Brown, 10, tells BBC Sport about her hopes to become Britain's youngest ever summer Olympian.",
      "url": "http:\/\/www.bbc.co.uk\/sport\/av\/get-inspired\/47558160",
      "urlToImage": "https:\/\/m.files.bbci.co.uk\/modules\/bbc-morph-sport-page\/2.15.12\/images\/bbc-sport-logo.png",
      "publishedAt": "2019-03-13T20:37:48.5503711Z",
      "content": null
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "California governor halts executions",
      "description": "A moratorium and a temporary reprieve has been announced for all 737 death row inmates in the state.",
      "url": "http:\/\/www.bbc.co.uk\/news\/world-us-canada-47549422",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/1C11\/production\/_86658170_gettyimages-1304767.jpg",
      "publishedAt": "2019-03-13T20:30:47Z",
      "content": "Image copyrightGetty ImagesImage caption\r\n The last execution in California was carried out in 2006\r\nCalifornia Governor Gavin Newsom has issued a moratorium on executions and a temporary reprieve for all 737 inmates on death row in the state.\r\nThe order issu\u2026 [+2385 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "MPs vote to reject no-deal Brexit",
      "description": "MPs reject a no-deal Brexit by 312 to 308, in a non-binding vote",
      "url": "http:\/\/www.bbc.co.uk\/news\/uk-politics-47562995",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/7A23\/production\/_97176213_breaking_news_bigger.png",
      "publishedAt": "2019-03-13T19:17:28Z",
      "content": "MPs reject a no-deal Brexit by 312 to 308, in a non-binding vote\r\nThis breaking news story is being updated and more details will be published shortly. Please refresh the page for the fullest version.\r\nYou can receive Breaking News on a smartphone or tablet v\u2026 [+89 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "US to ground all Boeing crash aircraft",
      "description": "It follows the lead of many other countries after two Boeing 737 Max 8 crashes in five months.",
      "url": "http:\/\/www.bbc.co.uk\/news\/business-47562727",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/7A23\/production\/_97176213_breaking_news_bigger.png",
      "publishedAt": "2019-03-13T18:48:56Z",
      "content": "President Donald Trump has issued an emergency order to ground all Boeing 737 Max aircraft following the crash of an Ethiopian Airlines jet on Sunday.\r\nThe Federal Aviation Administration had previously held out while many countries banned the aircraft from f\u2026 [+959 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "Exams whizz at centre of US cheat scandal",
      "description": "Prosecutors say Mark Riddell, 36, aced tests for students because he is a \"really smart guy\".",
      "url": "http:\/\/www.bbc.co.uk\/news\/world-us-canada-47544392",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/49C9\/production\/_105998881_1504b6ba-fcd8-42b2-bd70-bfa5a58bb2e7.jpg",
      "publishedAt": "2019-03-13T17:21:12Z",
      "content": "Image copyrightIMG AcademyImage caption\r\n Riddell studied at Harvard, according to an online biography\r\nA 36-year-old director of university entrance exam preparations at a Florida school catering towards elite athletes is at the centre of a US college cheati\u2026 [+3512 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "Lori Loughlin to face college scam charges",
      "description": "The sitcom actress and Desperate Housewives' Felicity Huffman are among 50 charged in a college scam.",
      "url": "http:\/\/www.bbc.co.uk\/news\/world-us-canada-47557056",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/14545\/production\/_105996238_mediaitem105996237.jpg",
      "publishedAt": "2019-03-13T16:04:32Z",
      "content": "Image copyrightGetty ImagesImage caption\r\n Felicity Huffman (left) and Lori Loughlin (right) were among those charged\r\nUS actress Lori Loughlin, of the sitcom Full House, is expected to surrender to authorities in Los Angeles after being charged in a college \u2026 [+7969 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "Deadly shooting at Brazilian school",
      "description": "At least five students and an adult are killed at the school in Suzano, S\u00e3o Paulo state, police say.",
      "url": "http:\/\/www.bbc.co.uk\/news\/world-latin-america-47558141",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/1660C\/production\/_106006619_brazilsaopaulo9760319.png",
      "publishedAt": "2019-03-13T14:01:41Z",
      "content": "At least five students have been killed in a shooting at a school in S\u00e3o Paulo state in Brazil, police say.\r\nOne adult was also killed in the attack at the Professor Raul Brasil state school in Suzano.\r\nTwo \"armed and hooded adolescents\" had carried out the a\u2026 [+163 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "Fears for pupils in Nigeria school collapse",
      "description": "The primary school was on the top floor of a building in the city of Lagos, reports say.",
      "url": "http:\/\/www.bbc.co.uk\/news\/world-africa-47555373",
      "urlToImage": "https:\/\/ichef.bbci.co.uk\/news\/1024\/branded_news\/178CC\/production\/_106006469_127.jpg",
      "publishedAt": "2019-03-13T12:08:34Z",
      "content": "Image caption\r\n There is no official word yet on casualties\r\nA building containing a primary school has collapsed in the Nigerian city of Lagos, with reports saying pupils are trapped in the rubble.\r\nThe school is said to have been on the top floor of the thr\u2026 [+514 chars]"
    },
    {
      "source": {
        "id": "bbc-news",
        "name": "BBC News"
      },
      "author": "BBC News",
      "title": "Reaction as MPs prepare for no-deal vote",
      "description": "Latest updates as MPs prepare to vote on whether to leave the EU without a deal on 29 March.",
      "url": "http:\/\/www.bbc.co.uk\/news\/live\/uk-politics-parliaments-47529293",
      "urlToImage": "https:\/\/c.files.bbci.co.uk\/0C4D\/production\/_105994130_may2.jpg",
      "publishedAt": "2019-03-13T07:38:32.4004145Z",
      "content": "Mrs May's deal was defeated in the Commons on Tuesday evening by 149 votes.Image caption: Mrs May's deal was defeated in the Commons on Tuesday evening by 149 votes.\r\nGood morning.\r\nSo where are we now over Brexit? \r\nLast night, MPs\r\noverwhelmingly threw out \u2026 [+791 chars]"
    }
  ]
}

For our app, we just want to show the titles and jump to their URLs, so we’ll distill this down to a simple model for Headline:

class Headline(val text: String, val url: String)

After you hang out with web services a while, you realize that your code to fabricate the URL tends toward ugly. The parameters need to be encoded. The string gets really long. Abstractions can make ugly beautiful, so let’s write a quick helper method that accepts the path and a map of the query parameters and yields a URL:

fun parameterizeUrl(url: String, parameters: Map<String, String>): URL {
  val builder = Uri.parse(url).buildUpon()
  parameters.forEach { key, value -> builder.appendQueryParameter(key, value) }
  val uri = builder.build()
  return URL(uri.toString())
}

Much of the work is be done by the Uri class. However, Uri is immutable; we need to create a Uri.Builder in order to append the parameters. Once we have a complete Uri, we can convert it to a URL. What a gauntlet.

We create our URL above with this call:

val parameters = mapOf("sources" to "bbc-news", "apiKey" to KEY)
val url = parameterizeUrl("https://newsapi.org/v2/top-headlines", parameters)

Isn’t that beautiful?

Downloading on Android

Let’s pull down this JSON in our app. There are lots of libraries that are happy to help us, but external dependencies have benefits and costs. Let’s stick with stock Android, which provides HttpsUrlConnection, a utility for issuing HTTP requests. We open a URL and then read from an InputStream that feeds the JSON string back to us. We expand this string into a JSONObject structure:

fun getJson(url: URL): JSONObject {
  val connection = url.openConnection() as HttpsURLConnection
  try {
    val json = BufferedInputStream(connection.inputStream).readBytes().toString(Charset.defaultCharset())
    return JSONObject(json)
  } finally {
    connection.disconnect()
  }
}

Let’s call this in our onCreate method to get our headlines:

val parameters = mapOf("sources" to "bbc-news", "apiKey" to KEY)
val url = parameterizeUrl("https://newsapi.org/v2/top-headlines", parameters)
val headlines = getJson(url)

It fails with a NetworkOnMainThreadException exception. What gives?

We are trying to send a request out from our phone, up to the nearest cell tower, bouncing tower to tower, into our provider’s wired network, through cables that span the ocean floor, to some other continent, through the United States intelligence offices, and finally on to its destination, only for the result to come back in a similar manner. And we’re trying to do this on the same thread that listens for button clicks.

Our mobile operating systems do not want us to run blocking code on the UI thread. In the case of network activity, they dynamically enforce this. So, we need to run our download on some other thread. Usually we want to take the results of our network activity and update the UI in some way, so we will need to have some sort of synchronization between our background thread and the UI thread.

Android provides a solution for this background-then-UI pattern called AsyncTask, which we subclass. It automatically starts up a thread for us, which executes our doInBackground method. Then it passes its returned value to our onPostExecute method, which is run on the UI thread. Our subclass for fetching a list of headlines takes on this overall shape:

class HeadlinesDownloader(val activity: MainActivity) : AsyncTask<URL, Void, List<Headline>>() {
  override fun doInBackground(vararg urls: URL): List<Headline> {
    headlines = ... some background computation ...
    return headlines
  }

  override fun onPostExecute(headlines: List<Headline>) {
    super.onPostExecute(headlines)
    ... update the UI ...
  }
}

We move our fetch of the JSON into doInBackground, where it is free to issue network requests. Then we rip up the JSON structure and turn each article into a Headline:

override fun doInBackground(vararg urls: URL): List<Headline> {
  val result = getJson(url)

  val headlinesJson = result.getJSONArray("articles")
  val headlines = (0 until headlinesJson.length()).map { i ->
    val headline = headlinesJson.getJSONObject(i)
    Headline(headline.getString("title"), headline.getString("url"))
  }

  return headlines
}

JSONArray doesn’t really support iteration, so this is a bit of a hack. We create a range of the array’s indices, and then map that into a list of headlines. The return value is fed into onPostExecute, where we update the UI. We haven’t figured out the UI yet, so let’s just pop up a Toast:

override fun onPostExecute(headlines: List<Headline>) {
  super.onPostExecute(headlines)
  Toast.makeText(activity, headlines.joinToString(","), Toast.LENGTH_SHORT).show()
}

Note that if we try to be sneaky and update the UI on our background thread, we will generally get a CalledFromWrongThreadException. (Toast behaves a little differently, raising a different exception.)

We fire off our AsyncTask from onCreate:

HeadlinesDownloader(this).execute(url)

And it fails! The logs tell us that we need to request permission to access the internet. We declare this in the manifest:

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

The INTERNET permission is not considered dangerous and doesn’t need to be approved with a dialog. When we run this again, a very bloated Toast appears.

Context Leaks

The AsyncTask hides a lot of threading and synchronization yuck from us. Like any human invention, it does have a weakness. What if the activity that start the AsyncTask expires, perhaps because of a very innocent configuration change? What then is onPostExecute supposed to do? The UI that it’s responsible for updating may no longer be visible.

One solution is to use a WeakReference to wrap around the activity. A normal strong reference prevents an object from being freed by the garbage collector. A weak reference also hangs on to an object, but if all strong references to it go away, the object will be freed the next time the garbage collector runs. We switch our AsyncTask constructor to receive a strong reference but retain a WeakReference:

class HeadlinesDownloader(activity: MainActivity) : AsyncTask<URL, Void, List<Headline>>() {
  private val context = WeakReference(activity)

  ...
}

In onPostExecute, we only update the UI if the referenced object is not null. We could use a conditional statement for this, but we’re in Kotlin. We use its ? operator and a let block:

override fun onPostExecute(headlines: List<Headline>) {
  super.onPostExecute(headlines)
  context.get()?.let { Toast.makeText(it, headlines.joinToString(","), Toast.LENGTH_SHORT).show() }
}

That’s enough for today. See you next time!

Sincerely,

P.S. It’s time for a haiku, which was more relevant when I thought we were going to talk about lists!

To break free of lists
Work on one thing at a time
Save the rest for now