Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

How to Create Android Notepad App – Part 2

5.00/5 (4 votes)
7 Oct 2015CPOL11 min read 13K  
This post is a step by step tutorial on how to add data persistence to the Android Notepad app we create in the last post.   You are highly encouraged to read the last posts to get the most out of this tutorial especially if you are new to Android development. 

This post is a step by step tutorial on how to add data persistence to the Android Notepad app we create in the last post.   You are highly encouraged to read the last posts to get the most out of this tutorial especially if you are new to Android development. In that post, we left off with a functioning RecyclerView showing sample data. Now we want to replace that sample data with real data. You can get the source code for this tutorial from here.

Please share this tutorial with anyone that may benefit from it and if you like the post, please leave me a comment, I will appreciate it.

Data Access Object

Data Access Object Pattern or DAO pattern is used to separate low level data accessing API or operations from high level business services.

http://www.tutorialspoint.com/design_pattern/data_access_object_pattern.htm

 

How does this apply to our humble notepad app you may ask?  Well it is because of change. Things change, requirements change, data sources change. Remember that we are going to be using Content Provider to access our database; now, what happens if we change our mind and decide to access the SQLite database directly?  then we will have to re-write all the parts of our app that need to access the database to reflect the switch from Content Provider to direct SQLite access.

To reduce the impact of this kind of scenario we can implement a data access object which will expose consistent data access interface to our app regardless of the underlying data source. For this reason, we will need to add a data access object to our app, and this object will be a standard java class and it will manage our data persistence need.

Add a Java class file named NoteManager.java to your data package. This class will have a factory method that returns an instance of this class to ensure that we always have one instance. Here is the initial content of this file.

public class NoteManager {
    private Context mContext;
    private static NoteManager sNoteManagerInstance = null;

    public static NoteManager newInstance(Context context){

        if (sNoteManagerInstance == null){
            sNoteManagerInstance = new NoteManager(context.getApplicationContext());
        }

        return sNoteManagerInstance;
    }

    private NoteManager(Context context){
        this.mContext = context.getApplicationContext();
    }
}

In the above code we made the constructor private so that it cannot be instantiated from the outside. Then we created two instance variables, one is the Context which we need when we call the ContentResolver and the other one is our data access object itself. The factory method newInstance either returns an existing instance of the the object or create a new one and return it. With that out of the way, we can proceed with creating the CRUD methods.

Create Note Method

This method is called to insert a record into a table.  It will accept Note object as a parameter. This means that before this method is called, you should have created a Note in the Note editor screen. This method then passes the Note object to the ContentResolver  which in turn passes it to the Content Provider and then to the SQLite database. Yeah!, that many steps if you are wondering. Lets see these steps in action.

public long create(Note note) {
        ContentValues values = new ContentValues();
        values.put(Constants.COLUMN_TITLE, note.getTitle());
        values.put(Constants.COLUMN_CONTENT, note.getContent());
        values.put(Constants.COLUMN_CREATED_TIME, System.currentTimeMillis());
        values.put(Constants.COLUMN_MODIFIED_TIME, System.currentTimeMillis());
        Uri result = mContext.getContentResolver().insert(NoteContentProvider.CONTENT_URI, values);
        long id = Long.parseLong(result.getLastPathSegment());
        return id;
    }

Lets walk through what the above code block is doing.

  1. First we passed in a Note object as parameter to the method.
  2. Then we used the properties in that object to create a ContentValue.
  3. Content Value is just a key-value pair bundle used to move data around. It is similar to the Intent you ought to be familiar with. The Intent is mostly used in the front-end (Activity, Fragment) while ContentValues are used in the backend SQLite.
  4. The keys for the ContentValues are the column names for the tables that you want to insert the data into, and the value are the values you want to insert. Here again we took advantage of the fact that we already defined the column names for our tables as constants.
  5. We then called the ContentResolver and  passed it the URI of the table we want to insert data into and the values we want to insert.
  6. There is only one ContentResolver so you can call it from anywhere as long as you can get a hold of a Context.
  7. The ContentResolver uses the entry in the manifest file to figure out which Content Provider it should call.
  8. The call to the ContentResolver returns a URI and we use the method getLastPathSegment() to get the id of the newly inserted item.
  9. This call to getLastPathSegment() is optional, because it is possible to produce a null.

