Lesson Monday

Our MyRestaurants application can now successfully save user-provided zip codes and entire restaurant objects to Firebase. It can even dynamically update the user interface when our database changes. However, we're still prompting users to enter a zip code every time they open the app, even though we already have their latest-searched location saved. And we're only allowing them to execute new searches from the MainActivity. This isn't very user-friendly.

In this lesson we'll add a search widget to our app bar. This will prevent users from having to enter a zip code each time they launch the app. Instead, we'll automatically display restaurants from their last-searched location when they select "Find Restaurants". Entering a zip code will only be required if and when users would like to search a new zip code.

As outlined in the Android Developers Guides, a search widget behaves similarly to the EditText fields we've already used, but offers additional configuration including handling input events, offering search suggestions, and creating Intents when the user executes a query.

Downloading Icons

First, let’s download the white magnifying glass search icon from the Google Material Icons site. This icon will be used to denote that a search feature is available. We'll drag and drop the icon into the project in the same manner described in previous lessons. The resulting file structure should look like this:

icons-organized-in-project

Creating a Menu

Next, let's create a menu for our search widget. Right-click on the res directory and select new > Android resource directory.

create-menu-resource-directory-in-android-studio

Then, select menu from the resource type dropdown, and name the new directory menu:

create-menu-resource-in-android-studio

Next, we'll create a new menu resource file within this new directory and call it menu_search.xml :

create-new-menu-resource-file1

create-new-menu-resource-file2

Inside this file, we'll place the following layout code:

res/menu/menu_search.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_search"
        android:icon="@drawable/ic_search_white_24dp"
        app:showAsAction="always"
        app:actionViewClass="android.support.v7.widget.SearchView"
        android:title="Search">
    </item>
</menu>

Here, we set showAsAction to always to ensure the search icon (our magnifying glass) is always visible.

actionViewClass is where we determine which Android component to use as the action view. An action view is an action that provides functionality within the app bar. In this case we're setting app:actionViewClass to android.support.v7.widget.SearchView, which points to the SearchView class.

Remember when we said that SearchView behaves similarly to the EditText, but with additional configuration like handling input events, offering search suggestions, and creating Intents when the user executes a query? Setting app:actionViewClass to the SearchView class is what makes area of our layout a SearchView object, thereby giving it this extra functionality.

Adding Search Functionality

Next, let’s inflate the menu, gather the user's search query from our SearchView, and send it to our getRestaurants() method to request and display information about restaurants in the zip code provided by the user.

Also, now that we're done experimenting with writing data to Firebase, let's switch back to saving the user's zip code to shared preferences, like we did previously. (As described in this lesson, saving zip codes in Firebase was only temporary, to allow us to practice saving small pieces of data before saving entire Java objects).

Stashing and Retrieving Data from SharedPreferences

We'll save a new zip code to shared preferences if the user enters one, or we'll pull their most recently searched location from shared preferences if they do not.

First, uncomment any code relating to SharedPreferences in RestaurantListActivity. We'll also need to declare the SharedPreferences Editor object as a member variable at the top of our file. Now that users will be able to search a new zip code here in our RestaurantListActivity, we'll need access to the Editor to stash this new zip code in SharedPreferences:

RestaurantListActivity.java
public class RestaurantListActivity extends AppCompatActivity {
    private SharedPreferences mSharedPreferences;
    private SharedPreferences.Editor mEditor;
    private String mRecentAddress;
...

Additionally, we'll need to include the addToSharedPreferences() method from our MainActivity here in RestaurantListActivity. This method will be responsible for writing data to Shared Preferences:

RestaurantListActivity.java
...
 private void addToSharedPreferences(String location) {
        mEditor.putString(Constants.PREFERENCES_LOCATION_KEY, location).apply();
    }
...

Next, below the existing onCreate() method we'll override the onCreateOptionsMenu() and onOptionsItemSelected() methods:

RestaurantListActivity.java
...
 @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_search, menu);
        ButterKnife.bind(this);

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        mEditor = mSharedPreferences.edit();

        MenuItem menuItem = menu.findItem(R.id.action_search);
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(menuItem);

        return true;
    }

   @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return super.onOptionsItemSelected(item);
    }
...

In onCreateOptionsMenu() we inflate and bind our Views, define our mSharedPreferences and mEditor member variables. Also to retrieve a user’s search from the SearchView, we must grab the action_search menu item from our new layout, and use the MenuItemCompat.getActionView() method.

onOptionsItemSelected() simply contains the line return super.onOptionsItemSelected(item);. This ensures that all functionality from the parent class (referred to here as super) will still apply despite us manually overriding portions of the menu's functionality.

SearchView Listeners

Now that we've located our SearchView, we can attach a special listener to it. SearchView objects have their own dedicated listeners called OnQueryTextListener, that listen for changes in the SearchView.

We'll call setOnQueryTextListener() and pass in a new SearchView.OnQueryTextListener. It has two methods we will need to override: onQueryTextSubmit() and onQueryTextChange():

