Dynamically Loading Recycler View Images in Android

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": "http://placehold.it/600/92c952",
    "thumbnailUrl": "http://placehold.it/150/92c952"
  },
  {
    "albumId": 1,
    "id": 2,
    "title": "reprehenderit est deserunt velit ipsam",
    "url": "http://placehold.it/600/771796",
    "thumbnailUrl": "http://placehold.it/150/771796"
  },
  {
    "albumId": 1,
    "id": 3,
    "title": "officia porro iure quia iusto qui ipsa ut modi",
    "url": "http://placehold.it/600/24f355",
    "thumbnailUrl": "http://placehold.it/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:

Photo Class

A class representing this data might be defined as follows:

public class Photo {
    private Number id;
    private Number albumId;
    private String title;
    private URL url;
    private URL thumbnailUrl;

    public Photo(Map<String, ?> map) {
        id = valueAt(map, "id");
        albumId = valueAt(map, "albumId");
        title = valueAt(map, "title");

        String url = valueAt(map, "url");
        String thumbnailUrl = valueAt(map, "thumbnailUrl");

        try {
            this.url = new URL(url);
            this.thumbnailUrl = new URL(thumbnailUrl);
        } catch (MalformedURLException exception) {
            throw new RuntimeException(exception);
        }
    }

    public int getId() {
        return id.intValue();
    }

    public int getAlbumId() {
        return albumId.intValue();
    }

    public String getTitle() {
        return title;
    }

    public URL getUrl() {
        return url;
    }

    public URL getThumbnailUrl() {
        return thumbnailUrl;
    }
}

The constructor extracts property data from a map instance using the static valueAt() method of the HTTP-RPC WebServiceProxy class. Instances of this class will be used later to retrieve the photo list as well as the thumbnails themselves.

Main Activity Class

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/recycler_view"
        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/image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView android:id="@+id/text_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginLeft="8dp"/>
</LinearLayout>

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. The executor service will be used later by WebServiceProxy to execute service requests:

public class MainActivity extends AppCompatActivity {
    // Photo view holder
    private class PhotoViewHolder extends RecyclerView.ViewHolder {
        ...
    }

    // Photo adapter
    private class PhotoAdapter extends RecyclerView.Adapter<PhotoViewHolder> {
        ...
    }

    private RecyclerView recyclerView;

    private ArrayList<Photo> photos = null;

    private HashMap<Integer, Bitmap> photoThumbnails = new HashMap<>();

    private ExecutorService executorService = Executors.newSingleThreadExecutor();

    private static String TAG = MainActivity.class.getName();

    ...
}

When the activity is created, the view is loaded and the recycler view configured:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    recyclerView = (RecyclerView)findViewById(R.id.recycler_view);

    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    recyclerView.setAdapter(new PhotoAdapter());
}

The photo list is loaded the first time the activity resumes. An instance of WebServiceProxy is used to retrieve the data. If the call succeeds, the response data (a list of map objects) is transformed into a list of Photo objects and the recycler view is refreshed. Otherwise, an error message is logged:

@Override
protected void onResume() {
    super.onResume();

    // Load photo data
    if (photos == null) {
        URL serverURL;
        try {
            serverURL = new URL("https://jsonplaceholder.typicode.com");
        } catch (MalformedURLException exception) {
            throw new RuntimeException(exception);
        }

        WebServiceProxy serviceProxy = new WebServiceProxy(serverURL, executorService) {
            @Override
            protected void dispatchResult(Runnable command) {
                runOnUiThread(command);
            }
        };

        serviceProxy.invoke("GET", "/photos", (List<Map<String, ?>> result, Exception exception) -> {
            if (exception == null) {
                photos = new ArrayList<>(result.size());

                for (Map<String, ?> value : result) {
                    photos.add(new Photo(value));
                }

                recyclerView.getAdapter().notifyDataSetChanged();
            } else {
                Log.e(TAG, exception.getMessage());
            }
        });
    }
}

Note that the service proxy overrides dispatchResult() to ensure that the response handler is executed on the UI thread.