Read Note Method

Databases will have limited use cases if they only allow a one way method to insert data without a corresponding mechanism to retrieve those data. In this section we will discuss how to retrieve data from the database using ContentResolver. In the create method above, we called the insert() of the ContentResolver, which in turn calls the insert method of the Content Provider which triggers a  call to the create() of SQLite database. So in a loose sense, there is a one to one correlation between the “Create” acronym of CRUD (create, read, update and delete) and SQLite database create(). This one to one correlation does not exist for the read acronymn.

SQLite database does not have a read() method, if you want to get something out of the database you have to provide a written description of exactly what you want to retrieve. This written description is called a query and your query could be simple or complex depending on the data needs of your app. Generally there are two flavors of CRUD read methods: one that returns a List of all items in a table or one that returns a specific item from the table. Add the method below to your NoteManager.java class

This method gets all Notes in the note table and return them as List of Notes

public List<Note> getAllNotes() {
        List<Note> notes = new ArrayList<Note>();
        Cursor cursor = mContext.getContentResolver().query(NoteContentProvider.CONTENT_URI, Constants.COLUMNS, null, null, null);
        if (cursor != null){
            cursor.moveToFirst();
            while (!cursor.isAfterLast()){
                notes.add(Note.fromCursor(cursor));
                cursor.moveToNext();
            }
            cursor.close();
        }
        return notes;
    }

Here is what the above code block is doing:

  1. First we declared that the return type of this method is a Java List of type Note
  2. We instantiated an ArrayList of Notes. FYI: Java List is an interface or a specification while an ArrayList is a concrete implementation of the methods specified in the List interface.
  3. Then we called the query method of ContentResolver, again passing it the URI representing the table we are interested in and the columns or projections we want to retrieve.
  4. This query will be translated to a “SELECT * ” SQL query when it hits the SQLite database
  5. Our query return a result of type Cursor and what is a Cursor you may ask?
  6. A Cursor is a List that knows how to traverse itself. A query result could return 0, one or more  records, therefore Android wraps that result in a Cursor object and the Cursor hands them over to you in batches of any number. So if you access the first 20 records in the result set, the Cursor remembers where you left off just like the blinking cursor on your keyboard and when you request the next batch it continues from there.
  7. At this point SQLite database is done, what we do with the Cursor object is up to us. We have indicated that we want to return an ArrayList<Note> but what is contained in the Cursor is a list of rows of note table data.
  8. We need yet another method to retrieve all those rows and convert each of them into a Note.java object that we can add to our ArrayList and return.
  9. Because of this, we are going to create another method that accepts a Cursor object and then retrieves the information in that cursor object to reconstruct a Note object.
  10. Open your Note.java class file and add a method called getNoteFromCursor(Cursor cursor) and the content for this method is below.
public static Note getNotefromCursor(Cursor cursor){
        Note note = new Note();
        note.setId(cursor.getLong(cursor.getColumnIndex(Constants.COLUMN_ID)));
        note.setTitle(cursor.getString(cursor.getColumnIndex(Constants.COLUMN_TITLE)));
        note.setContent(cursor.getString(cursor.getColumnIndex(Constants.COLUMN_CONTENT)));

        //get Calendar instance
        Calendar calendar = GregorianCalendar.getInstance();

        //set the calendar time to date created
        calendar.setTimeInMillis(cursor.getLong(cursor.getColumnIndex(Constants.COLUMN_CREATED_TIME)));
        note.setDateCreated(calendar);

        //set the calendar time to date modified
        calendar.setTimeInMillis(cursor.getLong(cursor.getColumnIndex(Constants.COLUMN_MODIFIED_TIME)));
        note.setDataModified(calendar);
        return note;
    }