RestaurantListActivity.java
...
 @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_search, menu);
        ButterKnife.bind(this);

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        mEditor = mSharedPreferences.edit();

        MenuItem menuItem = menu.findItem(R.id.action_search);
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(menuItem);

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {

            @Override
            public boolean onQueryTextSubmit(String query) {
                addToSharedPreferences(query);
                getRestaurants(query);
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                return false;
            }

        });
        return true;
    }
...

As you may have anticipated, OnQueryTextSubmit() is run automatically when the user submits a query into our SearchView, and onQueryTextChange() is run whenever any changes to the SearchView contents occur.

Because we only want to gather the input after the user has submitted something (and not every time they type a single character into the field), we'll place our logic in onQueryTextSubmit(), leaving onQueryTextChange fairly empty. We call addToSharedPreferences() to save the zip code the user searches, and getRestaurants() to begin executing a request to the Yelp API to return restaurants in that area.

After these changes, the completed RestaurantListActivity file should appear as follows:

RestaurantListActivity.java
public class RestaurantListActivity extends AppCompatActivity {
    private SharedPreferences mSharedPreferences;
    private SharedPreferences.Editor mEditor;
    private String mRecentAddress;

    @Bind(R.id.recyclerView) RecyclerView mRecyclerView;

    private RestaurantListAdapter mAdapter;
    public ArrayList<Restaurant> mRestaurants = 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");

        getRestaurants(location);

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        mRecentAddress = mSharedPreferences.getString(Constants.PREFERENCES_LOCATION_KEY, null);

        if (mRecentAddress != null) {
            getRestaurants(mRecentAddress);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_search, menu);
        ButterKnife.bind(this);

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        mEditor = mSharedPreferences.edit();

        MenuItem menuItem = menu.findItem(R.id.action_search);
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(menuItem);

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {

            @Override
            public boolean onQueryTextSubmit(String query) {
                addToSharedPreferences(query);
                getRestaurants(query);
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                return false;
            }
        });

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return super.onOptionsItemSelected(item);
    }

    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) {
                mRestaurants = yelpService.processResults(response);

                RestaurantListActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mAdapter = new RestaurantListAdapter(getApplicationContext(), mRestaurants);
                        mRecyclerView.setAdapter(mAdapter);
                        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(RestaurantListActivity.this);
                        mRecyclerView.setLayoutManager(layoutManager);
                        mRecyclerView.setHasFixedSize(true);
                    }
                });
            }
        });
    }

    private void addToSharedPreferences(String location) {
        mEditor.putString(Constants.PREFERENCES_LOCATION_KEY, location).apply();
    }

}

Refactoring MainActivity

We can now remove both the locationEditText from activity_main.xml and any code for adding its contents to shared preferences or Firebase from MainActivity.java, since we are now saving zip codes to shared preferences in RestaurantListActivity.

Our refactored MainActivity.java file should look like this:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Bind(R.id.findRestaurantsButton) Button mFindRestaurantsButton;
    @Bind(R.id.appNameTextView) TextView mAppNameTextView;
    @Bind(R.id.savedRestaurantsButton) Button mSavedRestaurantsButton;

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

        Typeface ostrichFont = Typeface.createFromAsset(getAssets(), "fonts/ostrich-regular.ttf");
        mAppNameTextView.setTypeface(ostrichFont);

        mFindRestaurantsButton.setOnClickListener(this);
        mSavedRestaurantsButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {

        if(v == mFindRestaurantsButton) {
            Intent intent = new Intent(MainActivity.this, RestaurantListActivity.class);
            startActivity(intent);
        }

        if (v == mSavedRestaurantsButton) {
            Intent intent = new Intent(MainActivity.this, SavedRestaurantListActivity.class);
            startActivity(intent);
        }
    }
}

And activity_main.xml should appear as follows:

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.MainActivity"
    android:background="#000000">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/imageView"
        android:src="@drawable/background"
        android:scaleType="centerCrop" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MyRestaurants"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="35dp"
        android:textColor="@color/colorTextIcons"
        android:textSize="60sp"
        android:textStyle="bold"
        android:id="@+id/appNameTextView"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Find Restaurants"
        android:id="@+id/findRestaurantsButton"
        android:background="@color/colorAccent"
        android:textColor="@color/colorTextIcons"
        android:visibility="visible"
        android:layout_above="@+id/savedRestaurantsButton"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_marginBottom="10dp" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="My Saved Restaurants"
        android:id="@+id/savedRestaurantsButton"
        android:background="@color/colorAccent"
        android:textColor="@color/colorTextIcons"
        android:visibility="visible"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"/>

</RelativeLayout>

Now, users will be greeted with two buttons when they launch our application:

main-activity-without-edit-text

"Find Restaurants" will continue to take users to a list of restaurants in the area they last searched. If the user has never searched for a zip code, they'll be able to use our new search widget at the top of the page in RestaurantListActivity to execute a new search:

search-view-icon

search-view-in-action


Example GitHub Repo for MyRestaurants

Terminology


  • Search widget: Behaves similarly to EditText fields, but offers additional configuration including handling input events, offering search suggestions, and creating intents when the user executes a search.

  • Action view: An object (or View) that provides functionality within the app bar.

Examples


Additional Resources