Item content is managed by the view holder and adapter classes. The corresponding Photo instance is retrieved from the photos list and used to configure the item view:

private class PhotoViewHolder extends RecyclerView.ViewHolder {
    private ImageView imageView;
    private TextView textView;

    public PhotoViewHolder(View view) {
        super(view);

        imageView = (ImageView)view.findViewById(R.id.image_view);
        textView = (TextView)view.findViewById(R.id.text_view);
    }
}

private class PhotoAdapter extends RecyclerView.Adapter<PhotoViewHolder> {
    @Override
    public PhotoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new PhotoViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photo, parent, false));
    }

    @Override
    public void onBindViewHolder(PhotoViewHolder holder, int position) {
        Photo photo = photos.get(position);

        // Attempt to load image from cache
        Bitmap thumbnail = photoThumbnails.get(photo.getId());

        if (thumbnail == null) {
            // Image was not found in cache; load it from the server

            ...
        } else {
            holder.imageView.setImageBitmap(thumbnail);
        }

        holder.textView.setText(photo.getTitle());
    }

    @Override
    public int getItemCount() {
        return (photos == null) ? 0 : photos.size();
    }
}

If the thumbnail image is already available in the cache, it is used to populate the item's image view. Otherwise, it is loaded from the server and added to the cache as shown below. If the item is still visible when the image request returns, it is updated immediately:

// Image was not found in cache; load it from the server
URL thumbnailUrl = photo.getThumbnailUrl();

URL serverURL;
try {
    serverURL = new URL(String.format("%s://%s", thumbnailUrl.getProtocol(), thumbnailUrl.getHost()));
} catch (MalformedURLException exception) {
    throw new RuntimeException(exception);
}

WebServiceProxy serviceProxy = new WebServiceProxy(serverURL, executorService) {
    @Override
    protected Object decodeImageResponse(InputStream inputStream, String imageType) {
        return BitmapFactory.decodeStream(inputStream);
    }

    @Override
    protected void dispatchResult(Runnable command) {
        runOnUiThread(command);
    }
};

serviceProxy.invoke("GET", thumbnailUrl.getPath(), (Bitmap result, Exception exception) -> {
    photoThumbnails.put(photo.getId(), result);

    // If view is still visible, update image
    PhotoViewHolder viewHolder = (PhotoViewHolder)recyclerView.findViewHolderForAdapterPosition(position);

    if (viewHolder != null) {
        viewHolder.imageView.setImageBitmap(result);
    }
});

Summary

This article provided an overview of how images can be dynamically loaded as needed to populate recycler views in Android. For more information, please see the HTTP-RPC project on GitHub.

Dynamically Loading Table View Images in iOS

iOS applications often display thumbnail images in table views alongside other text-based content such as contact names or product descriptions. However, these images are not usually delivered with the initial request, but must instead be retrieved separately afterward. They are typically downloaded in the background as needed to avoid blocking the main thread, which would temporarily render the user interface unresponsive.

For example, consider this REST service, which returns a list of simulated photo data:

