Class ContactsFragment
displays the contact list in a RecyclerView
and provides a FloatingActionButton
that the user can touch to add a new contact.
Figure 9.31 lists ContactsFragment
’s package
statement and import
statements and the beginning of its class definition. The ContactsFragment
uses a LoaderManager
and a Loader
to query the AddressBookContentProvider
and receive a Cursor
that the ContactsAdapter
(Section 9.11) uses to supply data to the RecyclerView
. ContactsFragment
implements interface LoaderManager.LoaderCallbacks<Cursor>
(line 23) so that it can respond to method calls from the LoaderManager
to create the Loader
and process the results returned by the AddressBookContentProvider
.
1 // ContactsFragment.java
2 // Fragment subclass that displays the alphabetical list of contact names
3 package com.deitel.addressbook;
4
5 import android.content.Context;
6 import android.database.Cursor;
7 import android.net.Uri;
8 import android.os.Bundle;
9 import android.support.design.widget.FloatingActionButton;
10 import android.support.v4.app.Fragment;
11 import android.support.v4.app.LoaderManager;
12 import android.support.v4.content.CursorLoader;
13 import android.support.v4.content.Loader;
14 import android.support.v7.widget.LinearLayoutManager;
15 import android.support.v7.widget.RecyclerView;
16 import android.view.LayoutInflater;
17 import android.view.View;
18 import android.view.ViewGroup;
19
20 import com.deitel.addressbook.data.DatabaseDescription.Contact;
21
22 public class ContactsFragment extends Fragment
23 implements LoaderManager.LoaderCallbacks<Cursor> {
24
Figure 9.32 defines the nested interface ContactsFragmentListener
, which contains the callback methods that MainActivity
implements to be notified when the user selects a contact (line 28) and when the user touches the FloatingActionButton
to add a new contact (line 31).
25 // callback method implemented by MainActivity
26 public interface ContactsFragmentListener {
27 // called when contact selected
28 void onContactSelected(Uri contactUri);
29
30 // called when add button is pressed
31 void onAddContact();
32 }
33
Figure 9.33 shows class ContactsFragment
’s fields. Line 34 declares a constant that’s used to identify the Loader
when processing the results returned from the AddressBookContentProvider
. In this case, we have only one Loader
—if a class uses more than one Loader
, each should have a constant with a unique integer value so that you can identify which Loader
to manipulate in the LoaderManager.LoaderCallbacks<Cursor>
callback methods. The instance variable listener
(line 37) will refer to the object that implements the interface (MainActivity
). Instance variable contactsAdapter
(line 39) will refer to the ContactsAdapter
that binds data to the RecyclerView
.
34 private static final int CONTACTS_LOADER = 0; // identifies Loader
35
36 // used to inform the MainActivity when a contact is selected
37 private ContactsFragmentListener listener;
38
39 private ContactsAdapter contactsAdapter; // adapter for recyclerView
40
Overridden Fragment
method onCreateView
(Fig. 9.34) inflates and configures the Fragment
’s GUI. Most of this method’s code has been presented in prior chapters, so we focus only on the new features here. Line 47 indicates that the ContactsFragment
has menu items that should be displayed on the Activity
’s app bar (or in its options menu). Lines 56–74 configure the RecyclerView
. Lines 60–67 create the ContactsAdapter
that populates the RecyclerView
. The argument to the constructor is an implementation of the ContactsAdapter.ContactClickListener
interface (Section 9.11) specifying that when the user touches a contact, the ContactsFragmentListener
’s onContactSelected
should be called with the Uri
of the contact to display in a DetailFragment
.
41 // configures this fragment's GUI
42 @Override
43 public View onCreateView(
44 LayoutInflater inflater, ViewGroup container,
45 Bundle savedInstanceState) {
46 super.onCreateView(inflater, container, savedInstanceState);
47 setHasOptionsMenu(true); // fragment has menu items to display
48
49 // inflate GUI and get reference to the RecyclerView
50 View view = inflater.inflate(
51 R.layout.fragment_contacts, container, false);
52 RecyclerView recyclerView =
53 (RecyclerView) view.findViewById(R.id.recyclerView);
54
55 // recyclerView should display items in a vertical list
56 recyclerView.setLayoutManager(
57 new LinearLayoutManager(getActivity().getBaseContext()));
58
59 // create recyclerView's adapter and item click listener
60 contactsAdapter = new ContactsAdapter(
61 new ContactsAdapter.ContactClickListener() {
62 @Override
63 public void onClick(Uri contactUri) {
64 listener.onContactSelected(contactUri);
65 }
66 }
67 );
68 recyclerView.setAdapter(contactsAdapter); // set the adapter
69
70 // attach a custom ItemDecorator to draw dividers between list items
71 recyclerView.addItemDecoration(new ItemDivider(getContext()));
72
73 // improves performance if RecyclerView's layout size never changes
74 recyclerView.setHasFixedSize(true);
75
76 // get the FloatingActionButton and configure its listener
77 FloatingActionButton addButton =
78 (FloatingActionButton) view.findViewById(R.id.addButton);
79 addButton.setOnClickListener(
80 new View.OnClickListener() {
81 // displays the AddEditFragment when FAB is touched
82 @Override
83 public void onClick(View view) {
84 listener.onAddContact();
85 }
86 }
87 );
88
89 return view;
90 }
91
Class ContactsFragment
overrides Fragment
lifecycle methods onAttach
and onDetach
(Fig. 9.35) to set instance variable listener
. In this app, listener
refers to the host Activity
(line 96) when the ContactsFragment
is attached and is set to null
(line 103) when the ContactsFragment
is detached.
92 // set ContactsFragmentListener when fragment attached
93 @Override
94 public void onAttach(Context context) {
95 super.onAttach(context);
96 listener = (ContactsFragmentListener) context;
97 }
98
99 // remove ContactsFragmentListener when Fragment detached
100 @Override
101 public void onDetach() {
102 super.onDetach();
103 listener = null;
104 }
105
Fragment
lifecycle method onActivityCreated (Fig. 9.36) is called after a Fragment
’s host Activity
has been created and the Fragment
’s onCreateView
method completes execution—at this point, the Fragment
’s GUI is part of the Activity
’s view hierarchy. We use this method to tell the LoaderManager
to initialize a Loader
—doing this after the view hierarchy exists is important because the RecyclerView
must exist before we can display the loaded data. Line 110 uses Fragment
method getLoaderManager to obtain the Fragment
’s LoaderManager
object. Next we call LoaderManager
’s initLoader
method, which receives three arguments:
• the integer ID used to identify the Loader
• a Bundle
containing arguments for the Loader
’s constructor, or null
if there are no arguments
• a reference to the implementation of the interface LoaderManager.LoaderCallbacks<Cursor>
(this
represents the ContactsAdapter
)—you’ll see the implementations of this interface’s methods onCreateLoader
, onLoadFinished
and onLoaderReset
in Section 9.10.8.
If there is not already an active Loader
with the specified ID, the initLoader
method asynchronously calls the onCreateLoader
method to create and start a Loader
for that ID. If there is an active Loader
, the initLoader
method immediately calls the onLoadFinished
method.
106 // initialize a Loader when this fragment's activity is created
107 @Override
108 public void onActivityCreated(Bundle savedInstanceState) {
109 super.onActivityCreated(savedInstanceState);
110 getLoaderManager().initLoader(CONTACTS_LOADER, null, this);
111 }
112
ContactsFragment
method updateContactList
(Fig. 9.37) simply notifies the ContactsAdapter
when the data changes. This method is called when new contacts are added and when existing contacts are updated or deleted.
113 // called from MainActivity when other Fragment's update database
114 public void updateContactList() {
115 contactsAdapter.notifyDataSetChanged();
116 }
117
Figure 9.38 presents class ContactsFragment
’s implementations of the callback methods in interface LoaderManager.LoaderCallbacks<Cursor>
.
118 // called by LoaderManager to create a Loader
119 @Override
120 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
121 // create an appropriate CursorLoader based on the id argument;
122 // only one Loader in this fragment, so the switch is unnecessary
123 switch (id) {
124 case CONTACTS_LOADER:
125 return new CursorLoader(getActivity(),
126 Contact.CONTENT_URI, // Uri of contacts table
127 null, // null projection returns all columns
128 null, // null selection returns all rows
129 null, // no selection arguments
130 Contact.COLUMN_NAME + " COLLATE NOCASE ASC"); // sort order
131 default:
132 return null;
133 }
134 }
135
136 // called by LoaderManager when loading completes
137 @Override
138 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
139 contactsAdapter.swapCursor(data);
140 }
141
142 // called by LoaderManager when the Loader is being reset
143 @Override
144 public void onLoaderReset(Loader<Cursor> loader) {
145 contactsAdapter.swapCursor(null);
146 }
147 }
The LoaderManager
calls method onCreateLoader (lines 119–134) to create and return a new Loader
for the specified ID, which the LoaderManager
manages in the context of the Fragment
’s or Activity
’s lifecycle. Lines 123–133 determine the Loader
to create, based on the ID received as onCreateLoader
’s first argument.
For the ContactsFragment
, we need only one Loader
, so the switch
statement is unnecessary, but we included it here as a good practice.
Lines 125–130 create and return a CursorLoader
that queries the AddressBookContentProvider
to get the list of contacts, then makes the results available as a Cursor
. The CursorLoader
constructor receives the Context
in which the Loader
’s lifecycle is managed and uri
, projection
, selection
, selectionArgs
and sortOrder
arguments that have the same meaning as those in the ContentProvider
’s query
method (Section 9.8.3). In this case, we specified null
for the projection
, selection
and selectionArgs
arguments and indicated that the contacts should be sorted by name in a case insensitive manner.
Method onLoadFinished (lines 137–140) is called by the LoaderManager
after a Loader
finishes loading its data, so you can process the results in the Cursor
argument. In this case, we call the ContactsAdapter
’s swapCursor
method with the Cursor
as an argument, so the ContactsAdapter
can refresh the RecyclerView
based on the new Cursor
contents.
Method onLoaderReset (lines 143–146) is called by the LoaderManager
when a Loader
is reset and its data is no longer available. At this point, the app should immediately disconnect from the data. In this case, we call the ContactsAdapter
’s swapCursor
method with the argument null
to indicate that there is no data to bind to the RecyclerView
.
In Section 8.6, we discussed how to create a RecyclerView.Adapter
that’s used to bind data to a RecyclerView
. Here we highlight only the new code that helps the ContactsAdapter
(Fig. 9.39) to populate the RecyclerView
with contact names from a Cursor
.
1 // ContactsAdapter.java
2 // Subclass of RecyclerView.Adapter that binds contacts to RecyclerView
3 package com.deitel.addressbook;
4
5 import android.database.Cursor;
6 import android.net.Uri;
7 import android.support.v7.widget.RecyclerView;
8 import android.view.LayoutInflater;
9 import android.view.View;
10 import android.view.ViewGroup;
11 import android.widget.TextView;
12
13 import com.deitel.addressbook.data.DatabaseDescription.Contact;
14
15 public class ContactsAdapter
16 extends RecyclerView.Adapter<ContactsAdapter.ViewHolder> {
17
18 // interface implemented by ContactsFragment to respond
19 // when the user touches an item in the RecyclerView
20 public interface ContactClickListener {
21 void onClick(Uri contactUri);
22 }
23
24 // nested subclass of RecyclerView.ViewHolder used to implement
25 // the view-holder pattern in the context of a RecyclerView
26 public class ViewHolder extends RecyclerView.ViewHolder {
27 public final TextView textView;
28 private long rowID;
29
30 // configures a RecyclerView item's ViewHolder
31 public ViewHolder(View itemView) {
32 super(itemView);
33 textView = (TextView) itemView.findViewById(android.R.id.text1);
34
35 // attach listener to itemView
36 itemView.setOnClickListener(
37 new View.OnClickListener() {
38 // executes when the contact in this ViewHolder is clicked
39 @Override
40 public void onClick(View view) {
41 clickListener.onClick(Contact.buildContactUri(rowID));
42 }
43 }
44 );
45 }
46
47 // set the database row ID for the contact in this ViewHolder
48 public void setRowID(long rowID) {
49 this.rowID = rowID;
50 }
51 }
52
53 // ContactsAdapter instance variables
54 private Cursor cursor = null;
55 private final ContactClickListener clickListener;
56
57 // constructor
58 public ContactsAdapter(ContactClickListener clickListener) {
59 this.clickListener = clickListener;
60 }
61
62 // sets up new list item and its ViewHolder
63 @Override
64 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
65 // inflate the android.R.layout.simple_list_item_1 layout
66 View view = LayoutInflater.from(parent.getContext()).inflate(
67 android.R.layout.simple_list_item_1, parent, false);
68 return new ViewHolder(view); // return current item's ViewHolder
69 }
70
71 // sets the text of the list item to display the search tag
72 @Override
73 public void onBindViewHolder(ViewHolder holder, int position) {
74 cursor.moveToPosition(position);
75 holder.setRowID(cursor.getLong(cursor.getColumnIndex(Contact._ID)));
76 holder.textView.setText(cursor.getString(cursor.getColumnIndex(
77 Contact.COLUMN_NAME)));
78 }
79
80 // returns the number of items that adapter binds
81 @Override
82 public int getItemCount() {
83 return (cursor != null) ? cursor.getCount() : 0;
84 }
85
86 // swap this adapter's current Cursor for a new one
87 public void swapCursor(Cursor cursor) {
88 this.cursor = cursor;
89 notifyDataSetChanged();
90 }
91 }
Lines 20–22 define the nested interface ContactClickListener
that class ContactsFragment
implements to be notified when the user touches a contact in the RecyclerView
. Each item in the RecyclerView
has a click listener that calls the ContactClickListener
’s onClick
method and passes the selected contact’s Uri
. The ContactsFragment
then notifies the MainActivity
that a contact was selected, so the MainActivity
can display the contact in a DetailFragment
.
Class ViewHolder
(lines 26–51) maintains a reference to a RecyclerView
item’s TextView
and the database’s rowID
for the corresponding contact. The rowID
is necessary because we sort the contacts before displaying them, so each contact’s position number in the RecyclerView
most likely does not match the contact’s row ID in the database. ViewHolder
’s constructor stores a reference to the RecyclerView
item’s TextView
and sets its View.OnClickListener
, which passes the contact’s URI to the adapter’s ContactClickListener
.
Method onCreateViewHolder
(lines 63–69) inflates the GUI for a ViewHolder
object. In this case we used the predefined layout android.R.layout.simple_list_item_1
, which defines a layout containing one TextView
named text1
.
Method onBindViewHolder
(lines 72–78) uses Cursor
method moveToPosition to move to the contact that corresponds to the current RecyclerView
item’s position. Line 75 sets the ViewHolder
’s rowID
. To get this value, we use Cursor
method getColumnIndex to look up the column number of the Contact._ID
column. We then pass that number to Cursor
method getLong to get the contact’s row ID. Lines 76–77 set the text for the ViewHolder
’s textView
, using a similar process—in this case, look up the column number for the Contact.COLUMN_NAME
column, then call Cursor
method getString to get the contact’s name.
Method getItemCount
(lines 81–84) returns the total number of rows in the Cursor
or 0 if Cursor
is null
.
Method swapCursor
(lines 87–90) replaces the adapter’s current Cursor
and notifies the adapter that its data changed. This method is called from the ContactsFragment
’s onLoadFinished
and onLoaderReset
methods.
The AddEditFragment
class provides a GUI for adding new contacts or editing existing ones. Many of the programming concepts used in this class have been presented earlier in this chapter or in prior chapters, so we focus here only on the new features.
Figure 9.40 lists the package
statement, import
statements and the beginning of the AddEditFragment
class definition. The class extends Fragment
and implements the LoaderManager.LoaderCallbacks<Cursor>
interface to respond to LoaderManager
events.
1 // AddEditFragment.java
2 // Fragment for adding a new contact or editing an existing one
3 package com.deitel.addressbook;
4
5 import android.content.ContentValues;
6 import android.content.Context;
7 import android.database.Cursor;
8 import android.net.Uri;
9 import android.os.Bundle;
10 import android.support.design.widget.CoordinatorLayout;
11 import android.support.design.widget.FloatingActionButton;
12 import android.support.design.widget.Snackbar;
13 import android.support.design.widget.TextInputLayout;
14 import android.support.v4.app.Fragment;
15 import android.support.v4.app.LoaderManager;
16 import android.support.v4.content.CursorLoader;
17 import android.support.v4.content.Loader;
18 import android.text.Editable;
19 import android.text.TextWatcher;
20 import android.view.LayoutInflater;
21 import android.view.View;
22 import android.view.ViewGroup;
23 import android.view.inputmethod.InputMethodManager;
24
25 import com.deitel.addressbook.data.DatabaseDescription.Contact;
26
27 public class AddEditFragment extends Fragment
28 implements LoaderManager.LoaderCallbacks<Cursor> {
29
Figure 9.41 declares the nested interface AddEditFragmentListener
containing the callback method onAddEditCompleted
. MainActivity
implements this interface to be notified when the user saves a new contact or saves changes to an existing one.
30 // defines callback method implemented by MainActivity
31 public interface AddEditFragmentListener {
32 // called when contact is saved
33 void onAddEditCompleted(Uri contactUri);
34 }
35
Figure 9.42 lists the class’s fields:
• The constant CONTACT_LOADER
(line 37) identifies the Loader
that queries the AddressBookContentProvider
to retrieve one contact for editing.
• The instance variable listener
(line 39) refers to the AddEditFragmentListener
(MainActivity
) that’s notified when the user saves a new or updated contact.
• The instance variable contactUri
(line 40) represents the contact to edit.
• The instance variable addingNewContact
(line 41) specifies whether a new contact is being added (true
) or an existing contact is being edited (false
).
• The instance variables at lines 44–53 refer to the Fragment
’s TextInputLayout
s, FloatingActionButton
and CoordinatorLayout
.
36 // constant used to identify the Loader
37 private static final int CONTACT_LOADER = 0;
38
39 private AddEditFragmentListener listener; // MainActivity
40 private Uri contactUri; // Uri of selected contact
41 private boolean addingNewContact = true; // adding (true) or editing
42
43 // EditTexts for contact information
44 private TextInputLayout nameTextInputLayout;
45 private TextInputLayout phoneTextInputLayout;
46 private TextInputLayout emailTextInputLayout;
47 private TextInputLayout streetTextInputLayout;
48 private TextInputLayout cityTextInputLayout;
49 private TextInputLayout stateTextInputLayout;
50 private TextInputLayout zipTextInputLayout;
51 private FloatingActionButton saveContactFAB;
52
53 private CoordinatorLayout coordinatorLayout; // used with SnackBars
54
Figure 9.43 contains the overridden Fragment
methods onAttach
, onDetach
and onCreateView
. Methods onAttach
and onDetach
set instance variable listener
to refer to the host Activity
when the AddEditFragment
is attached and to set listener
to null
when the AddEditFragment
is detached.
55 // set AddEditFragmentListener when Fragment attached
56 @Override
57 public void onAttach(Context context) {
58 super.onAttach(context);
59 listener = (AddEditFragmentListener) context;
60 }
61
62 // remove AddEditFragmentListener when Fragment detached
63 @Override
64 public void onDetach() {
65 super.onDetach();
66 listener = null;
67 }
68
69 // called when Fragment's view needs to be created
70 @Override
71 public View onCreateView(
72 LayoutInflater inflater, ViewGroup container,
73 Bundle savedInstanceState) {
74 super.onCreateView(inflater, container, savedInstanceState);
75 setHasOptionsMenu(true); // fragment has menu items to display
76
77 // inflate GUI and get references to EditTexts
78 View view =
79 inflater.inflate(R.layout.fragment_add_edit, container, false);
80 nameTextInputLayout =
81 (TextInputLayout) view.findViewById(R.id.nameTextInputLayout);
82 nameTextInputLayout.getEditText().addTextChangedListener(
83 nameChangedListener);
84 phoneTextInputLayout =
85 (TextInputLayout) view.findViewById(R.id.phoneTextInputLayout);
86 emailTextInputLayout =
87 (TextInputLayout) view.findViewById(R.id.emailTextInputLayout);
88 streetTextInputLayout =
89 (TextInputLayout) view.findViewById(R.id.streetTextInputLayout);
90 cityTextInputLayout =
91 (TextInputLayout) view.findViewById(R.id.cityTextInputLayout);
92 stateTextInputLayout =
93 (TextInputLayout) view.findViewById(R.id.stateTextInputLayout);
94 zipTextInputLayout =
95 (TextInputLayout) view.findViewById(R.id.zipTextInputLayout);
96
97 // set FloatingActionButton's event listener
98 saveContactFAB = (FloatingActionButton) view.findViewById(
99 R.id.saveFloatingActionButton);
100 saveContactFAB.setOnClickListener(saveContactButtonClicked);
101 updateSaveButtonFAB();
102
103 // used to display SnackBars with brief messages
104 coordinatorLayout = (CoordinatorLayout) getActivity().findViewById(
105 R.id.coordinatorLayout);
106
107 Bundle arguments = getArguments(); // null if creating new contact
108
109 if (arguments != null) {
110 addingNewContact = false;
111 contactUri = arguments.getParcelable(MainActivity.CONTACT_URI);
112 }
113
114 // if editing an existing contact, create Loader to get the contact
115 if (contactUri != null)
116 getLoaderManager().initLoader(CONTACT_LOADER, null, this);
117
118 return view;
119 }
120
Method onCreateView
inflates the GUI and gets references to the Fragment
’s TextInputLayout
s and configures the FloatingActionButton
. Next, we use Fragment
method getArguments
to get the Bundle
of arguments (line 107). When we launch the AddEditFragment
from the MainActivity
, we pass null
for the Bundle
argument, because the user is adding a new contact’s information. In this case, getArguments
returns null
. If getArguments
returns a Bundle
(line 109), then the user is editing an existing contact. Line 111 reads the contact’s Uri
from the Bundle
by calling method getParcelable
. If contactUri
is not null
, line 116 uses the Fragment
’s LoaderManager
to initialize a Loader
that the AddEditFragment
will use to get the data for the contact being edited.
Figure 9.44 shows the TextWatcher nameChangedListener
and method updatedSaveButtonFAB
. The listener calls method updatedSaveButtonFAB
when the user edits the text in the nameTextInputLayout
’s EditText
. The name must be non-empty in this app, so method updatedSaveButtonFAB
displays the FloatingActionButton
only when the nameTextInputLayout
’s EditText
is not empty.
121 // detects when the text in the nameTextInputLayout's EditText changes
122 // to hide or show saveButtonFAB
123 private final TextWatcher nameChangedListener = new TextWatcher() {
124 @Override
125 public void beforeTextChanged(CharSequence s, int start, int count,
126 int after) {}
127
128 // called when the text in nameTextInputLayout changes
129 @Override
130 public void onTextChanged(CharSequence s, int start, int before,
131 int count) {
132 updateSaveButtonFAB();
133 }
134
135 @Override
136 public void afterTextChanged(Editable s) { }
137 };
138
139 // shows saveButtonFAB only if the name is not empty
140 private void updateSaveButtonFAB() {
141 String input =
142 nameTextInputLayout.getEditText().getText().toString();
143
144 // if there is a name for the contact, show the FloatingActionButton
145 if (input.trim().length() != 0)
146 saveContactFAB.show();
147 else
148 saveContactFAB.hide();
149 }
150
When the user touches this Fragment
’s FloatingActionButton
, the saveContactButtonClicked
listener (Fig. 9.45, lines 152–162) executes. Method onClick
hides the keyboard (lines 157–159), then calls method saveContact
.
151 // responds to event generated when user saves a contact
152 private final View.OnClickListener saveContactButtonClicked =
153 new View.OnClickListener() {
154 @Override
155 public void onClick(View v) {
156 // hide the virtual keyboard
157 ((InputMethodManager) getActivity().getSystemService(
158 Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(
159 getView().getWindowToken(), 0);
160 saveContact(); // save contact to the database
161 }
162 };
163
164 // saves contact information to the database
165 private void saveContact() {
166 // create ContentValues object containing contact's key-value pairs
167 ContentValues contentValues = new ContentValues();
168 contentValues.put(Contact.COLUMN_NAME,
169 nameTextInputLayout.getEditText().getText().toString());
170 contentValues.put(Contact.COLUMN_PHONE,
171 phoneTextInputLayout.getEditText().getText().toString());
172 contentValues.put(Contact.COLUMN_EMAIL,
173 emailTextInputLayout.getEditText().getText().toString());
174 contentValues.put(Contact.COLUMN_STREET,
175 streetTextInputLayout.getEditText().getText().toString());
176 contentValues.put(Contact.COLUMN_CITY,
177 cityTextInputLayout.getEditText().getText().toString());
178 contentValues.put(Contact.COLUMN_STATE,
179 stateTextInputLayout.getEditText().getText().toString());
180 contentValues.put(Contact.COLUMN_ZIP,
181 zipTextInputLayout.getEditText().getText().toString());
182
183 if (addingNewContact) {
184 // use Activity's ContentResolver to invoke
185 // insert on the AddressBookContentProvider
186 Uri newContactUri = getActivity().getContentResolver().insert(
187 Contact.CONTENT_URI, contentValues);
188
189 if (newContactUri != null) {
190 Snackbar.make(coordinatorLayout,
191 R.string.contact_added, Snackbar.LENGTH_LONG).show();
192 listener.onAddEditCompleted(newContactUri);
193 }
194 else {
195 Snackbar.make(coordinatorLayout,
196 R.string.contact_not_added, Snackbar.LENGTH_LONG).show();
197 }
198 }
199 else {
200 // use Activity's ContentResolver to invoke
201 // insert on the AddressBookContentProvider
202 int updatedRows = getActivity().getContentResolver().update(
203 contactUri, contentValues, null, null);
204
205 if (updatedRows > 0) {
206 listener.onAddEditCompleted(contactUri);
207 Snackbar.make(coordinatorLayout,
208 R.string.contact_updated, Snackbar.LENGTH_LONG).show();
209 }
210 else {
211 Snackbar.make(coordinatorLayout,
212 R.string.contact_not_updated, Snackbar.LENGTH_LONG).show();
213 }
214 }
215 }
216
The saveContact
method (lines 165–215) creates a ContentValues
object (line 167) and adds to it key–value pairs representing the column names and values to be inserted into or updated in the database (lines 168–181). If the user is adding a new contact (lines 183–198), lines 186–187 use ContentResolver
method insert to invoke insert
on the AddressBookContentProvider
and place the new contact into the database. If the insert is successful, the returned Uri
is non-null
and lines 190–192 display a SnackBar
indicating that the contact was added, then notify the AddEditFragmentListener
with the contact that was added. Recall that when the app is running on a tablet, this results in the contact’s data being displayed in a DetailFragment
next to the ContactsFragment
. If the insert is not successful, lines 195–196 display an appropriate SnackBar
.
If the user is editing an existing contact (lines 199–214), lines 202–203 use ContentResolver
method update to invoke update
on the AddressBookContentProvider
and store the edited contact’s data. If the update is successful, the returned integer is greater than 0 (indicating the specific number of rows updated) and lines 206–208 notify the AddEditFragmentListener
with the contact that was edited, then display an appropriate message. If the updated is not successful, lines 211–212 display an appropriate SnackBar
.
Figure 9.46 presents the AddEditFragment
’s implementations of the methods in interface LoaderManager.LoaderCallbacks<Cursor>
. These methods are used in class AddEditFragment
only when the user is editing an existing contact. Method onCreateLoader
(lines 219–233) creates a CursorLoader
for the specific contact being edited. Method onLoadFinished
(lines 236–267) checks whether the cursor is non-null
and, if so, calls cursor
method moveToFirst. If this method returns true
, then a contact matching the contactUri
was found in the database and lines 241–263 get the contact’s information from the Cursor
and display it in the GUI. Method onLoaderReset
is not needed in AddEditFragment
, so it does nothing.
217 // called by LoaderManager to create a Loader
218 @Override
219 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
220 // create an appropriate CursorLoader based on the id argument;
221 // only one Loader in this fragment, so the switch is unnecessary
222 switch (id) {
223 case CONTACT_LOADER:
224 return new CursorLoader(getActivity(),
225 contactUri, // Uri of contact to display
226 null, // null projection returns all columns
227 null, // null selection returns all rows
228 null, // no selection arguments
229 null); // sort order
230 default:
231 return null;
232 }
233 }
234
235 // called by LoaderManager when loading completes
236 @Override
237 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
238 // if the contact exists in the database, display its data
239 if (data != null && data.moveToFirst()) {
240 // get the column index for each data item
241 int nameIndex = data.getColumnIndex(Contact.COLUMN_NAME);
242 int phoneIndex = data.getColumnIndex(Contact.COLUMN_PHONE);
243 int emailIndex = data.getColumnIndex(Contact.COLUMN_EMAIL);
244 int streetIndex = data.getColumnIndex(Contact.COLUMN_STREET);
245 int cityIndex = data.getColumnIndex(Contact.COLUMN_CITY);
246 int stateIndex = data.getColumnIndex(Contact.COLUMN_STATE);
247 int zipIndex = data.getColumnIndex(Contact.COLUMN_ZIP);
248
249 // fill EditTexts with the retrieved data
250 nameTextInputLayout.getEditText().setText(
251 data.getString(nameIndex));
252 phoneTextInputLayout.getEditText().setText(
253 data.getString(phoneIndex));
254 emailTextInputLayout.getEditText().setText(
255 data.getString(emailIndex));
256 streetTextInputLayout.getEditText().setText(
257 data.getString(streetIndex));
258 cityTextInputLayout.getEditText().setText(
259 data.getString(cityIndex));
260 stateTextInputLayout.getEditText().setText(
261 data.getString(stateIndex));
262 zipTextInputLayout.getEditText().setText(
263 data.getString(zipIndex));
264
265 updateSaveButtonFAB();
266 }
267 }
268
269 // called by LoaderManager when the Loader is being reset
270 @Override
271 public void onLoaderReset(Loader<Cursor> loader) { }
272 }
The DetailFragment
class displays one contact’s information and provides menu items on the app bar that enable the user to edit or delete that contact.
Figure 9.47 lists the package
statement, import
statements and the beginning of the DetailFragment
class definition. The class extends Fragment
and implements the LoaderManager.LoaderCallbacks<Cursor>
interface to respond to LoaderManager
events.
1 // DetailFragment.java
2 // Fragment subclass that displays one contact's details
3 package com.deitel.addressbook;
4
5 import android.app.AlertDialog;
6 import android.app.Dialog;
7 import android.content.Context;
8 import android.content.DialogInterface;
9 import android.database.Cursor;
10 import android.net.Uri;
11 import android.os.Bundle;
12 import android.support.v4.app.DialogFragment;
13 import android.support.v4.app.Fragment;
14 import android.support.v4.app.LoaderManager;
15 import android.support.v4.content.CursorLoader;
16 import android.support.v4.content.Loader;
17 import android.view.LayoutInflater;
18 import android.view.Menu;
19 import android.view.MenuInflater;
20 import android.view.MenuItem;
21 import android.view.View;
22 import android.view.ViewGroup;
23 import android.widget.TextView;
24
25 import com.deitel.addressbook.data.DatabaseDescription.Contact;
26
27 public class DetailFragment extends Fragment
28 implements LoaderManager.LoaderCallbacks<Cursor> {
29
Figure 9.48 declares the nested interface DetailFragmentListener
containing the callback methods that MainActivity
implements to be notified when the user deletes a contact (line 32) and when the user touches the edit menu item to edit a contact (line 35).
30 // callback methods implemented by MainActivity
31 public interface DetailFragmentListener {
32 void onContactDeleted(); // called when a contact is deleted
33
34 // pass Uri of contact to edit to the DetailFragmentListener
35 void onEditContact(Uri contactUri);
36 }
37
Figure 9.49 shows the class’s fields:
• The constant CONTACT_LOADER
(line 38) identifies the Loader
that queries the AddressBookContentProvider
to retrieve one contact to display.
• The instance variable listener
(line 40) refers to the DetailFragmentListener
(MainActivity
) that’s notified when the user deletes a contact or initiates editing of a contact.
• The instance variable contactUri
(line 41) represents the contact to display.
• The instance variables at lines 43–49 refer to the Fragment
’s TextView
s.
38 private static final int CONTACT_LOADER = 0; // identifies the Loader
39
40 private DetailFragmentListener listener; // MainActivity
41 private Uri contactUri; // Uri of selected contact
42
43 private TextView nameTextView; // displays contact's name
44 private TextView phoneTextView; // displays contact's phone
45 private TextView emailTextView; // displays contact's email
46 private TextView streetTextView; // displays contact's street
47 private TextView cityTextView; // displays contact's city
48 private TextView stateTextView; // displays contact's state
49 private TextView zipTextView; // displays contact's zip
50
Figure 9.50 contains overridden Fragment
methods onAttach
, onDetach
and onCreateView
. Methods onAttach
and onDetach
set instance variable listener
to refer to the host Activity
when the DetailFragment
is attached and to set listener
to null
when the DetailFragment
is detached. The onCreateView
method (lines 66–95) obtains the selected contact’s Uri
(lines 74–77). Lines 80–90 inflate the GUI and get references to the TextView
s. Line 93 uses the Fragment
’s LoaderManager
to initialize a Loader
that the DetailFragment
will use to get the data for the contact to display.
51 // set DetailFragmentListener when fragment attached
52 @Override
53 public void onAttach(Context context) {
54 super.onAttach(context);
55 listener = (DetailFragmentListener) context;
56 }
57
58 // remove DetailFragmentListener when fragment detached
59 @Override
60 public void onDetach() {
61 super.onDetach();
62 listener = null;
63 }
64
65 // called when DetailFragmentListener's view needs to be created
66 @Override
67 public View onCreateView(
68 LayoutInflater inflater, ViewGroup container,
69 Bundle savedInstanceState) {
70 super.onCreateView(inflater, container, savedInstanceState);
71 setHasOptionsMenu(true); // this fragment has menu items to display
72
73 // get Bundle of arguments then extract the contact's Uri
74 Bundle arguments = getArguments();
75
76 if (arguments != null)
77 contactUri = arguments.getParcelable(MainActivity.CONTACT_URI);
78
79 // inflate DetailFragment's layout
80 View view =
81 inflater.inflate(R.layout.fragment_detail, container, false);
82
83 // get the EditTexts
84 nameTextView = (TextView) view.findViewById(R.id.nameTextView);
85 phoneTextView = (TextView) view.findViewById(R.id.phoneTextView);
86 emailTextView = (TextView) view.findViewById(R.id.emailTextView);
87 streetTextView = (TextView) view.findViewById(R.id.streetTextView);
88 cityTextView = (TextView) view.findViewById(R.id.cityTextView);
89 stateTextView = (TextView) view.findViewById(R.id.stateTextView);
90 zipTextView = (TextView) view.findViewById(R.id.zipTextView);
91
92 // load the contact
93 getLoaderManager().initLoader(CONTACT_LOADER, null, this);
94 return view;
95 }
96
The DetailFragment
displays in the app bar options for editing the current contact and for deleting it. Method onCreateOptionsMenu
(Fig. 9.51, lines 98–102) inflates the menu resource file fragment_details_menu.xml
. Method onOptionsItemSelected
(lines 105–117) uses the selected MenuItem
’s resource ID to determine which one was selected. If the user touched the edit option (), line 109 calls the DetailFragmentListener
’s onEditContact
method with the contactUri
—MainActivity
passes this to the AddEditFragment
. If the user touched the delete option (), line 112 calls method deleteContact
(Fig. 9.52).
97 // display this fragment's menu items
98 @Override
99 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
100 super.onCreateOptionsMenu(menu, inflater);
101 inflater.inflate(R.menu.fragment_details_menu, menu);
102 }
103
104 // handle menu item selections
105 @Override
106 public boolean onOptionsItemSelected(MenuItem item) {
107 switch (item.getItemId()) {
108 case R.id.action_edit:
109 listener.onEditContact(contactUri); // pass Uri to listener
110 return true;
111 case R.id.action_delete:
112 deleteContact();
113 return true;
114 }
115
116 return super.onOptionsItemSelected(item);
117 }
118
Method deleteContact
(Fig. 9.52, lines 120–123) displays a DialogFragment
(lines 126–157) asking the user to confirm that the currently displayed contact should be deleted. If the user touches DELETE in the dialog, lines 147–148 call ContentResolver
method delete (lines 147–148) to invoke the AddressBookContentProvider
’s delete
method and remove the contact from the database. Method delete receives the Uri
of the content to delete, a String
representing the WHERE
clause that determines what to delete and a String
array of arguments to insert in the WHERE
clause. In this case, the last two arguments are null
, because the row ID of the contact to delete is embedded in the Uri
—this row ID is extracted from the Uri
by the AddressBookContentProvider
’s delete
method. Line 149 calls the listener
’s onContactDeleted
method so that MainActivity
can remove the DetailFragment
from the screen.
119 // delete a contact
120 private void deleteContact() {
121 // use FragmentManager to display the confirmDelete DialogFragment
122 confirmDelete.show(getFragmentManager(), "confirm delete");
123 }
124
125 // DialogFragment to confirm deletion of contact
126 private final DialogFragment confirmDelete =
127 new DialogFragment() {
128 // create an AlertDialog and return it
129 @Override
130 public Dialog onCreateDialog(Bundle bundle) {
131 // create a new AlertDialog Builder
132 AlertDialog.Builder builder =
133 new AlertDialog.Builder(getActivity());
134
135 builder.setTitle(R.string.confirm_title);
136 builder.setMessage(R.string.confirm_message);
137
138 // provide an OK button that simply dismisses the dialog
139 builder.setPositiveButton(R.string.button_delete,
140 new DialogInterface.OnClickListener() {
141 @Override
142 public void onClick(
143 DialogInterface dialog, int button) {
144
145 // use Activity's ContentResolver to invoke
146 // delete on the AddressBookContentProvider
147 getActivity().getContentResolver().delete(
148 contactUri, null, null);
149 listener.onContactDeleted(); // notify listener
150 }
151 }
152 );
153
154 builder.setNegativeButton(R.string.button_cancel, null);
155 return builder.create(); // return the AlertDialog
156 }
157 };
158
Figure 9.53 presents the DetailFragment
’s implementations of the methods in interface LoaderManager.LoaderCallbacks<Cursor>
. Method onCreateLoader
(lines 160–181) creates a CursorLoader
for the specific contact being displayed. Method onLoadFinished
(lines 184–206) checks whether the cursor is non-null
and, if so, calls cursor
method moveToFirst
. If this method returns true
, then a contact matching the contactUri
was found in the database and lines 189–204 get the contact’s information from the Cursor
and display it in the GUI. Method onLoaderReset
is not needed in DetailFragment
, so it does nothing.
159 // called by LoaderManager to create a Loader
160 @Override
161 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
162 // create an appropriate CursorLoader based on the id argument;
163 // only one Loader in this fragment, so the switch is unnecessary
164 CursorLoader cursorLoader;
165
166 switch (id) {
167 case CONTACT_LOADER:
168 cursorLoader = new CursorLoader(getActivity(),
169 contactUri, // Uri of contact to display
170 null, // null projection returns all columns
171 null, // null selection returns all rows
172 null, // no selection arguments
173 null); // sort order
174 break;
175 default:
176 cursorLoader = null;
177 break;
178 }
179
180 return cursorLoader;
181 }
182
183 // called by LoaderManager when loading completes
184 @Override
185 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
186 // if the contact exists in the database, display its data
187 if (data != null && data.moveToFirst()) {
188 // get the column index for each data item
189 int nameIndex = data.getColumnIndex(Contact.COLUMN_NAME);
190 int phoneIndex = data.getColumnIndex(Contact.COLUMN_PHONE);
191 int emailIndex = data.getColumnIndex(Contact.COLUMN_EMAIL);
192 int streetIndex = data.getColumnIndex(Contact.COLUMN_STREET);
193 int cityIndex = data.getColumnIndex(Contact.COLUMN_CITY);
194 int stateIndex = data.getColumnIndex(Contact.COLUMN_STATE);
195 int zipIndex = data.getColumnIndex(Contact.COLUMN_ZIP);
196
197 // fill TextViews with the retrieved data
198 nameTextView.setText(data.getString(nameIndex));
199 phoneTextView.setText(data.getString(phoneIndex));
200 emailTextView.setText(data.getString(emailIndex));
201 streetTextView.setText(data.getString(streetIndex));
202 cityTextView.setText(data.getString(cityIndex));
203 stateTextView.setText(data.getString(stateIndex));
204 zipTextView.setText(data.getString(zipIndex));
205 }
206 }
207
208 // called by LoaderManager when the Loader is being reset
209 @Override
210 public void onLoaderReset(Loader<Cursor> loader) { }
211 }
In this chapter, you created an Address Book app for adding, viewing, editing and deleting contact information that’s stored in a SQLite database.
You used one activity to host all of the app’s Fragment
s. On a phone-sized device, you displayed one Fragment
at a time. On a tablet, the activity displayed the Fragment
containing the contact list, and you replaced that with Fragment
s for viewing, adding and editing contacts as necessary. You used the FragmentManager
and FragmentTransaction
s to dynamically display Fragment
s. You used Android’s Fragment
back stack to provide automatic support for Android’s back button. To communicate data between Fragment
s and the host activity, you defined in each Fragment
subclass a nested interface of callback methods that the host activity implemented.
You used a subclass of SQLiteOpenHelper
to simplify creating the database and to obtain a SQLiteDatabase
object for manipulating the database’s contents. You also managed database query results via a Cursor
(package android.database).
To access the database asynchronously outside the GUI thread, you defined a subclass of ContentProvider
that specified how to query, insert, update and delete data. When changes were made to the SQLite database, the ContentProvider
notified listeners so data could be updated in the GUI. The ContentProvider
defined Uri
s that it used to determine the tasks to perform.
To invoke the ContentProvider
’s query
, insert
, update
and delete
capabilities, we invoked the corresponding methods of the activity’s built-in ContentResolver
. You saw that the ContentProvider
and ContentResolver
handle communication for you. The ContentResolver
’s methods received as their first argument a Uri
that specified the ContentProvider
to access. Each ContentResolver
method invoked the corresponding method of the ContentProvider
, which in turn used the Uri
to help determine the task to perform.
As we’ve stated previously, long-running operations or operations that block execution until they complete (e.g., file and database access) should be performed outside the GUI thread. You used a CursorLoader
to perform asynchronous data access. You learned that Loader
s are created and managed by an Activity
’s or Fragment
’s LoaderManager
, which ties each Loader
’s lifecycle to that of its Activity
or Fragment
. You implmeneted interface LoaderManager.LoaderCallbacks
to respond to Loader
events indicating when a Loader should be created, finishes loading its data, or is reset and the data is no longer available.
You defined common GUI component attribute–value pairs as a style
resource, then applied the style
to the TextView
s that display a contact’s information. You also defined a border for a TextView
by specifying a Drawable
for the TextView
’s background
. The Drawable
could be an image, but in this app you defined the Drawable
as a shape
in a resource file.
In Chapter 10, we discuss the business side of Android app development. You’ll see how to prepare your app for submission to Google Play, including making icons. We’ll discuss how to test your apps on devices and publish them on Google Play. We discuss the characteristics of great apps and the Android design guidelines to follow. We provide tips for pricing and marketing your app. We also review the benefits of offering your app for free to drive sales of other products, such as a more feature-rich version of the app or premium content. We show how to use Google Play to track app sales, payments and more.
9.1 Fill in the blanks in each of the following statements:
a) SQLite database query results are managed via a(n) ____________ (package android.database
).
b) A(n) ____________ exposes an app’s data for use in that app or in other apps.
c) Fragment
method ____________ returns the Bundle
of arguments to the Fragment
.
d) The Cursor
returned by method query
contains all the table rows that match the method’s arguments—the so-called ____________.
e) A FragmentTransaction
(package android.app
) obtained from the ____________ allows an Activity
to add, remove and transition between Fragment
s.
f) ____________ and the ____________ help you perform asynchronous data access from any Activity
or Fragment
.
9.2 State whether each of the following is true or false. If false, explain why.
a) It’s good practice to release resources like database connections when they are not being used so that other activities can use the resources.
b) It’s considered good practice to ensure that Cursor
method moveToFirst
returns false
before attempting to get data from the Cursor
.
c) A ContentProvider
defines Uri
s that help determine the task to perform when the ContentProvider
receives a request.
d) An Activity
’s or Fragment
’s LoaderManager
s are tied to the Activity
’s or Fragment
’s lifecycle.
e) To invoke a ContentProvider
’s query
, insert
, update
and delete
capabilities, you use the corresponding methods of a ContentResolver
.
f) You must coordinate comminication between a ContentProvider
and ContentResolver
.
a) Cursor
.
b) ContentProvider
.
c) getArguments
.
d) result set.
e) FragmentManager
.
f) Loader
s, LoaderManager
.
a) True.
b) False. It’s considered good practice to ensure that Cursor
method moveToFirst
returns true
before attempting to get data from the Cursor
.
c) True.
d) False. An Activity
’s or Fragment
’s Loader
s are created and managed by its LoaderManager
(package android.app
), which ties each Loader
’s lifecycle to its Activity
’s or Fragment
’s lifecycle.
e) True.
f) False. A ContentProvider
and ContentResolver
handle communication for you—including between apps if your ContentProvider
exposes its data to other apps.
9.3 (Flag Quiz App Modification) Revise the Flag Quiz app to use one Activity
, dynamic Fragment
s and FragmentTransaction
s as you did in the Address Book app.
9.4 (Movie Collection App) Using the techniques you learned in this chapter, create an app that allows you to enter information about your movie collection. Provide fields for the title, year, director and any other fields you’d like to track. The app should provide similar activities to the Address Book app for viewing the list of movies (in alphabetical order), adding and/or updating the information for a movie and viewing the details of a movie.
9.5 (Recipe App) Using the techniques you learned in this chapter, create a cooking recipe app. Provide fields for the recipe name, category (e.g., appetizer, entree, desert, salad, side dish), a list of the ingredients and instructions for preparing the dish. The app should provide similar activities to the Address Book app for viewing the list of recipes (in alphabetical order), adding and/or updating a recipe and viewing the details of a recipe.
9.6 (Shopping List App) Create an app that allows the user to enter and edit a shopping list. Include a favorites feature that allows the user to easily add items purchased frequently. Include an optional feature to input a price for each item and a quantity so the user can track the total cost of all of the items on the list.
9.7 (Expense Tracker App) Create an app that allows the user to keep track of personal expenses. Provide categories for classifying each expense (e.g., monthly expenses, travel, entertainment, necessities). Provide an option for tagging recurring expenses that automatically adds the expense to a calendar at the proper frequency (daily, weekly, monthly or yearly). Optional: Investigate Android’s status-bar notifications mechanism at developer.android.com/guide/topics/ui/notifiers/index.html
. Provide notifications to remind the user when a bill is due.
9.8 (Cooking with Healthier Ingredients App) Obesity in the United States is increasing at an alarming rate. Check the map at http://stateofobesity.org/adult-obesity/
, which shows adult obesity trends in the United States since 1990. As obesity increases, so do occurrences of related problems (e.g., heart disease, high blood pressure, high cholesterol, type 2 diabetes). Create an app that helps users choose healthier ingredients when cooking, and helps those allergic to certain foods (e.g., nuts, gluten) find substitutes. The app should allow the user to enter a recipe, then should suggest healthier replacements for some of the ingredients. For simplicity, your app should assume the recipe has no abbreviations for measures such as teaspoons, cups, and tablespoons, and uses numerical digits for quantities (e.g., 1 egg, 2 cups) rather than spelling them out (one egg, two cups). Some common substitutions are shown in Fig. 9.54. Your app should display a warning such as, “Always consult your physician before making significant changes to your diet.”
The app should take into consideration that replacements are not always one-for-one. For example, if a cake recipe calls for three eggs, it might reasonably use six egg whites instead. Conversion data for measurements and substitutes can be obtained at websites such as:
http://chinesefood.about.com/od/recipeconversionfaqs/f/usmetricrecipes.htm
Your app should consider the user’s health concerns, such as high cholesterol, high blood pressure, weight loss, gluten allergy, and so on. For high cholesterol, the app should suggest substitutes for eggs and dairy products; if the user wishes to lose weight, low-calorie substitutes for ingredients such as sugar should be suggested.