9.10 ContactsFragment Class

Class ContactsFragment displays the contact list in a RecyclerView and provides a FloatingActionButton that the user can touch to add a new contact.

9.10.1 Superclass and Implemented Interface

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


Fig. 9.31 | ContactsFragment superclass and implemented interface.

9.10.2 ContactsFragmentListener

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


Fig. 9.32 | Nested interface ContactsFragmentListener.

9.10.3 Fields

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


Fig. 9.33 | ContactsFragment fields.

9.10.4 Overridden Fragment Method onCreateView

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


Fig. 9.34 | Overridden Fragment method onCreateView.

9.10.5 Overridden Fragment Methods onAttach and onDetach

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


Fig. 9.35 | Overridden Fragment methods onAttach and onDetach.

9.10.6 Overridden Fragment Method onActivityCreated

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


Fig. 9.36 | Overridden Fragment method onActivityCreated.

9.10.7 Method updateContactList

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


Fig. 9.37 | ContactsFragment method updateContactList.

9.10.8 LoaderManager.LoaderCallbacks<Cursor> Methods

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  }


Fig. 9.38 | LoaderManager.LoaderCallbacks<Cursor> methods.

Method onCreateLoader

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.


Image Good Programming Practice 9.1

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

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

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.

9.11 ContactsAdapter Class

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   }


Fig. 9.39 | Subclass of RecyclerView.Adapter that binds contacts to RecyclerView.

Nested Interface ContactClickListener

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.

Nested Class ViewHolder

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.

Overridden RecyclerView.Adapter Method onCreateViewHolder

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.

Overridden RecyclerView.Adapter Method onBindViewHolder

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.

Overridden RecyclerView.Adapter Method getItemCount

Method getItemCount (lines 81–84) returns the total number of rows in the Cursor or 0 if Cursor is null.

Method swapCursor

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.

9.12 AddEditFragment Class

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.

9.12.1 Superclass and Implemented Interface

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


Fig. 9.40 | AddEditFragment package statement and import statements.

9.12.2 AddEditFragmentListener

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


Fig. 9.41 | Nested interface AddEditFragmentListener.

9.12.3 Fields

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 TextInputLayouts, 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


Fig. 9.42 | AddEditFragment fields.

9.12.4 Overridden Fragment Methods onAttach, onDetach and onCreateView

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


Fig. 9.43 | Overridden Fragment Methods onAttach, onDetach and onCreateView.

Method onCreateView inflates the GUI and gets references to the Fragment’s TextInputLayouts 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.

9.12.5 TextWatcher nameChangedListener and Method updateSaveButtonFAB

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


Fig. 9.44 | TextWatcher nameChangedListener and method updateSaveButtonFAB.

9.12.6 View.OnClickListener saveContactButtonClicked and Method saveContact

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


Fig. 9.45 | View.OnClickListener saveContactButtonClicked and method saveContact.

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.

9.12.7 LoaderManager.LoaderCallbacks<Cursor> Methods

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 }


Fig. 9.46 | LoaderManager.LoaderCallbacks<Cursor> methods.

9.13 DetailFragment Class

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.

9.13.1 Superclass and Implemented Interface

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


Fig. 9.47 | package statement, import statements, superclass and implemented interface.

9.13.2 DetailFragmentListener

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


Fig. 9.48 | Nested interface DetailFragmentListener.

9.13.3 Fields

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 TextViews.


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


Fig. 9.49 | DetailFragment fields.

9.13.4 Overridden Methods onAttach, onDetach and onCreateView

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 TextViews. 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


Fig. 9.50 | Overridden methods onAttach, onDetach and onCreateView.

9.13.5 Overridden Methods onCreateOptionsMenu and onOptionsItemSelected

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 (Image), line 109 calls the DetailFragmentListener’s onEditContact method with the contactUriMainActivity passes this to the AddEditFragment. If the user touched the delete option (Image), 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


Fig. 9.51 | Overridden methods onCreateOptionsMenu and onOptionsItemSelected.

9.13.6 Method deleteContact and DialogFragment confirmDelete

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


Fig. 9.52 | Method deleteContact and DialogFragment confirmDelete.

9.13.7 LoaderManager.LoaderCallback<Cursor> Methods

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 }   


Fig. 9.53 | LoaderManager.LoaderCallback<Cursor> methods.

9.14 Wrap-Up

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 Fragments. 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 Fragments for viewing, adding and editing contacts as necessary. You used the FragmentManager and FragmentTransactions to dynamically display Fragments. You used Android’s Fragment back stack to provide automatic support for Android’s back button. To communicate data between Fragments 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 Uris 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 Loaders 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 TextViews 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.

Self-Review Exercises

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 Fragments.

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 Uris that help determine the task to perform when the ContentProvider receives a request.

d) An Activity’s or Fragment’s LoaderManagers 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.

Answers to Self-Review Exercises

9.1

a) Cursor.

b) ContentProvider.

c) getArguments.

d) result set.

e) FragmentManager.

f) Loaders, LoaderManager.

9.2

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 Loaders 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.

Exercises

9.3 (Flag Quiz App Modification) Revise the Flag Quiz app to use one Activity, dynamic Fragments and FragmentTransactions 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.”

Image

Fig. 9.54 | Common ingredient substitutions.

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:

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.186.143