[
  {
    "albumId": 1,
    "id": 1,
    "title": "accusamus beatae ad facilis cum similique qui sunt",
    "url": "http://placehold.it/600/92c952",
    "thumbnailUrl": "http://placehold.it/150/92c952"
  },
  {
    "albumId": 1,
    "id": 2,
    "title": "reprehenderit est deserunt velit ipsam",
    "url": "http://placehold.it/600/771796",
    "thumbnailUrl": "http://placehold.it/150/771796"
  },
  {
    "albumId": 1,
    "id": 3,
    "title": "officia porro iure quia iusto qui ipsa ut modi",
    "url": "http://placehold.it/600/24f355",
    "thumbnailUrl": "http://placehold.it/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:

Photo Class

A class representing this data might be defined as follows:

class Photo: NSObject {
    var id: Int = 0
    var albumId: Int = 0
    var title: String?
    var url: URL?
    var thumbnailUrl: URL?

    init(dictionary: [String: Any]) {
        super.init()

        setValuesForKeys(dictionary)
    }

    override func setValue(_ value: Any?, forKey key: String) {
        switch key {
        case #keyPath(url):
            url = URL(string: value as! String)

        case #keyPath(thumbnailUrl):
            thumbnailUrl = URL(string: value as! String)

        default:
            super.setValue(value, forKey: key)
        }
    }
}

Since instances of this class will be populated using dictionary values returned by the web service, an initializer that takes a dictionary argument is provided. The setValuesForKeys(_:) method of NSObject is used to map dictionary entries to property values. The ID and title properties are handled automatically by this method; the class overrides setValue(_:forKey:) to convert the URLs from strings into actual URL instances.

View Controller Class

A basic user interface for displaying service results in a table view is shown below:

Row data is stored in an array of Photo instances. Previously loaded thumbnail images are stored in a dictionary that associates UIImage instances with photo IDs:

class ViewController: UITableViewController {
    // Row data
    var photos: [Photo]!

    // Image cache
    var thumbnailImages: [Int: UIImage] = [:]

    ...    
}

The photo list is loaded the first time the view appears. The WSWebServiceProxy class of the open-source HTTP-RPC library is used to retrieve the data. If the call succeeds, the response data (an array of dictionary objects) is transformed into an array of Photo objects and the table view is refreshed. Otherwise, an error message is displayed:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    // Load photo data
    if (photos == nil) {
        let serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: URL(string: "https://jsonplaceholder.typicode.com")!)

        serviceProxy.invoke("GET", path: "/photos") { result, error in
            if (error == nil) {
                let photos = result as! [[String: Any]]

                self.photos = photos.map({
                    return Photo(dictionary: $0)
                })

                self.tableView.reloadData()
            } else {
                let alertController = UIAlertController(title: "Error", message: error!.localizedDescription, preferredStyle: .alert)

                alertController.addAction(UIAlertAction(title: "OK", style: .default))

                self.present(alertController, animated: true)
            }
        }
    }
}

Cell content is generated as follows. The corresponding Photo instance is retrieved from the photos array and used to configure the cell:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return (photos == nil) ? 0 : photos.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)

    if (cell == nil) {
        cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
    }

    let photo = photos[indexPath.row];

    cell!.textLabel!.text = photo.title

    // Attempt to load image from cache
    cell!.imageView!.image = thumbnailImages[photo.id]

    if (cell!.imageView!.image == nil && photo.thumbnailUrl != nil) {
        // Image was not found in cache; load it from the server

        ...
    }

    return cell!
}

If the thumbnail image is already available in the cache, it is used to populate the cell's image view. Otherwise, it is loaded from the server and added to the cache as shown below. If the cell is still visible when the image request returns, it is updated immediately and reloaded:

let serverURL = URL(string: String(format: "%@://%@", photo.thumbnailUrl!.scheme!, photo.thumbnailUrl!.host!))!

let serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: serverURL)

serviceProxy.invoke("GET", path: photo.thumbnailUrl!.path) { result, error in
    if (error == nil) {
        // If cell is still visible, update image and reload row
        let cell = tableView.cellForRow(at: indexPath)

        if (cell != nil) {
            if let thumbnailImage = result as? UIImage {
                self.thumbnailImages[photo.id] = thumbnailImage

                cell!.imageView!.image = thumbnailImage

                tableView.reloadRows(at: [indexPath], with: .none)
            }
        }
    } else {
        print(error!.localizedDescription)
    }
}

Finally, if the system is running low on memory, the image cache is cleared:

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()

    // Clear image cache
    thumbnailImages.removeAll()
}

Summary

This article provided an overview of how images can be dynamically loaded as needed to populate table view cells in iOS. For more ways to simplify iOS app development, please see my projects on GitHub:

  • MarkupKit – declarative UI for iOS and tvOS
  • HTTP-RPC – lightweight multi-platform REST

A Lightweight Alternative to JAX-RS

