Updated 10/5/2019 for HTTP-RPC 6.6
As in iOS, Android applications often display thumbnail images alongside other text-based content such as contact names or product descriptions in recycler views. However, the images are not usually delivered with the initial request, but are instead retrieved separately afterward. They are typically downloaded in the background as needed to avoid blocking the UI thread, which would temporarily render the application unresponsive.
For example, consider this sample REST service, which returns a list of simulated photo data:
[
{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"albumId": 1,
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
},
{
"albumId": 1,
"id": 3,
"title": "officia porro iure quia iusto qui ipsa ut modi",
"url": "https://via.placeholder.com/600/24f355",
"thumbnailUrl": "https://via.placeholder.com/150/24f355"
}, ...
]
Each record contains a photo ID, album ID, and title, as well as URLs for both thumbnail and full-size images; for example:

Example Application
A basic user interface for displaying service results in a recycler view is shown below:

The markup for the main activity simply declares an instance of RecyclerView
that will occupy the entire screen:
Item markup is shown below:
Photo Class
A class representing the photo data might be defined as follows:
class Photo (map: Map) {
val id = map["id"] as Int
val title = map["title"] as String
val thumbnailUrl = URL(map["thumbnailUrl"] as String)
}
The constructor extracts property values from the map data provided by the service. This data will be used later to retrieve the thumbnails.
View Holder and Adapter Classes
View holder and adapter classes are used to produce and configure individual photo item views. Item data is stored in a list of Photo
instances, and previously loaded thumbnail images are stored in a map that associates Bitmap
instances with photo IDs:
class MainActivity : AppCompatActivity() {
// Photo view holder
inner class PhotoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
}
// Photo adapter
inner class PhotoAdapter : RecyclerView.Adapter() {
...
}
// Photo list
var photos: List? = null
// Thumbnail cache
val photoThumbnails = HashMap()
...
}
Main Activity
When the activity is created, the main view is loaded and the recycler view configured:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = PhotoAdapter()
}
The photo list is loaded the first time the activity resumes. An instance of WebServiceProxy
(provided by the open-source HTTP-RPC project) is used to retrieve the data. If the call succeeds, the response data is transformed into a list of Photo
objects, and the recycler view is refreshed. Otherwise, an error message is logged:
override fun onResume() {
super.onResume()
// Load photo data
if (photos == null) {
doInBackground({
val webServiceProxy = WebServiceProxy("GET", URL("https://jsonplaceholder.typicode.com/photos"))
val photos = webServiceProxy.invoke { inputStream, _, _ -> ObjectMapper().readValue(inputStream, List::class.java) }
photos.map { @Suppress("UNCHECKED_CAST") Photo(it as Map) }
}) { activity, result ->
result.onSuccess { value ->
photos = value
activity?.recyclerView?.adapter?.notifyDataSetChanged()
}.onFailure { exception ->
println(exception.message)
}
}
}
}
Background Task Execution
The photo list is loaded asynchronously using doInBackground()
, an extension method added to the Activity
class. It provides a lambda-based wrapper around AsyncTask
that allows the result handler to safely dereference the activity without leaking memory. Because the activity type is a generic, the callback can access the members of the activity without a cast:
fun <A> A.doInBackground(task: () -> R, resultHandler: (activity: A?, result: Result) -> Unit) {
BackgroundTask(this, task, resultHandler).execute()
}
class BackgroundTask<A>(activity: A,
private val task: () -> R,
private val resultHandler: (activity: A?, result: Result) -> Unit
) : AsyncTask() {
private val activityReference = WeakReference<A>(activity)
private var value: R? = null
private var exception: Exception? = null
override fun doInBackground(vararg params: Unit?): R? {
try {
value = task()
} catch (exception: Exception) {
this.exception = exception
}
return value
}
override fun onPostExecute(value: R?) {
resultHandler(activityReference.get(), if (exception == null) {
Result.success(value!!)
} else {
Result.failure(exception!!)
})
}
}
Item Content
Item content is managed by the view holder and adapter classes. The Photo
instance is retrieved from the list and used to configure the item view:
// Photo view holder
inner class PhotoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.findViewById(R.id.imageView)
val textView: TextView = view.findViewById(R.id.textView)
}
// Photo adapter
inner class PhotoAdapter : RecyclerView.Adapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoViewHolder {
return PhotoViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_photo, parent, false))
}
override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
val photo = photos!![position]
// Attempt to load image from cache
val thumbnail = photoThumbnails[photo.id]
holder.imageView.setImageBitmap(thumbnail)
if (thumbnail == null) {
...
}
holder.textView.text = photo.title
}
override fun getItemCount(): Int {
return photos?.size ?: 0
}
}
If the thumbnail image is already available in the cache, it is used to populate the item’s image view. Otherwise, it is asynchronously loaded from the server and added to the cache as shown below:
if (thumbnail == null) {
// Request image
doInBackground({
val webServiceProxy = WebServiceProxy("GET", photo.thumbnailUrl)
webServiceProxy.invoke { inputStream, _, _ -> BitmapFactory.decodeStream(inputStream) }
}) { activity, result ->
// Add image to cache and update view holder, if visible
result.onSuccess { value ->
photoThumbnails[photo.id] = value
val viewHolder = activity?.recyclerView?.findViewHolderForAdapterPosition(position) as? PhotoViewHolder
viewHolder?.imageView?.setImageBitmap(value)
}
}
}
If the item is still visible when the image request returns, its image view is updated immediately. Otherwise, it will be updated the next time the item is shown.
More Information
Complete source code for this example can be found here. For more information, see the HTTP-RPC README.