Week 8 - The Toolbar, Menus, and Navigation
Corresponding Text
Android Programming, pp. 235-254
The AppCompat Library
We're going to be working with the toolbar. The toolbar provides a means of providing the user with actions, an additional way of navigating through the app, and a way of consistently branding the app. The toolbar was added to Android in version 5.0; prior to this, app made us of an action bar. The toolbar builds on the action bar but is more flexible. Android version 5.0 corresponds to SDK version 21; since our app support SDK version 19, we won't be able to use the native tool bar from the Android library. Instead, we can use a back-ported version from the AppCompat library. We'll have to make a few changes to our app to make use of the AppCompat library and the toolbar provided by it.
First, let's add the AppCompat library as a dependency of our app. In Android
Studio, select File -> Project Structure... from the menus. With app
selected, click the Dependencies tab. Click the + button and
Add Library dependency. Search for com.android.support:appcompat-v7
.
Click OK to close the Project Structure dialog.
Next, we need to make sure the app is using one of the AppCompat theme's. Open
app/manifests/AndroidManifest.xml
and note the value of the android:theme
attribute: @style/AppTheme
. The theme is defined in res/values/styles.xml
.
The file should contain something like this:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
If it doesn't, change the parent attribute so it matches the value above.
Finally, we have to update all our activities to extend AppCompatActivity. So far, all our activities have extended FragmentActivity or a subclass of FragmentActivity. We won't lose any functionality by extending AppCompatActivity instead of FragmentActivity because AppCompatActivity itself extends FragmentActivity. For our app, we'll need to update ContactPagerActivity and SingleFragmentActivity to extend AppCompatActivity. We don't need to make any changes to ContactActivity or AddressBookActivity since they extend SingleFragmentActivity.
At this point, we can run the app. The only difference is the new toolbar at the top of our app.
Menus
The right area of the toolbar is reserved for the menu. A menu consists of
action items that can perform an action on the current screen or on the
app as a whole. We'll create actions to create a contact and to display
favorites or all contacts. We'll need some string resources for use with the
menu; add these to res/values/strings.xml
:
<string name="new_contact">New Contact</string>
<string name="show_favorites">Show Favorites</string>
<string name="show_all">Show All</string>
Menus, like layouts, are a type of resource file. To add a new menu resource
file, right click on the project's res
folder and select
New -> Android resource file. Name the file fragment_address_book
(since
we'll be using it with the address book fragment) and set Resource type to
Menu
Using the Design view, drag a Menu Item and drop it on the toolbar; set
its id to menu_item_create_contact
and its title to
@string/new_contact
. Drag another Menu Item below the previous one and set
its id to menu_item_toggle_favorites
and its title to
@string/show_favorites
. The XML for the menu resource file should look
like this:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/new_contact"
android:id="@+id/menu_item_create_contact"
/>
<item android:title="@string/show_favorites"
android:id="@+id/menu_item_toggle_favorites"
/>
</menu>
The menu is currently configured such that its menu items appear in the overflow menu, accessed by pressing the three dots on the right side of the toolbar. Let's keep the toggle item in the overflow menu but move the new contact item out of the overflow menu and use an icon as well as text, provided there's enough room on the screen.
With the new contact menu item selected, click the button next to the icon
field. Search for and select ic_menu_add
. Change the value of
showAsAction to include ifRoom
and `withText``.
The XML for the menu resource file should now look like this:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/new_contact"
android:id="@+id/menu_item_create_contact"
android:icon="@android:drawable/ic_menu_add"
app:showAsAction="ifRoom|withText"/>
<item android:title="@string/show_favorites"
android:id="@+id/menu_item_toggle_favorites"
/>
</menu>
Note that we have to use the app:showAsAction attribute rather than the default android:showAsAction attribute due to legacy support issues with the AppCompat library.
The design view should look like this:
Now that we've designed the menu, let's add it to our app. In Android, menus
are managed by callbacks from the Activity class. When a menu is needed,
Activity.onCreateOptionsMenu() is called. Our app, however, relies on
code to be implemented in fragments. Fortunately, framgments also have
a onCreateOptionsMenu() that we can override. The FragmentManager is
responsible for call the appropriate fragment's method when the hosting
activity's onCreateOptionsMenu() method is called. To notify the
FragmentManager that this fragment can receive callbacks, we have to add
a call to setHasOptionsMenu(true)
in the AddressBookFragment.onCreate()
method.
To add the menu to the address book, add the following code to the AddressBookFragment class:
public class AddressBookFragment extends Fragment {
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_address_book, menu);
}
...
}
Here, we call MenuInflater.inflate() specifying the id of our menu resource and the Menu instance that will be populated with our menu items. We can now run the app and we should be able to see the menu items.
Now that we have the menu appearing in the app, we need to add code that will perform some action when a user selects one of the menu items. To do this, we have to override the onOptionsItemSelected() method in AddressBookFragment. When the method is called, a MenuItem is passed as a parameter. We can use MenuItem.getItemId() to get the ID of the item and determine the appropriate action.
Before we handle the menu items, let's add a method to AddressBook:
public class AddressBook {
...
public List<Contact> getFavoriteContacts() {
List<Contact> favorites = new ArrayList<>();
for (Contact c: mContacts) {
if (c.isFavorite()) {
favorites.add(c);
}
}
return favorites;
}
...
}
This method will return a list of contacts marked as favorites. Next, we can add the code necessary to toggle between showing all contacts and those that are favorites:
public class AddressBookFragment extends Fragment {
...
private boolean mShowFavoritesOnly = false;
...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_create_contact:
// we'll add this later
return true;
case R.id.menu_item_toggle_favorites:
mShowFavoritesOnly = !mShowFavoritesOnly;
if (mShowFavoritesOnly) {
item.setTitle(R.string.show_all);
mContactAdapter.mContacts =
AddressBook.get().getFavoriteContacts();
}
else {
item.setTitle(R.string.show_favorites);
mContactAdapter.mContacts =
AddressBook.get().getContacts();
}
mContactAdapter.notifyDataSetChanged();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
...
}
First, we'll keep track of whether or not we're displaying only favorite contacts or all contacts using a private field, mShowFavoritesOnly. In the onOptionsItemSelected() method, we'll determine what to do based on the menu item's ID. We'll add code to add a new contact shortly. For now, we can toggle between favorites and all contacts by first changing the value of mShowFavoritesOnly then setting the menu item's title and the adapter's list of contacts based on the value of mShowFavoritesOnly. After we change the adapter's list of contacts, we have to call Adapter.notifyDataSetChanged(). Finally, we return true to indicate no further processing of the item is necessary.
To support adding new contacts, let's first reduce the number of contacts that are automatically generated - we'll keep some so they exist when we start the app. Recall that we create these initial contacts in the AddressBook constructor. While we're modifying AddressBook, let's also add a method to add a contact to the list of contacts. Here's what AddressBook should look like:
public class AddressBook {
...
private AddressBook() {
mContacts = new ArrayList<>();
for (int i=0; i<5; i++) {
Contact contact = new Contact();
contact.setName("Person " + i);
contact.setEmail("Person" + i + "@email.com");
// set every 2nd as a favorite
if (i % 2 == 0) {
contact.setFavorite(true);
}
mContacts.add(contact);
}
}
...
public void add(Contact contact) {
mContacts.add(contact);
}
...
}
Next, we can add code to handle the new contact menu item to AddressBookFragment:
public class AddressBookFragment extends Fragment {
...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_create_contact:
Contact contact = new Contact();
AddressBook.get().add(contact);
Intent intent = ContactPagerActivity.newIntent(
getActivity(),contact.getID());
startActivity(intent);
return true;
case R.id.menu_item_toggle_favorites:
mShowFavoritesOnly = !mShowFavoritesOnly;
if (mShowFavoritesOnly) {
item.setTitle(R.string.show_all);
mContactAdapter.mContacts =
AddressBook.get().getFavoriteContacts();
}
else {
item.setTitle(R.string.show_favorites);
mContactAdapter.mContacts =
AddressBook.get().getContacts();
}
mContactAdapter.notifyDataSetChanged();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
...
}
This new code creates a Contact, adds it to the AddressBook, and then uses ContactPagerActivity to create an intent and display the contact's details which we can edit. We can now run the app, add contacts, and filter the list to display only our favorites.
Hierarchical Navigation
So far, we've been able to use the back button to return to a previous screen while using our app. This type of navigation is called temporal navigation - using the back button takes us back to the last place we were. An alternative to temporal navigation is hierarchical navigation. Hierarchical navigation allows users to move up the app hierarchy - returning to the parent activity at any time. Hierarchical navigation is made available to users through an up button that appears as a left-pointing arrow in the toolbar. In order to enable this functionality, we have to specify an activity's parent in the app's manifest. The following will allow users to return to the AddressBookActivity from the ContactPagerActivity:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.arthurneuman.mycontacts">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".ContactPagerActivity"
android:parentActivityName=".AddressBookActivity">
</activity>
<activity android:name=".AddressBookActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
While this performs the same action as pushing the back button in our app, in more complicated apps, this could be more useful. In more complicated apps, hierarchical navigation would allow to return to a screen that would normally require multiple presses of the back button.