HTTP-RPC is an open-source framework for simplifying development of REST applications. It allows developers to access REST-based web services using a convenient, RPC-like metaphor while preserving fundamental REST principles such as statelessness and uniform resource access. The project currently includes support for consuming web services in Objective-C/Swift and Java (including Android), making it easy to interact with services regardless of target device or operating system.

HTTP-RPC also includes optional support for implementing REST services in Java, providing a lightweight alternative to larger REST frameworks such as JAX-RS. The entire platform is distributed as two JAR files totaling approximately 30KB in size, making it an ideal choice for applications where a minimal footprint is required.

This article introduces the HTTP-RPC server framework and provides an overview of its key features.

DispatcherServlet

DispatcherServlet is an abstract base class for REST services. 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), or as JSON. DispatcherServlet converts the request parameters to the expected argument types, invokes the method, and serializes the return value to the output stream as JSON.

The RequestMethod annotation is used to associate a service method with an HTTP verb such as GET or POST. The optional ResourcePath annotation can be used to associate the method with a specific path relative to the servlet. If unspecified, the method is associated with the servlet itself.

Multiple methods may be associated with the same verb and path. DispatcherServlet selects the best method to execute based on the provided argument values. For example, the following service class might be used to implement some simple mathematical operations:

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

    @RequestMethod("GET")
    @ResourcePath("/sum")
    public double getSum(List<Double> values) {
        double total = 0;

        for (double value : values) {
            total += value;
        }

        return total;
    }
}

The following request would cause the first method to be invoked:

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

This request would invoke the second method:

GET /math/sum?values=1&values=2&values=3

In either case, the service would return the value 6 in response.

Method Arguments

Method arguments may be any of the following types:

  • Numeric primitive or wrapper class (e.g. int or Integer)
  • boolean or Boolean
  • String
  • java.util.List
  • java.util.Map
  • java.net.URL

List arguments represent either multi-value parameters submitted using one of the form encodings or array structures submitted as JSON. Map arguments represent object structures submitted as JSON, and must use strings for keys. List and map values are automatically converted to their declared types when possible.

URL arguments represent file uploads. They may be used only with POST requests submitted using the multi-part form data encoding. For example:

@WebServlet(urlPatterns={"/upload/*"})
@MultipartConfig
public class FileUploadServlet extends DispatcherServlet {
    @RequestMethod("POST")
    public void upload(URL file) throws IOException {
        ...
    }

    @RequestMethod("POST")
    public void upload(List<URL> files) throws IOException {
        ...
    }
}

Return Values

Return values are converted to their JSON equivalents as follows:

  • Number: number
  • Boolean: true/false
  • CharSequence: string
  • Iterable: array
  • java.util.Map: object

Methods may also return void or Void to indicate that they do not produce a value.

For example, the following method would produce a JSON object containing three values. The mapOf() and entry() methods are provided by the framework to help simplify map creation:

@RequestMethod("GET")
public Map<String, ?> getMap() {
    return mapOf(
        entry("text", "Lorem ipsum"),
        entry("number", 123),
        entry("flag", true)
    );
}

The service would return the following in response:

{
    "text": "Lorem ipsum",
    "number": 123,
    "flag": true
}

Request and Repsonse Properties

DispatcherServlet provides the following methods to allow a service to access the request and response objects associated with the current operation:

protected HttpServletRequest getRequest() { ... }
protected HttpServletResponse getResponse() { ... }

For example, a service might access the request to get the name of the current user, or use the response to return a custom header.

The response object can also be used to produce a custom result. If a service method commits the response by writing to the output stream, the return value (if any) will be ignored by DispatcherServlet. This allows a service to return content that cannot be easily represented as JSON, such as image data or alternative text formats.

Path Variables

Path variables may be specified by a "?" character in the resource path. For example:

@RequestMethod("GET")
@ResourcePath("/contacts/?/addresses/?")
public List<Map<String, ?>> getContactAddresses() { ... }

The getKeys() method returns the list of variables associated with the current request:

protected List<String> getKeys() { ... }

For example, given the following path:

