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.
For this recipe, you just need to have Android Studio up and running and a physical or virtual Android device.
Let's see how to set up a project using a content provider. We will be using the Navigation Drawer template for it:
DailyThoughts
. Click on the Next button.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>
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) }));
onOptionsItemSelected
method, remove the second if
statement that is displaying a toast. We do not need it.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>
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; } }
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);} }
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.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); }
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; }
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; }
We will implement the other methods using the following steps:
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); } }
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); }
delete
and update
methods are out of scope for this recipe, so we will not implement them now. Challenge: Add your own implementation here. 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" />
application
tag:<permission android:name="com.packt.dailythoughts.READ_DATABASE"android:protectionLevel="normal"/>
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>
fragment_thoughts.xml
for our list of thoughts and fragment_thoughts_detail
to enter new thoughts.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>
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>
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>
fragments
, and add two new classes to it: ThoughtsDetailFragment
and ThoughtsFragment
, both of which will be descendants of the Fragment
class.ThoughtsFragment
class, add the LoaderCallBack
implementation:public class ThoughtsFragment extends Fragment implementsLoaderManager.LoaderCallbacks<Cursor>{
onCreateLoader
, onLoadFinished
, and onLoaderReset
implementations.private ListView mListView;private SimpleCursorAdapter mAdapter;
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; }
The following steps will help us to add a loader manager to our app:
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); }
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; }
onLoadFinished
, notify the adapter about the loaded data:mAdapter.swapCursor(data);
onLoaderReset
method. In this situation, the data is no longer available so we can delete the reference.mAdapter.swapCursor(null);
ThoughtsDetailFragment
method. 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; }
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); }
MainActivity
class and add two private members that will refer to the fragments we have created as follows:private ThoughtsFragment mThoughtsFragment; private ThoughtsDetailFragment mThoughtsDetailFragment;
private ThoughtsFragment getThoughtsFragment(){ if (mThoughtsFragment==null) { mThoughtsFragment = new ThoughtsFragment(); } return mThoughtsFragment; } private ThoughtsDetailFragment getThoughtDetailFragment() { if (mThoughtsDetailFragment==null){ mThoughtsDetailFragment = new ThoughtsDetailFragment(); } return mThoughtsDetailFragment; }
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(); } }
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(); }
ThoughtsDetailFragment
one more time. To the end of the addThought
method, add this to do the trick:getActivity().getFragmentManager().beginTransaction(). remove(this).commit();
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; }
addThought
member to let the listener know things have been saved:if (mDetailFragmentListener != null){ mDetailFragmentListener.onSave(); }
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(); }
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:
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.