Lesson Weekend

In this lesson we'll update our MyRestaurants application to display the names of the restaurants received from the Yelp API instead of the hard-coded restaurant array in RestaurantsActivity.

Previously, we created an ArrayAdapter and attached it to a ListView in the onCreate() method. However, now that we're relying on the Yelp API, we cannot actually display any restaurants until after we've successfully received a response. In order to ensure these things are handled gracefully and in the correct order, we'll need to explore a concept called threading.

Threads and Multi-Threading

In programming, a thread is the execution of instructions that can be managed independently. For instance, following a recipe to make pancakes in the morning can be considered a metaphorical "thread".

Multi-threading is simply a program executing multiple threads at once. Let's say you're simultaneously making coffee while cooking pancakes. That's multi-threading.

Threading in Android

When an Android application is launched, a main thread is always created. This thread is in charge of the user interface. Every additional component is also run on this thread, unless explicitly instructed otherwise. It is also the only thread that may update the user interface. For this reason, it's also often referred to as a UI thread.

It's really important not to slow down the main thread with lengthy processes. If tons of complex code is executed on the main thread, it can get held up. This results in poor performance: Apps can slow down, freeze, or even crash entirely.

As we know, OkHttp and SignPost manage the complex process of contacting Yelp. Because creating OAuth signatures, requesting data, and waiting for a response all take time, OkHttp completes this work on a background, or worker thread. This is an additional thread, separate from our main thread, that may execute code. By keeping API requests off our main/UI thread, our application remains performant and responsive.

Threading in MyRestaurants

Currently, our onResponse() callback is triggered as soon as we receive data from the Yelp API. In this callback, we run our .processResults() method to parse JSON, create restaurant objects, and return an array of each new object.

After this occurs, we need to update our RestaurantsActivity's corresponding View to actually show the user these restaurants. But how do we do this?

Because onResponse() is a callback executed by OkHttp, it's actually running on that background thread OkHttp creates. And, we can only alter the user interface from the main/UI thread. So, we can't add code to display our restaurants in the callback. If we tried, our app would crash and we would receive an error reading android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

The onCreate() method always occurs on the main/UI thread. But we can't set the adapter and ListView to display our list of restaurants there either. Because onCreate() is executed before we complete our API request, we simply don't have the information to display yet.

In order to ensure everything occurs in the correct order and thread we'll need to wait until our restaurant information is successfully returned, then explicitly instruct our app to return to the UI thread where we can alter the user interface, and display restaurants to the user.

Switching to UI Thread

Thankfully, this is such a common conundrum that there's a built-in method in Android to do this: .runOnUiThread(). As described in its documentation entry, this method takes something called a Runnable as an argument, and places all code within it in the UI thread.

Runnable is an interface meant to handle sharing code between threads. It has only one method: run(). This method contains the code we want to run on the thread specified.

We'll call .runOnUiThread(), and override its run() method in the onResponse() callback:

RestaurantsActivity.java
public class RestaurantsActivity extends AppCompatActivity {
  ...
    private void getRestaurants(String location) {
        final YelpService yelpService = new YelpService();

        yelpService.findRestaurants(location, new Callback() {
        ...
            @Override
            public void onResponse(Call call, Response response) {
                restaurants = yelpService.processResults(response);

                RestaurantsActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                    }

                  });
          ...
...

Additionally, notice we've removed the try/catch block from the onResponse() callback in getRestaurants(). We originally introduced this try/catch in the API Requests and Responses lesson before we wrote a method to parse our data. However, we now have a try/catch block in Yelp Service's processResults() method to handle any exceptions. Because processResults() is being called right here in onResponse(), we don't need to catch exceptions twice.

Moving on, when a user navigates to the RestaurantsActivity, we want them to see a list of restaurant names specific to the zip code they entered in the MainActivity. Let's create a list of these names here:

RestaurantsActivity.java
...
            @Override
            public void onResponse(Call call, Response response) {
                restaurants = yelpService.processResults(response);

                RestaurantsActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        String[] restaurantNames = new String[restaurants.size()];
                        for (int i = 0; i < restaurantNames.length; i++) {
                            restaurantNames[i] = restaurants.get(i).getName();
                        }
             ...

Next, we can create a new ArrayAdapter to pass our data to the View. The resulting code should look like this:

RestaurantsActivity.java
...

            @Override
            public void onResponse(Call call, Response response) {
                restaurants = yelpService.processResults(response);

                RestaurantsActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        String[] restaurantNames = new String[restaurants.size()];
                        for (int i = 0; i < restaurantNames.length; i++) {
                            restaurantNames[i] = restaurants.get(i).getName();
                        }

                        ArrayAdapter adapter = new ArrayAdapter(RestaurantsActivity.this, 
                           android.R.layout.simple_list_item_1, restaurantNames);
                        mListView.setAdapter(adapter);
                    }
                });
            }
        });