/contacts/jsmith/addresses/home

getKeys() would return the following:

["jsmith", "home"]

Summary

HTTP-RPC is an open-source framework for simplifying development of REST applications. It provides a lightweight alternative to larger Java REST frameworks such as JAX-RS, making it an ideal choice for low-footprint applications such as microservices or IoT.

For more information, see the project README.

Creating a Simple Android REST Client Using HTTP-RPC

HTTP-RPC is an open-source framework for simplifying development of REST applications. It allows developers to access REST-based web services using a convenient, RPC-like metaphor while preserving fundamental REST principles such as statelessness and uniform resource access.

The project currently includes support for consuming web services in Objective-C/Swift, Java, and JavaScript. It provides a consistent client API that makes it easy to interact with services regardless of target device or operating system.

This article provides a demonstration of how the HTTP-RPC Java client library can be used to invoke services provided by JSONPlaceholder, a "fake online REST API", in Android.

Service API

JSONPlaceholder offers a collection of web services that simulate common REST operations on a variety of resource types such as "albums", "photos", and "users". For example, the following URL retrieves a JSON document containing a list of simulated user records:

https://jsonplaceholder.typicode.com/users

The document is similar to the following:

[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  },
  ...
]

Additionally, the service provides a collection of simulated discussion posts, which can be retrieved on a per-user basis as follows:

https://jsonplaceholder.typicode.com/posts?userId=1

For example:

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  ...
]

Sample Application

The sample application presents two views. The first one displays a list of users:

The second displays a list of posts by the selected user:

WebServiceProxy Class

The WebServiceProxy class is used to invoke service operations. Internally, it uses an instance of HttpURLConnection to send and receive data. Response data is deserialized automatically into appropriate Java types including String, Number, Boolean, List, and Map.

Service operations are initiated by calling the invoke() method, which takes the following arguments:

  • method – the HTTP method to execute
  • path – the resource path
  • arguments – a map containing the request arguments as key/value pairs
  • resultHandler – an instance of ResultHandler that will be invoked upon completion of the service operation

A convenience method is also provided for executing operations that don't take any arguments.

Arguments are passed to the service either via the query string or in the request body, like an HTML form. List arguments represent multi-value parameters and are handled similarly to <select multiple> tags in HTML.

The result handler is a callback that is invoked upon completion of the request. If successful, the first argument will contain the value returned by the server. Otherwise, the first argument will be null, and the second will contain an exception representing the error that occurred.

For example, the following code might be used to invoke an operation that returns the sum of two numbers, specified by the "a" and "b" arguments. The static mapOf() and entry() methods are provided by the WebServiceProxy class to simplify argument map creation:

// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), (result, exception) -> {
    // result is 6
});

ExampleApplication Class

An instance of WebServiceProxy is created by the example application at startup:

private static WebServiceProxy serviceProxy;

public static WebServiceProxy getServiceProxy() {
    return serviceProxy;
}

@Override
public void onCreate() {
    super.onCreate();

    URL serverURL;
    try {
        serverURL = new URL("https://jsonplaceholder.typicode.com");
    } catch (MalformedURLException exception) {
        throw new RuntimeException(exception);
    }

    serviceProxy = new WebServiceProxy(serverURL, Executors.newSingleThreadExecutor()) {
        private Handler handler = new Handler(Looper.getMainLooper());

        @Override
        protected void dispatchResult(Runnable command) {
            handler.post(command);
        }
    };
}

The user and post activities discussed below use this proxy to invoke their respective service operations.

UserActivity Class

The UserActivity class is responsible for presenting the list of users returned from /users. Internally, it maintains a collection of objects representing the user list. In onResume(), if the list has not already been loaded, it uses the service proxy to retrieve the user list from the server:

public class UserActivity extends AppCompatActivity {
    private ListView userListView;

    private List<Map<String, ?>> userList = null;

    private BaseAdapter userListAdapter = new BaseAdapter() {
        ...
    };

    private static String TAG = UserActivity.class.getName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        userListView = (ListView)findViewById(R.id.user_list_view);

