Consuming and updating data using a content provider – daily thoughts

To demonstrate how to create and use content providers we will create an app to store what is on your mind and how happy you are on a daily basis.

Yes, there are apps doing that; however, if you want to create an app to record sport notes and scores instead, feel free to modify the code as it involves basically the same functionality.

In this recipe, we will store new thoughts and retrieve them using a content provider. For the various elements of the app, we will be using fragments because they will neatly demonstrate the effect of the observer pattern.

Getting ready

For this recipe, you just need to have Android Studio up and running and a physical or virtual Android device.

How to do it...

Let's see how to set up a project using a content provider. We will be using the Navigation Drawer template for it:

  1. Create a new project in Android Studio and name it DailyThoughts. Click on the Next button.
  2. Select the Phone and Tablet option and click on the Next button.
  3. Choose Navigation Drawer Activity and click on the Next button.
  4. Accept all values on the Customize the Activity page and click on the Finish button.
  5. Open the strings.xml file within the res/values folder. Modify the strings for the entries that start with title_section. Replace them with the menu items needed for our app. Also replace the action_sample string:
    <string name="title_section_daily_notes">Daily  
     thoughts</string><string name="title_section_note_list">Thoughts 
     list</string>
    <string name="action_add">Add thought</string>
  6. Open the NavigationDrawerFragment file, and in the onCreate method, modify the strings for the adapter accordingly:
    mDrawerListView.setAdapter(new ArrayAdapter<String>(
            getActionBar().getThemedContext(),
            android.R.layout.simple_list_item_activated_1,
            android.R.id.text1,
            new String[]{
                    getString(R.string.title_section_daily_notes),
                    getString(R.string.title_section_note_list)
            }));
  7. In the same class, within the onOptionsItemSelected method, remove the second if statement that is displaying a toast. We do not need it.
  8. Open main.xml from the res/menu folder. Remove the item for the settings and modify the first item so it will use the action_add string. Also rename it's ID and add a neat icon for it:
    <menu xmlns:android= 
     "http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"  
       tools:context=".MainActivity">
    <item android:id="@+id/action_add"  
     android:title="@string/action_add"android:icon="@android:drawable/ic_input_add"android:showAsAction="withText|ifRoom" />
    </menu>
  9. In the MainActivity file, in the onSectionAttached section, apply the correct strings for the different options:
    public void onSectionAttached(int number) {
        switch (number) {
            case 0:
                mTitle = getString(  
                 R.string.title_section_daily_notes);
                break;
            case 1:
                mTitle = getString( 
                 R.string.title_section_note_list);
                 break;
        }
    }
  10. Create a new package named db. Within this package, create a new class, DatabaseHelper, that extends the SQLiteOpenHelper class. It will help us to create a new database for our application. It will contain just one table: thoughts. Each Thought table will have an id, a name and a happiness rating:
    public class DatabaseHelper extends SQLiteOpenHelper {
        public static final String DATABASE_NAME = 
         "DAILY_THOUGHTS";
        public static final String THOUGHTS_TABLE_NAME =   
         "thoughts";
        static final int DATABASE_VERSION = 1;
        static final String CREATE_DB_TABLE =
          " CREATE TABLE " + THOUGHTS_TABLE_NAME +
          " (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +" name TEXT NOT NULL, " +" happiness INT NOT NULL);";public DatabaseHelper(Context context){
            super(context, DATABASE_NAME, null, 
             DATABASE_VERSION);}
        @Override
        public void onCreate(SQLiteDatabase db)
        {
            db.execSQL(CREATE_DB_TABLE);
        }
        @Override 
    	 public void onUpgrade(SQLiteDatabase db, int 
         oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS " +  
             THOUGHTS_TABLE_NAME);
            onCreate(db);}
    }
  11. Create another package and name it providers. Within this package, create a new class called ThoughtsProvider. This will be our content provider for all our daily thoughts. Make it a descendant of the ContentProvider class.
  12. From the Code menu, choose the Implement methods option. In the dialog that appears, all available methods are selected. Accept this suggestion and click on the OK button. Your new class will be extended with these methods.
  13. On top of the class, we will create some static variables:
    static final String PROVIDER_NAME =  
     "com.packt.dailythoughts";
    static final String URL = "content://" + PROVIDER_NAME +  
     "/thoughts";
    public static final Uri CONTENT_URI = Uri.parse(URL);
    public static final String THOUGHTS_ID = "_id";
    public static final String THOUGHTS_NAME = "name";
    public static final String THOUGHTS_HAPPINESS = 
     "happiness";
    static final int THOUGHTS = 1;
    static final int THOUGHT_ID = 2;
    static final UriMatcher uriMatcher;
    static{
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(PROVIDER_NAME, "thoughts", 
         THOUGHTS);
        uriMatcher.addURI(PROVIDER_NAME, "thoughts/#",   
         THOUGHT_ID);
    }
  14. Add a private member, db, that refers to the SQLiteDatabase class, and modify the onCreate method. We create a new database helper:
    private SQLiteDatabase db;
    @Override 
       public boolean onCreate() {
        Context context = getContext();
        DatabaseHelper dbHelper = new DatabaseHelper(context);
        db = dbHelper.getWritableDatabase();
        return (db == null)? false:true;
    }