Let us step through what the above code is doing

 

  1. The Cursor class is not just a wrapper around rows of data set, it has useful methods to retrieve the contents of those rows of data.
  2. Any SQLite column can essentially contain any type of data and the Cursor provides methods to convert them to specific Java data types
  3. For example if you want to get a String from a column then you can use cursor.getString() to unbox that column content into a String, and so on.
  4. These methods getString(), getLong(), etc. accepts an integer which is the index of the column that you want to unbox.
  5. However instead of memorizing the indexes of your column, the Cursor object also have a method called getColumnIndex() that accepts a name that you can use to return the index of your columns.
  6. Here again, we took advantage of the fact that we defined our column names as constants to supply the names of our columns easily.
  7. Then we use the retrieved information to re-create a Note object that was saved in that row which we then return.
  8. We made the getCustomerFromCursor() method static so we can have access to it without creating a new instance of Note.

Next we need to add a read method that fetches one Note at a time given the id of that Note. Add this method below to your NoteManager.java class

public Note getNote(Long id) {
    Note note;
    Cursor cursor = mContext.getContentResolver().query(NoteContentProvider.CONTENT_URI,
            Constants.COLUMNS, Constants.COLUMN_ID + " = " + id, null, null);
    if (cursor != null){
        cursor.moveToFirst();
        note = Note.getNotefromCursor(cursor);
        return note;
    }
    return null;
}

 

Update Note Method

Thing change and Ms. Jane Doe could now become Mrs. Jane Smith, and if that happens do we create a new record in the database or update an existing record? The answer is obvious. We update the existing record. Thankfully SQLite database has an update method and so does the ContentResolver which corresponds to the “U” in the CRUD acronym. Add the method below to your NoteManager.java

public void update(Note note) {
    ContentValues values = new ContentValues();
    values.put(Constants.COLUMN_TITLE, note.getTitle());
    values.put(Constants.COLUMN_CONTENT, note.getContent());
    values.put(Constants.COLUMN_CREATED_TIME, System.currentTimeMillis());
    values.put(Constants.COLUMN_MODIFIED_TIME, System.currentTimeMillis());
    mContext.getContentResolver().update(NoteContentProvider.CONTENT_URI,
            values, Constants.COLUMN_ID  + "=" + note.getId(), null);

}

If the above method looks familiar then you are doing a great job following because it should. The update method is similar to the create method except that we are providing the id of the Note to be updated in the where clause of the call to ContentResolver.

Delete Note Method

The last letter in the CRUD acronym is “D” for delete. Add the method below to your NoteManager.java to implement delete.

public void delete(Note note) {
        mContext.getContentResolver().delete(
                NoteContentProvider.CONTENT_URI, Constants.COLUMN_ID + "=" + note.getId(), null);
    }

And that ends our implementation of the four CRUD methods, now let us put them to good use.

Data Driven App

In this section we will use the data access methods that we have created to power our app. Use the following steps to complete this app.

Step 1: Add Instance Variables and Methods – in either the NotePlainEditorFragment.java or NoteLinedEditorFragment.java whichever one that you want to implement add the following instance variables near the top of the file:

private View mRootView;
    private EditText mTitleEditText;
    private EditText mContentEditText;
    private Note mCurrentNote = null;

Then add this method that will now be used to create an instance of the Fragment

public static NotePlainEditorFragment newInstance(long id){
        NotePlainEditorFragment fragment = new NotePlainEditorFragment();

        if (id > 0){
            Bundle bundle = new Bundle();
            bundle.putLong("id", id);
            fragment.setArguments(bundle);
        }
        return fragment;
    }

Now add this method that gets the Id of a Note if one is passed in