        userListView.setOnItemClickListener((parent, view, position, id) -> {
            Intent intent = new Intent(UserActivity.this, PostActivity.class);

            intent.putExtra(PostActivity.USER_ID_KEY, id);

            startActivity(intent);
        });
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (userList == null) {
            ExampleApplication.getServiceProxy().invoke("GET", "/users",
                (List<Map<String, ?>> result, Exception exception) -> {
                if (exception == null) {
                    userList = result;

                    userListView.setAdapter(userListAdapter);
                } else {
                    Log.e(TAG, exception.getMessage());
                }
            });
        }
    }
}

A list adapter is used to present the user details for each row:

private BaseAdapter userListAdapter = new BaseAdapter() {
    @Override
    public int getCount() {
        return userList.size();
    }

    @Override
    public Object getItem(int position) {
        return userList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return ((Number)userList.get(position).get("id")).longValue();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = getLayoutInflater().inflate(R.layout.item_user, null);
        }

        Map<String, ?> note = userList.get(position);

        String name = (String)note.get("name");

        TextView nameTextView = (TextView)convertView.findViewById(R.id.name_text_view);
        nameTextView.setText(name);

        String email = (String)note.get("email");

        TextView emailTextView = (TextView)convertView.findViewById(R.id.email_text_view);
        emailTextView.setText(email);

        return convertView;
    }
};

PostActivity Class

When a user is selected, an instance of PostActivity is presented. This class is responsible for presenting the list of user posts returned from /posts. Like UserActivity, it maintains a collection of objects representing the server response, and populates the list in onResume():

public class PostActivity extends AppCompatActivity {
    private ListView postListView;

    private List<Map<String, ?>> postList = null;

    private BaseAdapter postListAdapter = new BaseAdapter() {
        ...
    };

    public static final String USER_ID_KEY = "userID";

    private static String TAG = PostActivity.class.getName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_post);

        postListView = (ListView)findViewById(R.id.post_list_view);
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (postList == null) {
            long userID = getIntent().getLongExtra(USER_ID_KEY, 0);

            ExampleApplication.getServiceProxy().invoke("GET", "/posts", mapOf(entry("userId", userID)),
                (List<Map<String, ?>> result, Exception exception) -> {
                if (exception == null) {
                    postList = result;

                    postListView.setAdapter(postListAdapter);
                } else {
                    Log.e(TAG, exception.getMessage());
                }
            });
        }
    }
}

Again, a list adapter is used to present the details for each row:

private BaseAdapter postListAdapter = new BaseAdapter() {
    @Override
    public int getCount() {
        return postList.size();
    }

    @Override
    public Object getItem(int position) {
        return postList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return ((Number)postList.get(position).get("id")).longValue();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = getLayoutInflater().inflate(R.layout.item_post, null);
        }

        Map<String, ?> note = postList.get(position);

        String title = (String)note.get("title");

        TextView titleTextView = (TextView)convertView.findViewById(R.id.title_text_view);
        titleTextView.setText(title);

        String body = (String)note.get("body");

        TextView bodyTextView = (TextView)convertView.findViewById(R.id.body_text_view);
        bodyTextView.setText(body);

        return convertView;
    }
};

More Information

This article provided a demonstration of how the HTTP-RPC Java client library can be used to build a simple Android REST client application. The complete source code for the sample application can be found here.

The latest version of HTTP-RPC can be downloaded here. For more information, see the project README.

HTTP-RPC: A Lightweight Multi-Platform REST Client Framework

HTTP-RPC is an open-source framework for simplifying development of REST applications. It allows developers to access REST-based web services using a convenient, RPC-like metaphor while preserving fundamental REST principles such as statelessness and uniform resource access.

The project currently includes support for consuming web services in Objective-C/Swift and Java (including Android). It provides a consistent, callback-based API that makes it easy to interact with services regardless of target device or operating system.

This article introduces the HTTP-RPC framework and provides an overview of some of its key features.

Service Operations

