Lecture 4 Data-Driven Views

A previous lecture discussed how to use Views to display content and support user interaction. This lecture extends those concepts and presents techniques for creating data-driven views—views that can dynamically present a data model in the form of a scrollable list. It also explains how to access data on the web using the Volley library. Overall, this process demonstrates a common way to connect the user interface for the app (defined as XML) with logic and data controls (defined in Java), following the Model-View-Controller architecture found throughout the Android framework.

This lecture references code found at https://github.com/info448/lecture04-lists.

4.1 ListViews and Adapters

In particular, this lecture discussed how to utilize a ListView12, which is a ViewGroup that displays a scrollable list of items! A ListView is basically a LinearLayout inside of a ScrollView (which is a ViewGroup that can be scrolled). Each element within the LinearLayout is another View (usually a Layout) representing a particular item in a list.

But the ListView does extra work beyond just nesting Views: it keeps track of what items are already displayed on the screen, inflating only the visible items (plus a few extra on the top and bottom as buffers). Then as the user scrolls, the ListView takes the disappearing views and recycles them (altering their content, but not re-inflating from scratch) in order to reuse them for the new items that appear. This lets it save memory, provide better performance, and overall work more smoothly. See this tutorial for diagrams and further explanation of this recycling behavior.

  • Note that a more advanced and flexible version of this behavior is offered by the RecyclerView class, which works in mostly the same way but requires a few extra steps to set up. See also this guide for more details.

The ListView control uses a Model-View-Controller (MVC) architecture. This is a design pattern common to UI systems which organizes programs into three parts:

  1. The Model, which is the data or information in the system
  2. The View, which is the display or representation of that data
  3. The Controller, which acts as an intermediary between the Model and View and hooks them together.

The MVC pattern can be found all over Android. At a high level, the resources provide models and views (separately), while the Java Activities act as controllers.

Fun fact: The Model-View-Controller pattern was originally developed as part of the Smalltalk language, which was the first Object-Oriented language!

Thus in order to utilize a ListView, we’ll have some data to be displayed (the model), the views (layouts) to be shown, and the ListView itself will connect these together act as the controller. Specifically, the ListView is a subclass of AdapterView, which is a View backed by a data source—the AdapterView exists to hook the View and the data together (just as a controller should).

  • There are other AdapterViews as well. For example, GridView works exactly the same way as a ListView, but lays out items in a scrollable grid rather than a scrollable list.

In order to use a ListView, we need to get the pieces in place:

  1. First we specify the model: some raw data. We will start with a simple list of Strings, filling it with placeholder data:

    val data = mutableListOf<String>()
    for(i in 99 downTo 1){
        data.add(i.toString() + " bottles of beer on the wall")
    }   

    While we normally should define such hard-coded data as an XML resource, we’ll create it dynamically for testing (and to make it changeable later!). Using a List instead of an Array makes it easier to construct (and works the same way).

  2. Next we specify the view: a View to show for each datum in the list. Define an XML layout resource for that (list_item is a good name and a common idiom).

    We don’t really need to specify a full Layout (though we could if we wanted): just a basic TextView will suffice. Have the width match_parent and the height wrap_content. Don’t forget an id!

    <!-- need to include the XML namespace (xmlns) so the `android` namespace validates -->
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/txtItem"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    To make it look better, you can specify android:minHeight="?android:attr/listPreferredItemHeight" (using the framework’s preferred height for lists), and some center_vertical gravity. The android:lines property is also useful if you need more space.

  3. Finally, we specify the controller: the ListView itself. Add that item to the Activity’s Layout resource (practice: what should its dimensions be?)

To finish the controller ListView, we ned to provide it with an Adapter13 which will connect the model to the view. The Adapter does the “translation” work between model and view, performing a mapping from data types (e.g., a String) and View types (e.g., a TextView).

Specifically, we will use an ArrayAdapter, which is one of the simplest Adapters to use (and because we have an array of data!) An ArrayAdapter creates Views by calling .toString() on each item in the array, and setting that String as the content of a TextView!

//kotlin
val adapter = ArrayAdapter<String>(this,
    R.layout.list_item_layout, R.layout.list_item_txtView, myStringArray);
  • Note the parameters of the constructor: a Context to access resources, the layout resource to use for each item, the TextView within that layout (the target of the mapping), and the data array (the source of the mapping). Also note that this instance utilizes generics: we’re using an array of Strings (as opposed to an array of Dogs or some other type).