private void getCurrentNote(){
        Bundle args = getArguments();
        if (args != null && args.containsKey("id")){
            long id = args.getLong("id", 0);
            if (id > 0){
                mCurrentNote = NoteManager.newInstance(getActivity()).getNote(id);
            }
        }
    }

Step 2: Instantiate Variables – update the onCreateView() method of your Fragment like this:

@Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        mRootView = inflater.inflate(R.layout.fragment_note_plain_editor, container, false);
        mTitleEditText = (EditText)mRootView.findViewById(R.id.edit_text_title);
        mContentEditText = (EditText)mRootView.findViewById(R.id.edit_text_note);        
        return mRootView;
    }

Step 3: Add Menu Icons – using any icon of your choice, create two menu items for your Fragment in the res/menu folder. Here is an example of my res/menu/menu_note_edit_plain.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".MainActivity">


    <item android:id="@+id/action_save"
          android:title="@string/action_save"
          android:icon="@drawable/ic_action_save"
          app:showAsAction="always"/>

      <item android:id="@+id/action_delete"
            android:title="@string/action_delete"
            android:icon="@drawable/ic_action_trash"
            app:showAsAction="ifRoom"/>
</menu>

You need to add your icons to the res/drawable folder and add two entries to your res/value/string.xml file for “Save” and “Delete”

Step 4: Implement Menu – remember that the Fragment does not own the screen real estate so the first thing we need to do is to notify the Activity that this Fragment wants to place an item in the menu. You do this by setting hasOptionsMenu to true like this:

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);       
    }

Then add the menu file in the onCreateOptions method like this:

@Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        menu.clear();
        inflater.inflate(R.menu.menu_note_edit_plain, menu);
    }

Now if you run the app, there will be two icons in the menu but they will not do anything if you click on them, so we have to add a listener for those icons by implementing onOptionsItemSelected like this:

@Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){            
            case R.id.action_delete:
             //delete note
                break;
            case R.id.action_save:
                //save note
                break;
        }
        return super.onOptionsItemSelected(item);
    }

Step 5: Implement Save Note – we have to actually implement a method that saves the Note which we can call when the “Save” icon is clicked. Add this method to your class:

private boolean saveNote(){
       
        String title = mTitleEditText.getText().toString();
        if (TextUtils.isEmpty(title)){
            mTitleEditText.setError("Title is required");
            return false;
        }

        String content = mContentEditText.getText().toString();
        if (TextUtils.isEmpty(content)){
            mContentEditText.setError("Content is required");
            return false;
        }

            Note note = new Note();
            note.setTitle(title);
            note.setContent(content);
            NoteManager.newInstance(getActivity()).create(note);
       
        return true;

    }

Now, update your onOptionsItemSelected like this

@Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case R.id.action_delete:
             //delete note
                break;
            case R.id.action_save:
                //save note
                if (saveNote()){
                    makeToast("Note saved");
                }
                break;
        }
        return super.onOptionsItemSelected(item);
    }

Add the convenience method makeToast()

private void makeToast(String message){
    Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show();
}

Step 6: Implement Update Note – if a Note need to be updated, then we need to populate the title and content of that Note from the passed in Note. Add the method populateFields() to your class

private void populateFields() {
        mTitleEditText.setText(mCurrentNote.getTitle());
        mContentEditText.setText(mCurrentNote.getContent());       
    }

Now, in the onCreate() method, call the getCurrentNote() method that we added. And then add the onResume() life cycle method from where you determine if the Note need to be populated like this:

@Override
    public void onResume() {
        super.onResume();
        if (mCurrentNote != null){
            populateFields();
        }
    }

Remember to call 

getCurrentNote()

 from the onCreate() method after the call to setHasOptionMenu. Now update the saveNote() method to include update method.

 