Services are accessed by applying an HTTP verb such as GET or POST to a target resource. The target is specified by a path representing the name of the resource, and is generally expressed as a noun such as /calendar or /contacts.

Arguments are provided either via the query string or in the request body, like an HTML form. Results are typically returned as JSON; however, image and text content is also supported. Service operations may also return no value.

For example, the following request might retrieve the sum of two numbers, whose values are specified by the a and b query arguments:

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

Alternatively, the argument values could be specified as a list rather than as two fixed variables:

GET /math/sum?values=1&values=2&values=3

In either case, the service would return the value 6 in response.

Client Implementations

The iOS client is distributed as a universal framework that is about 500KB in size and has no external dependencies. The Java client is packaged as a JAR file that is only 22KB in size and also has no dependencies.

The following examples demonstrate how the various client libraries can be used to invoke the operations of the hypothetical math service discussed in the previous section. Each example creates an instance of a platform-specific service proxy, then executes the service requests by specifying the HTTP method, resource path, method arguments, and a result handler that will be invoked on completion of the method. Note that the static mapOf() and entry() methods used in the Java example are provided by the WebServiceProxy class to help simplify argument map creation:

Swift

// Create service proxy
let serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: URL(string: "https://localhost:8443")!)

// Get sum of "a" and "b"
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["a": 2, "b": 4]) {(result, error) in
    // result is 6
}

// Get sum of all values
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["values": [1, 2, 3, 4]]) {(result, error) in
    // result is 6
}

Java

// Create service proxy
WebServiceProxy serviceProxy = new WebServiceProxy(new URL("https://localhost:8443"), Executors.newFixedThreadPool(10));

// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), (result, exception) -> {
    // result is 6
});

// Get sum of all values
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), (result, exception) -> {
    // result is 6
});

Although the examples are written in different programming languages, they are all structurally similar and demonstrate identical behavior.

More Information

This article introduced the HTTP-RPC framework and provided an overview of some of its key features. The latest HTTP-RPC release can be downloaded here. For more information, see the project README.

Creating a Simple iOS REST Client Using HTTP-RPC

HTTP-RPC is an open-source framework for simplifying development of REST applications. It allows developers to access REST-based web services using a convenient, RPC-like metaphor while preserving fundamental REST principles such as statelessness and uniform resource access.

The project currently includes support for consuming web services in Objective-C/Swift, Java, and JavaScript. It provides a consistent client API that makes it easy to interact with services regardless of target device or operating system.

This article provides a demonstration of how the HTTP-RPC iOS client library can be used to invoke services provided by JSONPlaceholder, a "fake online REST API".

Service API

JSONPlaceholder offers a collection of web services that simulate common REST operations on a variety of resource types such as "albums", "photos", and "users". For example, the following URL retrieves a JSON document containing a list of simulated user records:

https://jsonplaceholder.typicode.com/users

The document is similar to the following:

[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  },
  ...
]

Additionally, the service provides a collection of simulated discussion posts, which can be retrieved on a per-user basis as follows:

https://jsonplaceholder.typicode.com/posts?userId=1

For example:

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  ...
]

Sample Application

The sample application presents two views. The first one displays a list of users:

The second displays a list of posts by the selected user:

WSWebServiceProxy Class

The WSWebServiceProxy class is used to invoke service operations. Internally, this class uses an instance of NSURLSession to issue HTTP requests. NSJSONSerialization is used to decode the response data.

Service operations are executed by calling invoke:path:arguments:resultHandler:. This method takes the following arguments:

  • method – the HTTP method to execute (e.g. "GET", "POST")
  • path – the resource path
  • arguments – a dictionary containing the request arguments as key/value pairs
  • resultHandler – a callback that will be invoked upon completion of the method

A convenience method is also provided for executing operations that don't take any arguments.

Arguments are passed to the service either via the query string or in the request body, like an HTML form. Array arguments represent multi-value parameters and are handled similarly to <select multiple> tags in HTML.