We acquire a reference to the ListView with findViewById(), and call ListView#setAdapter() to attach the adapter to that controller.

//kotlin 
val listView = findViewById<ListView>(R.id.list_view)
listView.setAdapter(adapter)

And that’s all that is needed to create a scrollable list of data! To track the process: the Adapter will go through each item in the model, and “translate” that item into the contents of a View. These Views will then be displayed in a scrollable list.

Each item in this list is selectable (can be given an onClick callback). This allows us to click on any item in order to (for example) get more details about the item. Utilize the AdapterView#setOnItemClickListener(OnItemClickListener) function to register the callback.

  • The position parameter in the onItemClick() callback is the index of the item which was clicked. Use (Type)parent.getItemAtPosition(position) to access the data value associated with that View.

Additionally, each item does have an individual layout, so you can customize these appearances (e.g., if our layout also wanted to include pictures). See this tutorial for an example on making a custom adapter to fill in multiple Views with data from a list!

And remember, a GridView is basically the same thing (in fact, we can just change over that and have everything work, if we use polymorphism!) Note that the data type for the AdapterView is a little thorny if you want to be generic about it:

val adapterView = findViewById<AdapterView<ArrayAdapter<String>>>(R.id.list_view)

4.2 Networking with Volley

A list with hard-coded data isn’t very useful. It would be better if that data could be accessed dynamically, such as downloaded from the Internet!

There are a couple of different ways to programmatically send network requests from an Android application. The “lowest level” is to utilize the HttpURLConnection API. With this API, you call methods to open a connection to a URL and then to send an HTTP Request to that location. The response is returned as an InputStream, which you need to “read” byte by byte in order to reconstruct the received data (e.g., to make it back into a String). See this example for details.

While this technique is effective, it can be tedious to implement. Moreover, downloading network data can take a while—and these network method calls are synchronous and blocking, so will prevent other code from running while it downloads—including code that enables the user interface! Such block will lead to the infamous “Application not responding” (ANR) error. While it is possible to send such requests asynchronously on a background thread to avoid blocking, that requires additional overhead work. See the Services Lecture for more details.

To solve these problems with less work, it can be be more effective to utilize an external library that lets us abstract away this process and just talk about making network requests and getting data back from them. (This is similar to how in web programming the fetch API abstracts the opaque XMLHttpRequest object). In particular, this lecture will introduce the Volley library, which is an external library developed and maintained by Google. It provides a number of benefits over a more “manual” approach, including handling multiple concurrent requests and enabling the caching of downloaded data. It also causes network requests to be handled asynchronously on a background thread without any additional effort!

Volley’s main “competitor” is the Retrofit library produced by Square. While Retrofit is usually faster at processing downloaded data, Volley has built-in support for handling images (which will be useful in the future), and has a slightly more straightforward interface.

Using Volley

Because Volley is an external library (it isn’t built into the Android framework), you need to explicitly download and include it in your project. Luckily, we can use the Gradle build system to do this for us by listing Volley as a dependency for the project. Inside the app-level build.gradle file, add the following line inside the dependencies list:

compile 'com.android.volley:volley:1.0.0'

This will tell Android that it should download and include version 1.0.0 of the Volley library when it builds the app. Hit the “Sync” button to update and rebuild the project.

  • External libraries will be built into your app, increasing the file size of the compiled .apk (there is more code!). Though this won’t cause any problems for us, it’s worth keeping in mind as you design new apps.

Once you have included Volley as a dependency, you will have access to the classes and API to use in your code.

In order to request data with Volley, you will need to instantiate a Request object based on the type of data you will be downloading: a StringRequest for downloading text data, a JsonRequest for downloading JSON formatted data, or an ImageRequest for downloading images.

The constructor for StringRequest, for example, takes 4 arguments:

  1. A constant representing the HTTP method (verb) to use. E.g., Request.Method.GET
  2. The URL to send the request to (as a String)
  3. A Response.Listener object, which defines a callback function to be executed when the response is received.
  4. A Response.ErrorListener object, which defines a callback function to be executed in case of an error.

Because the last two listener objects are usually defined with anonymous classes, this can make the Request constructor look more complicated than it is (though Kotlin’s use of lambdas helps):

//java
//silly example: get 20 random dinosaur names
String url = "http://dinoipsum.herokuapp.com/api/?format=text&words=20&paragraphs=1";