private boolean saveNote(){

        String title = mTitleEditText.getText().toString();
        if (TextUtils.isEmpty(title)){
            mTitleEditText.setError("Title is required");
            return false;
        }

        String content = mContentEditText.getText().toString();
        if (TextUtils.isEmpty(content)){
            mContentEditText.setError("Content is required");
            return false;
        }


        if (mCurrentNote != null){
            mCurrentNote.setContent(content);
            mCurrentNote.setTitle(title);
            NoteManager.newInstance(getActivity()).update(mCurrentNote);

        }else {
            Note note = new Note();
            note.setTitle(title);
            note.setContent(content);
            NoteManager.newInstance(getActivity()).create(note);
        }
        return true;

    }

And also, update the onOptionItemSelected to now show toast of “updated” or “saved” for update and save calls respectively using ternary operation.

case R.id.action_save:
                //save note
                if (saveNote()){
                    makeToast(mCurrentNote !=  null ? "Note updated" : "Note saved");
                }
                break;

Step 7: Implement Delete Note: deleting a Note is easy, however we want to prompt the user to make sure that they really want to delete the Note. Add this method to your Fragment

public void promptForDelete(){
        final String titleOfNoteTobeDeleted = mCurrentNote.getTitle();
        AlertDialog.Builder alertDialog = new AlertDialog.Builder(getActivity());
        alertDialog.setTitle("Delete " + titleOfNoteTobeDeleted + " ?");
        alertDialog.setMessage("Are you sure you want to delete the note " + titleOfNoteTobeDeleted + "?");
        alertDialog.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                NoteManager.newInstance(getActivity()).delete(mCurrentNote);
                makeToast(titleOfNoteTobeDeleted + "deleted");
                startActivity(new Intent(getActivity(), MainActivity.class));
            }
        });
        alertDialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        alertDialog.show();
   }

With the prompt added, you can now update the onOptionsItemSelected like this

@Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case R.id.action_delete:
             //delete note
                if (mCurrentNote != null){
                    promptForDelete();
                }else {
                    makeToast("Cannot delete note that has not been saved");
                }
                break;
            case R.id.action_save:
                //save note
                if (saveNote()){
                    makeToast(mCurrentNote !=  null ? "Note updated" : "Note saved");
                }
                break;
        }
        return super.onOptionsItemSelected(item);
    }

Step 8: Update the NoteListFragment – first we need to now start getting data from the database instead of dummy data so change this line 

mNotes = SampleData.getSampleNotes();

  to 

mNotes = NoteManager.newInstance(getActivity()).getAllNotes();

Then in the OnItemTouchListener of the RecyclerView, update the code to now start NoteEditorActivity with the id of the selected Note like this

if (child != null && mGestureDetector.onTouchEvent(motionEvent)) {
                    int position = recyclerView.getChildLayoutPosition(child);
                    Note selectedNote = mNotes.get(position);
                    Intent editorIntent = new Intent(getActivity(), NoteEditorActivity.class);
                    editorIntent.putExtra("id", selectedNote.getId());
                }
                return false;

Step 9: Update the NoteEditorActivity – now update the NoteEditorActivity to look for the presence of an id, which will indicate that this is an edit call.

if (savedInstanceState == null){
            Bundle args = getIntent().getExtras();
            if (args != null && args.containsKey("id")){
                long id = args.getLong("id", 0);
                if (id > 0){
                    NotePlainEditorFragment fragment = NotePlainEditorFragment.newInstance(id);
                    openFragment(fragment, "Editor");
                }
            }
            openFragment(new NotePlainEditorFragment(), "Editor");
        }

And with that we are done!

Summary

This concludes this tutorial series where I have provided an introduction to SQLite, and Content Provider and ContentResolver. And in the process we created a simple notepad app to demonstrate the concepts. You should now be able to test you app and it should be working. If you have not done so, you can get the source code so you can compare notes.

Happy Coding!

The post How to Create Android Notepad App – Part 2 appeared first on Val Okafor.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)