The result handler is a callback that is invoked upon completion of the request. If the operation completes successfully, the first argument will contain the result of the operation. If the operation fails, the second argument will be populated with an instance of NSError describing the error that occurred.

For example, the following code might be used to invoke an operation that returns the sum of two numbers, specified by the "a" and "b" arguments. The service would return the value 6 in response:

// Get sum of "a" and "b"
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["a": 2, "b": 4]) {(result, error) in
    // result is 6
}

Application Delegate

An instance of WSWebServiceProxy is created by the application delegate at startup:

private(set) static var serviceProxy: WSWebServiceProxy!

var window: UIWindow?

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    AppDelegate.serviceProxy = WSWebServiceProxy(session: URLSession.shared, serverURL: URL(string: "https://jsonplaceholder.typicode.com")!)

    return true
}

The user and post view controllers discussed below use this proxy to invoke their respective service operations.

UserViewController Class

The UserViewController class is responsible for presenting the list of users returned from /users. Internally, it maintains an array of objects representing the user list. In viewWillAppear:, if the list has not already been loaded, it uses the service proxy to retrieve the user list from the server. An activity indicator is shown while the method is executing, and hidden once the request has completed:

class UserViewController: UITableViewController {
    let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)

    var users: [[String: AnyObject]]! = nil

    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (users == nil) {
            tableView.separatorStyle = UITableViewCellSeparatorStyle.none
            activityIndicatorView.startAnimating()

            AppDelegate.serviceProxy.invoke("GET", path: "/users") {(result, error) in
                self.tableView.separatorStyle = UITableViewCellSeparatorStyle.singleLine
                self.activityIndicatorView.stopAnimating()

                if (error == nil) {
                    self.users = result as! [[String: AnyObject]]

                    self.tableView.reloadData()
                } else {
                    NSLog(error!.localizedDescription)
                }
            }
        }
    }

    ...
}

A custom cell class is used to present the user details for each row:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return (users == nil) ? 0 : users.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let user = users[(indexPath as NSIndexPath).row]

    let cell = tableView.dequeueReusableCell(withIdentifier: UserCell.self.description()) as! UserCell

    cell.nameLabel.text = user["name"] as? String
    cell.emailLabel.text = user["email"] as? String

    return cell
}

Finally, when a user is selected, an instance of PostViewController is created and pushed onto the navigation stack:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let postViewController = PostViewController()

    postViewController.userID = users[(indexPath as NSIndexPath).row]["id"] as! Int

    navigationController?.pushViewController(postViewController, animated: true)
}

PostViewController Class

The PostViewController is responsible for presenting the list of user posts returned from /posts. Like UserViewController, it maintains an array of objects representing the server response, and populates the array in viewWillAppear::

class PostViewController: UITableViewController {
    var userID: Int!

    let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)

    var posts: [[String: AnyObject]]! = nil

    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        if (posts == nil) {
            tableView.separatorStyle = UITableViewCellSeparatorStyle.none
            activityIndicatorView.startAnimating()

            AppDelegate.serviceProxy.invoke("GET", path: "/posts", arguments: ["userId": userID]) {(result, error) in
                self.tableView.separatorStyle = UITableViewCellSeparatorStyle.singleLine
                self.activityIndicatorView.stopAnimating()

                if (error == nil) {
                    self.posts = result as! [[String: AnyObject]]

                    self.tableView.reloadData()
                } else {
                    NSLog(error!.localizedDescription)
                }
            }
        }
    }

    ...
}

Again, a custom cell class is used to present the details for each row:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return (posts == nil) ? 0 : posts.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let post = posts[(indexPath as NSIndexPath).row]

    let cell = tableView.dequeueReusableCell(withIdentifier: PostCell.self.description()) as! PostCell

    cell.titleLabel.text = post["title"] as? String
    cell.bodyLabel.text = post["body"] as? String

    return cell
}

More Information

This article provided a demonstration of how the HTTP-RPC iOS client library can be used to build a simple REST client application. The complete source code for the sample application can be found here.

The latest version of HTTP-RPC can be downloaded here. For more information, see the project README.