Queries

Next, implement the query method. A query returns a cursor object. A cursor represents the result of the query and points to one of the query results so the results can be buffered efficiently as it does not need to load data into memory:

private static HashMap<String, String> 
 THOUGHTS_PROJECTION; 
@Override 
public Cursor query(Uri uri, String[] projection, 
 String selection, String[] selectionArgs, String 
  sortOrder) {
   SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
   builder.setTables( 
    DatabaseHelper.THOUGHTS_TABLE_NAME);
   switch (uriMatcher.match(uri)) {
      case THOUGHTS:
        builder.setProjectionMap(
         THOUGHTS_PROJECTION);
         break;
      case THOUGHT_ID:
        builder.appendWhere( THOUGHTS_ID + "=" + uri.getPathSegments().get(1));
        break;
      default:
        throw new IllegalArgumentException(
         "Unknown URI: " + uri);
    }
    if (sortOrder == null || sortOrder == ""){
        sortOrder = THOUGHTS_NAME;
    }
    Cursor c = builder.query(db, projection,selection, selectionArgs,null, null, sortOrder);
    c.setNotificationUri(    
     getContext().getContentResolver(), uri);
    return c;
}

Note

The setNotificationUri call registers the instruction to watch a content URI for changes.

We will implement the other methods using the following steps:

  1. Implement the getType method. The dir directory suggests we want to get all thought records. The item term indicates that we are looking for a particular thought:
    @Override 
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
          case THOUGHTS:
            return "vnd.android.cursor.dir/vnd.df.thoughts";
         case THOUGHT_ID:
           return "vnd.android.cursor.item/vnd.df.thoughts";
         default:
           throw new IllegalArgumentException(
            "Unsupported URI: " + uri);
        }
    }
  2. Implement the insert method. It will create a new record based on the provided values, and if this succeeds we will be notified about the change:
    @Override
    public Uri insert(Uri uri, ContentValues values) {
       long rowID = db.insert(  
        DatabaseHelper.THOUGHTS_TABLE_NAME , "", values);
       if (rowID > 0)
       {
          Uri _uri = ContentUris.withAppendedId(CONTENT_URI, 
           rowID);
          getContext().getContentResolver().notifyChange( _uri, 
           null);
          return _uri;
        }
        throw new SQLException("Failed to add record: " + uri);
    }
  3. The delete and update methods are out of scope for this recipe, so we will not implement them now. Challenge: Add your own implementation here.
  4. Open the AndroidManifest.xml file and add add the provider tag within the application tag:
    <providerandroid:name=".providers.ThoughtsProvider"android:authorities="com.packt.dailythoughts"android:readPermission=  
         "com.packt.dailythoughts.READ_DATABASE"android:exported="true" />

    Note

    For security reasons, you should use false as the value for the exported property in most cases. The reason why we set the value of this property to true here is that later we will create another app that will be able to read the content from this app.

  5. Add the permission for other apps to read data. We will use that in the last recipe. Add it outside the application tag:
    <permission   
     android:name="com.packt.dailythoughts.READ_DATABASE"android:protectionLevel="normal"/>
  6. Open the strings.xml file and add new strings to it:
    <string name="my_thoughts">My thoughts</string>
    <string name="save">Save</string>
    <string name="average_happiness">Average 
      happiness</string>
  7. Create two new layout files: fragment_thoughts.xml for our list of thoughts and fragment_thoughts_detail to enter new thoughts.
  8. Define the layout for fragment_thoughts.xml. A ListView widget is just fine to display all thoughts:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android= 
     "http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
        <ListView
            android:id="@+id/thoughts_list"android:layout_width="match_parent"android:layout_height="wrap_content" ></ListView>
    </LinearLayout> 
  9. The layout for fragment_thoughts_detail.xml will contain the EditText and RatingBar widgets so we can enter what we are thinking and how happy how we currently are:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android=
      "http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_gravity="center"android:layout_margin="32dp"android:padding="16dp"android:layout_width="match_parent"android:background="@android:color/holo_green_light"android:layout_height="wrap_content">
        <TextView
            android:layout_margin="8dp"android:textSize="16sp"android:text="@string/my_thoughts"
         android:layout_width="match_parent"android:layout_height="wrap_content" />
        <EditText
            android:id="@+id/thoughts_edit_thoughts"android:layout_margin="8dp"android:layout_width="match_parent"android:layout_height="wrap_content" />
        <RatingBar
            android:id="@+id/thoughs_rating_bar_happy"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"android:clickable="true"android:numStars="5"android:rating="0" />
        <Button
            android:id="@+id/thoughts_detail_button"android:text="@string/save"          
            android:layout_width="match_parent"android:layout_height="wrap_content" />
    </LinearLayout>
  10. Also create a layout for the rows in the list of thoughts. Name it adapter_thought.xml. Add text views to display an ID a title, or name and the rating:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android=
      "http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_gravity="center"
        android:layout_margin="32dp"
        android:padding="16dp"
        android:layout_width="match_parent"
        android:background=
         "@android:color/holo_green_light"
        android:layout_height="wrap_content">
        <TextView
            android:layout_margin="8dp"
            android:textSize="16sp"
            android:text="@string/my_thoughts"
         android:layout_width="match_parent"
      android:layout_height="wrap_content" />
        <EditText
            android:id="@+id/thoughts_edit_thoughts"
            android:layout_margin="8dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <RatingBar
            android:id="@+id/thoughs_rating_bar_happy"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:clickable="true"
            android:numStars="5"
            android:rating="0" />
        <Button
            android:id="@+id/thoughts_detail_button"
            android:text="@string/save"          
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
    
  11. Create a new package, name it: fragments, and add two new classes to it: ThoughtsDetailFragment and ThoughtsFragment, both of which will be descendants of the Fragment class.
  12. To the ThoughtsFragment class, add the LoaderCallBack implementation:
    public class ThoughtsFragment extends Fragment   
      implementsLoaderManager.LoaderCallbacks<Cursor>{
  13. From the Code menu, choose Implement methods, accept the suggested methods, and click on the OK button. It will create the onCreateLoader, onLoadFinished, and onLoaderReset implementations.
  14. Add two private members that will hold the list view and an adapter:
    private ListView mListView;private SimpleCursorAdapter mAdapter;
  15. Override the onCreateView method, where we will inflate the layout and get a reference to the list view. From here we also will call the getData method:
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View view = inflater.inflate( 
         R.layout.fragment_thoughts, container, false);
        mListView = (ListView)view.findViewById( 
         R.id.thoughts_list);
        getData();
        return view;
    }

