Dynamically Loading RecyclerView Images in Android

4/27/2019 Updated for Kilo 1.2

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:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"/>
</LinearLayout>

Item markup is shown below:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="16dp">
    <ImageView android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginLeft="8dp"/>
</LinearLayout>

Photo Class

A class representing the photo data might be defined as follows:

class Photo (map: Map<String, Any>) {
    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<PhotoViewHolder>() {
        ...
    }

    // Photo list
    var photos: List<Photo>? = null

    // Thumbnail cache
    val photoThumbnails = HashMap<Int, Bitmap>()

    ...
}

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 Kilo 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<String, Any>) }
        }) { 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: Activity, R> A.doInBackground(task: () -> R, resultHandler: (activity: A?, result: Result<R>) -> Unit) {
    BackgroundTask(this, task, resultHandler).execute()
}

class BackgroundTask<A: Activity, R>(activity: A,
    private val task: () -> R,
    private val resultHandler: (activity: A?, result: Result<R>) -> Unit
) : AsyncTask<Unit, Unit, R?>() {
    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<PhotoViewHolder>() {
    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 Kilo README.

Do You Need PUT and PATCH?

Conventional wisdom says that REST APIs should be implemented as follows:

  • GET – read
  • POST – add
  • PUT/PATCH – modify
  • DELETE – remove

This mostly works well. Query parameters can be used to supply arguments for GET and DELETE operations. POSTs can use either URL-encoded or multipart form data, standard encodings supported by nearly all HTTP clients.

However, it doesn’t work quite as well for PUT or PATCH. PUT has no standard encoding, and requires the entire resource to be sent in the payload. PATCH was introduced as a workaround to this limitation, but it also lacks a standard encoding, and is not supported by all clients (notably Java).

However, the POST method can also be used to modify resources. The semantics of POST are less strict than PUT, so it can support partial updates, like PATCH. Further, the same encoding used for creating resources (URL-encoded or multipart) can also be used for updates.

For example:

  • POST /products – add a new resource to the “products” collection using the data specified in the request body
  • POST /products/101 – update the existing resource with ID 101 in the products collection using the (possibly partial) data specified in the request body

This approach works particularly well when resources are backed by relational database tables. An “add” POST maps directly to a SQL INSERT operation, and a “modify” POST translates to a SQL UPDATE. The key/value pairs in the body (whether URL-encoded or multipart) can be mapped directly to the table columns.

The approach also supports bulk inserts and updates. POSTing a URL-encoded payload works well for individual records, but JSON, CSV, or XML could easily be used to add or update multiple records at a time.

So, do you really need PUT and PATCH? Given that POST is more flexible, better supported, and can handle both create and update operations, I’d say no. Please share your thoughts in the comments!

Creating a Simple Web Service using HTTP-RPC and Kotlin

HTTP-RPC is an open-source framework for implementing RESTful and REST-like web services in Java. It is extremely lightweight and requires only a Java runtime environment and a servlet container. The entire framework is distributed as a single JAR file that is about 50KB in size, making it an ideal choice for applications where a minimal footprint is desired.

WebService

HTTP-RPC’s WebService type provides an abstract base class for REST-based web services. It extends the similarly abstract HttpServlet class provided by the servlet API.

Service operations are defined by adding public methods to a concrete service implementation. Methods are invoked by submitting an HTTP request for a path associated with a servlet instance. Arguments are provided either via the query string or in the request body, like an HTML form. WebService converts the request parameters to the expected argument types, invokes the method, and writes the return value to the output stream as JSON.

For example, the following class might be used to implement a simple math service:

@WebServlet(urlPatterns={"/math/*"})
public class MathServlet extends WebService {
    @RequestMethod("GET")
    @ResourcePath("/sum")
    public double getSum(double a, double b) {
        return a + b;
    }
}

The following request would cause the method to be invoked, and the service would return the value 6 in response:

GET /math/sum?a=2&b=4

Kotlin

In addition to Java, HTTP-RPC web services can be implemented in Kotlin, a strongly typed, modern programming language that targets the JVM. Kotlin offers a number of features that make it a compelling alternative to Java for server-side development:

  • Type inference
  • Properties
  • Named method parameters
  • Default argument values
  • Data classes
  • Optionals
  • Extensions

For example, the following service (written in Kotlin) provides some basic information about the host system:

@WebServlet(urlPatterns = ["/system-info/*"], loadOnStartup = 1)
class SystemInfoService : WebService() {
    class SystemInfo(
        val hostName: String,
        val hostAddress: String,
        val availableProcessors: Int,
        val freeMemory: Long,
        val totalMemory: Long
    )

    @RequestMethod("GET")
    fun getSystemInfo(): SystemInfo {
        val localHost = InetAddress.getLocalHost()
        val runtime = Runtime.getRuntime()

        return SystemInfo(
            localHost.hostName,
            localHost.hostAddress,
            runtime.availableProcessors(),
            runtime.freeMemory(),
            runtime.totalMemory()
        )
    }
}

Data returned by the service might look like this:

{
  "hostName": "vm.local",
  "hostAddress": "192.168.1.12",
  "availableProcessors": 4,
  "freeMemory": 222234120,
  "totalMemory": 257949696
}

As with Java-based HTTP-RPC services, API documentation can be accessed by appending “?api” to the service URL:

GET /system-info?api

The response might look something like this:

/system-info

  GET () -> SystemInfo
  

SystemInfo

  {
    hostAddress: string,
    hostName: string,
    availableProcessors: integer,
    freeMemory: long,
    totalMemory: long
  }
  

Summary

Although this is a somewhat contrived example, it demonstrates one of Kotlin’s main advantages: brevity. The SystemInfo data class would have been considerably more verbose in Java. More complex services would see similar benefits.

Complete source code for the examples can be found here. For more information, see the HTTP-RPC README.