...

Refactoring

We can also refactor by removing the following code:

  • Remove the restaurants array of hard-coded restaurant names, if you haven't already done so. We're now receiving restaurant names directly from the Yelp API.
  • Remove any code creating or setting ArrayAdapter in onCreate(). We're now creating and setting adapters in Runnable's .run() override within onResponse() instead.
  • Remove our click listener that displays a toast with the restaurant name from onCreate(). We'll no longer need this feature. In the next few lessons we'll program our app to navigate to a detail page when the user selects a restaurant.
  • Because our user interface is only displaying restaurant names (for now) let's log the other attributes of each restaurant in a loop to ensure they've been correctly saved too, we can do this in our run() override, like this:
RestaurantsActivity.java
...
                    @Override
                    public void run() {
                        String[] restaurantNames = new String[restaurants.size()];
                        for (int i = 0; i < restaurantNames.length; i++) {
                            restaurantNames[i] = restaurants.get(i).getName();
                        }
                        ArrayAdapter adapter = new ArrayAdapter(RestaurantsActivity.this,
                                android.R.layout.simple_list_item_1, restaurantNames);
                        mListView.setAdapter(adapter);

                        for (Restaurant restaurant : restaurants) {
                            Log.d(TAG, "Name: " + restaurant.getName());
                            Log.d(TAG, "Phone: " + restaurant.getPhone());
                            Log.d(TAG, "Website: " + restaurant.getWebsite());
                            Log.d(TAG, "Image url: " + restaurant.getImageUrl());
                            Log.d(TAG, "Rating: " + Double.toString(restaurant.getRating()));
                            Log.d(TAG, "Address: " + android.text.TextUtils.join(", ", restaurant.getAddress()));
                            Log.d(TAG, "Categories: " + restaurant.getCategories().toString());
                        }
                    }
...

After all changes described here, RestaurantsActivity should now look like this:

RestaurantsActivity.java
package com.epicodus.myrestaurants;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

import java.io.IOException;
import java.util.ArrayList;

import butterknife.Bind;
import butterknife.ButterKnife;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;

import static com.epicodus.myrestaurants.R.id.listView;

public class RestaurantsActivity extends AppCompatActivity {
    public static final String TAG = RestaurantsActivity.class.getSimpleName();

    @Bind(R.id.locationTextView) TextView mLocationTextView;
    @Bind(listView) ListView mListView;

    public ArrayList<Restaurant> restaurants = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_restaurants);
        ButterKnife.bind(this);

        Intent intent = getIntent();
        String location = intent.getStringExtra("location");

        mLocationTextView.setText("Here are all the restaurants near: " + location);
        getRestaurants(location);
    }

    private void getRestaurants(String location) {
        final YelpService yelpService = new YelpService();
        yelpService.findRestaurants(location, new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) {
                restaurants = yelpService.processResults(response);

                RestaurantsActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        String[] restaurantNames = new String[restaurants.size()];
                        for (int i = 0; i < restaurantNames.length; i++) {
                            restaurantNames[i] = restaurants.get(i).getName();
                        }

                        ArrayAdapter adapter = new ArrayAdapter(RestaurantsActivity.this,
                                android.R.layout.simple_list_item_1, restaurantNames);
                        mListView.setAdapter(adapter);

                        for (Restaurant restaurant : restaurants) {
                            Log.d(TAG, "Name: " + restaurant.getName());
                            Log.d(TAG, "Phone: " + restaurant.getPhone());
                            Log.d(TAG, "Website: " + restaurant.getWebsite());
                            Log.d(TAG, "Image url: " + restaurant.getImageUrl());
                            Log.d(TAG, "Rating: " + Double.toString(restaurant.getRating()));
                            Log.d(TAG, "Address: " + android.text.TextUtils.join(", ", restaurant.getAddress()));
                            Log.d(TAG, "Categories: " + restaurant.getCategories().toString());
                        }
                    }
                });
            }
        });
    }
}



Note: android.text.TextUtils.join(", ", restaurant.getAddress()) is just a nice little shortcut to join Lists or ArrayLists in Android.

Now let's run our app and we should see a new list of restaurants on our RestaurantActivity and a bunch of corresponding data in the logcat. Good work!


Example GitHub Repo for MyRestaurants

Terminology


  • Thread: The execution of instructions that can be managed independently.

  • Multi-threading: A program executing multiple threads at once.

  • Main Thread: The default, primary thread created anytime an Android application is launched. Also known as a UI thread, it is in charge of handling all user interface and activities, unless otherwise specified.

  • Runnable is an interface meant to handle sharing code between threads. It contains only one method: run(). This method generally contains the code we want to run on the thread specified.

Additional Resources