Request myRequest = new StringRequest(Request.Method.GET, url,
        new Response.Listener<String>() {
            public void onResponse(String response) {
                Log.v(TAG, response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e(TAG, error.toString());
            }
        });
//kotlin
val request = StringRequest(Request.Method.GET, urlString,
        //callback for success
        Response.Listener { response ->
            Log.v(TAG, response)
        }, 
        //callback for failure
        Response.ErrorListener { error -> 
            Log.e(TAG, error.toString()) 
        })

(Also note that the Response.Listener is a generic class, in which we specify the format we’re expecting the response to come back in. This is String for a StringRequest, but would be e.g., JSONObject for a JsonObjectRequest). Kotlin handles some of this for us.

In order to actually send this Request, you need a RequestQueue, which acts like a “dispatcher” and handles sending out the Requests on background threads and otherwise managing the network operations. We create a dispatcher with default parameters (for networking and caching) using the Volley.newRequestQueue() factory method:

//kotlin
val requestQueue: requestQueue = Volley.newRequestQueue(this.applicationContext);

The factory method takes in a Context for managing the cache; the best practice is to use the application’s Context so it isn’t dependent on a single Activity.

Once you have a RequestQueue, you can add your request to that in order to “send” it:

requestQueue.add(myRequest);

If you test this code, you’ll notice that it doesn’t work! The program will crash with a SecurityException.

As a security feature, Android apps by default have very limited access to the overall operating system (e.g., to do anything other than show a layout). An app can’t use the Internet (which might consume people’s data plans!) without explicit permission from the user. This permission is given by the user at install time.

In order to get permission, the app needs to ask for it (“Mother may I…?”). We do that by declaring that the app uses the Internet in the AndroidManifest.xml file (which has all the details of our app!)

<uses-permission android:name="android.permission.INTERNET"/>
<!-- put this ABOVE the <application> tag -->

Note that Marshmallow introduced a new security model in which users grant permissions at run-time, not install time, and can revoke permissions whenever they want. To handle this, you need to add code to request “dangerous” permissions (like Location, Phone, or SMS access) each time you use it. This process is discussed in the Files and Permissions Lecture. Using the Internet is not a dangerous permission, so only requires the permission declaration in the Manifest.

Once we’ve requested permission (and have been granted that permission by virtue of the user installing our application), we can finally connect to the Internet to download data. We can log out the request results to prove we got it!

Of course, we’d like to display that data on the screen (rather than just log it out). That is, we want to put it into the ListView, meaning that we need to feed it back into the Adapter (which works to populate the Views).

  • First, clear out any previous data items in the adapter using adapter.clear().
  • Then use adapter.add() or (adapter.addAll()) to add each of the new data items to the Adapter’s model! Note that you may need to do data parsing on the response body, such as splitting a String or constructing an array or ArrayList out of JSON data.
  • You can call notifyDataSetChanged() on the Adapter to make sure that the View knows the data has changed, but this method is already called by the .add() method so isn’t necessary in this situation.

You can use the JsonObjectRequest class to download data as a JSON Object rather than a raw String. JSON Objects and Arrays can be converted into Java Objects/Arrays using two classes: JSONObject and JSONArray. The constructors for each of these classes take a JSON String, and you can call the getJSONArray(key) and getJSONObject(key) in order to get nested objects and arrays from inside a JSONObject or JSONArray.

RequestQueue Singletons

If you are going to make multiple network requests for your application (which you usually will for anything of a reasonable size), it is wasteful to repeatedly instantiate new RequestQueue objects—these can take up significant memory and step on each others toes.

Instead, the best practice is to use the Singleton Design Pattern to ensure that your entire application only uses a single RequestQueue.

To do this, you will want to create an entire class (e.g., VolleyService) that will only ever be instantiated once (it will be a “singleton”). Since the Volley RequestQueue is controlled by this singleton, it means there will only ever be one RequestQueue.

//java
public class RequestSingleton { //make static if an inner class!

    //the single instance of this singleton
    private static RequestSingleton instance;

    private RequestQueue requestQueue = null; //the singleton's RequestQueue

    //private constructor; cannot instantiate directly
    private RequestSingleton(Context ctx){
        //create the requestQueue
        this.requestQueue = Volley.newRequestQueue(ctx.getApplicationContext());
    }

    //call this "factory" method to access the Singleton
    public static RequestSingleton getInstance(Context ctx) {
        //only create the singleton if it doesn't exist yet
        if(instance == null){
            instance = new RequestSingleton(ctx);
        }

        return instance; //return the singleton object
    }

    //get queue from singleton for direct action
    public RequestQueue getRequestQueue() {
        return this.requestQueue;
    }

    //convenience wrapper method
    public <T> void add(Request<T> req) {
        requestQueue.add(req);
    }
}
//kotlin
private class VolleyService 
    private constructor(ctx: Context) { //private constructor; cannot instantiate directly
    
    companion object { //to hold the shared instances
        private var instance: VolleyService? = null //the single instance of this singleton

        //call this "factory" method to access the Singleton
        fun getInstance(ctx: Context): VolleyService {
            //only create the singleton if it doesn't exist yet
            if (instance == null) {
                instance = VolleyService(ctx)
            }

            return instance as VolleyService //force casting
        }
    }

    //from Kotlin docs
    val requestQueue: RequestQueue by lazy { //instantiate once needed
        Volley.newRequestQueue(ctx.applicationContext) //return the context-based requestQueue
    }

    //convenience wrapper method
    fun <T> add(req: Request<T>) {
        requestQueue.add(req)
    }
}

See also the JetBrains Kotlin Demo of Volley for further examples

This structure will let you make multiple network requests from multiple components of your app, but without trying to have multiple “dispatchers” taking up memory.

Downloading Images

In addition to downloading text or JSON data via HTTP requests, Volley is also able to support downloading images to be shown in your app.

In general, handling images in Android is a difficult task. Images are large files (often multiple megabytes in size) that may require extensive and lingering data transfer to download and require processor-intensive decoding in order to be displayed. Since mobile devices are resource constrained (particularly in memory), trying to download and display lots of images—say in a scrollable list—can quickly cause problems. See Handling Bitmaps and Loading Large Bitmaps Efficiently for some examples of the complexity needed to work with images.

Volley provides some support to make downloading and processing images easier. In particular, it provides built-in support for network management (so that data transfers are most efficiently optimized), caching (so you don’t try to download the same image twice), and for easily displaying network-loaded images.

The other popular image-management libraries are Glide, Square’s Picasso, and Facebook’s Fresco. Google recommends using Glide for doing complex image work. However, if Volley’s image loading is sufficient for your task, that allows you to only need to work with a single library and networking queue.

In order to effectively download images with Volley, you need to set up an ImageLoader. This object will handling downloading remote images as well as caching them for the future.

To instantiate an ImageLoader, you need to provide a RequestQueue as well as an ImageCache object that represents how image data should be cached (e.g., in memory, on disk, etc.). The Volley documentation suggests using a LruCache object for caching to memory (though you can use a DiskBasedCache as well):

//kotlin
//instantiate the image loader (from Kotlin docs)
//params are the requestQueue and the Cache
val imageLoader: ImageLoader by lazy { //only instantiate when needed
    ImageLoader(requestQueue,
            object : ImageLoader.ImageCache { //anonymous cache object
                private val cache = LruCache<String, Bitmap>(20)
                override fun getBitmap(url: String): Bitmap? {
                    return cache.get(url)
                }
                override fun putBitmap(url: String, bitmap: Bitmap) {
                    cache.put(url, bitmap)
                }
            })
}
  • It’s a good idea to make this ImageLoader an instance variable of the VolleyService.

Once you have the ImageLoader, you can use it to download an image by calling it’s get() method, specifying a callback listener that will be executed when the image is finished downloading.

However, you almost always want to download an image in order to display it. Volley makes this easy by providing a customized View called NetworkImageView. A NetworkImageView is able to handle the downloading of its source image on its own, integrating that process into the Activity’s lifecycle (e.g., so it won’t download when the View isn’t displayed).

You declare a NetworkImageView in the layout XML in the same way you would specify an ImageView:

<com.android.volley.toolbox.NetworkImageView
    android:id="@+id/img_remote"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:scaleType="fitXY"
    />
  • The android:scaleType attribute indicates how the image should be scaled to fit the View.

In order to download an image into this View, you call the setImageUrl() method on the View from within your Java, specify the image url to load into the View and the ImageLoader to use for this network access:

val imageView = findViewById<NetworkImageView>(R.id.img_remote)
netView.setImageUrl("https://ischool.uw.edu/fb-300x300.png", imageLoader);

And that will let you download and show images from the Internet (without needing to make them into a drawable resource)!