Loader manager

The following steps will help us to add a loader manager to our app:

  1. Implement the getData method. We will use the initLoader method of loaderManager for that. The projection defines the fields we want to retrieve, and the target is an array of ID's within the adapter_thought_title layout, which will save us some work using the SimpleCursorAdapter class.
    private void getData(){String[] projection = new String[] { 
         ThoughtsProvider.THOUGHTS_ID,   
         ThoughtsProvider.THOUGHTS_NAME, 
         ThoughtsProvider.THOUGHTS_HAPPINESS};
        int[] target = new int[] {    
         R.id.adapter_thought_id,  
         R.id.adapter_thought_title,  
         R.id.adapter_thought_rating };
        getLoaderManager().initLoader(0, null, this);
        mAdapter = new SimpleCursorAdapter(getActivity(),   
         R.layout.adapter_thought, null, projection,  
          target, 0);
        mListView.setAdapter(mAdapter); 
    }
  2. After the initLoader call, a new loader needs to be created. For this we will have to implement the onLoadFinished method. We use the same projection as we did for the adapter and we will create a CursorLoader class using the uri content of the ThoughtsProvider we have created in the preceding steps. We will sort the outcome by ID (descending):
    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            String[] projection = new String[] { 
         ThoughtsProvider.THOUGHTS_ID,   
         ThoughtsProvider.THOUGHTS_NAME, 
         ThoughtsProvider.THOUGHTS_HAPPINESS};
        String sortBy = "_id DESC";CursorLoader cursorLoader = new 
        CursorLoader(getActivity(), 
        ThoughtsProvider.CONTENT_URI, projection, null, 
         null, sortBy);
        return cursorLoader;
    }
  3. In onLoadFinished, notify the adapter about the loaded data:
    mAdapter.swapCursor(data);
  4. Finally, let's add the implementation for the onLoaderReset method. In this situation, the data is no longer available so we can delete the reference.
    mAdapter.swapCursor(null);
  5. Let's have a look at the ThoughtsDetailFragmentmethod. Override the onCreateView method, inflate the layout, and add an on-click listener for the save button in the layout:
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View view = inflater.inflate( 
         R.layout.fragment_thoughts_detail, container,  
          false); 
       view.findViewById( 
        R.id.thoughts_detail_button).setOnClickListener( 
         new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                addThought();
            }
        });
        return view;
    }
  6. Add the addThought method. We will create new content values based on the input via the EditText and RatingBar field We will use the insert method of the content resolver based on the provided URI. After inserting the new record, we will clear the input:
    private void addThought(){
        EditText thoughtsEdit = 
         (EditText)getView().findViewById(    
          R.id.thoughts_edit_thoughts);
        RatingBar happinessRatingBar =            
         (RatingBar)getView().findViewById(
          R.id.thoughs_rating_bar_happy);
        ContentValues values = new ContentValues();
        values.put(ThoughtsProvider.THOUGHTS_NAME, 
         thoughtsEdit.getText().toString());
        values.put(ThoughtsProvider.THOUGHTS_HAPPINESS,    
         happinessRatingBar.getRating());
        getActivity().getContentResolver().insert( 
         ThoughtsProvider.CONTENT_URI, values);
        thoughtsEdit.setText("");
        happinessRatingBar.setRating(0);
    }
  7. Again it is time to glue things together. Open the MainActivity class and add two private members that will refer to the fragments we have created as follows:
    private ThoughtsFragment mThoughtsFragment;
    private ThoughtsDetailFragment mThoughtsDetailFragment;
  8. Add two private members that will initialize them if needed, and return the instance:
    private ThoughtsFragment getThoughtsFragment(){
        if (mThoughtsFragment==null) {
            mThoughtsFragment = new ThoughtsFragment();
        }
        return mThoughtsFragment;
    }
    private ThoughtsDetailFragment 
    getThoughtDetailFragment() {
       if (mThoughtsDetailFragment==null){
        mThoughtsDetailFragment = new ThoughtsDetailFragment();
        }
        return mThoughtsDetailFragment;
    }
  9. Remove the implementation for onNavigationDrawerItemSelected and a new one to display the list of thoughts. We will implement the KPI option later:
    @Override
      public void onNavigationDrawerItemSelected(int  
      position) {
       FragmentManager fragmentManager =    
        getFragmentManager();
       if (position==1) {
            fragmentManager.beginTransaction().   
             replace(R.id.container, 
              getThoughtsFragment()).commit();
        }
    }
  10. In the onOptionsItemSelected method, test whether the id is action_add, and if so, display the details fragment. Add the implementation just after the line where we get the id:
    if (id== R.id.action_add){FragmentManager fragmentManager = 
         getFragmentManager();
        fragmentManager.beginTransaction().add( 
         R.id.container, getThoughtDetailFragment()  
          ).commit();
    }

    Note

    Instead of replace, we use add here. We want the details fragment to appear on top of the stack.

  11. After saving details, the fragment has to be removed again. Open ThoughtsDetailFragment one more time. To the end of the addThought method, add this to do the trick:
    getActivity().getFragmentManager().beginTransaction().
     remove(this).commit();
  12. However, it would be better to let the activity handle the displaying of fragments since they are intended to be helpers to an activity. Instead, we will create a listener for an onSave event. On top of the class, add a DetailFragmentListener interface. Also create a private member and a setter for it:
    public interface DetailFragmentListener {
        void onSave();
    }
    private DetailFragmentListener 
     mDetailFragmentListener; 
    public void setDetailFragmentListener(  
     DetailFragmentListener listener){
        mDetailFragmentListener = listener;
    }
  13. Add these lines to the end of the addThought member to let the listener know things have been saved:
    if (mDetailFragmentListener != null){
        mDetailFragmentListener.onSave();
    }
  14. Go back to the MainActivity class, and add a listener implementation for it. You could use the Implement methods option from the Code menu for it if you want:
    public class MainActivity extends Activityimplements NavigationDrawerFragment. 
       NavigationDrawerCallbacks, 
        ThoughtsDetailFragment.DetailFragmentListener {
    @Override 
     public void onSave() {      
      getFragmentManager().beginTransaction().remove(
       mThoughtsDetailFragment).commit();
    }
  15. To tell the detail fragment that the main activity is listening, scroll to the getThoughtDetailFragment class and call the setListener method right after the creation of a new detail fragment:
    mThoughtsDetailFragment.setDetailFragmentListener(this);

Now run the app, choose Thoughts list from the navigation drawer, and click on the plus sign to add new thoughts. Following screenshot gives the example of adding thought:

Loader manager

We do not need to tell the fragment that contains the list about the new thought we have created in the detail fragment. Using a content provider with an observer, the list will be updated automatically.

This way we can accomplish more and achieve less error-prone functionality writing less code, which is exactly what we want. It allows us to improve the quality of our code.

See also

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

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