This lab will be a continuation of Lab 3. You will be given a solution to Lab 3 so you may begin Lab 4 even if you did not complete all of Lab 3. You will be persisting data using an SQLite Database and preserving the state of an application during its lifecycle. It is important to note that this lab is meant to be done in order, from start to finish. Each activity builds on the previous one, so skipping over earlier activities in the lab may cause you to miss an important lesson that you should be using in later activities.
Objectives
At the end of this lab you will be expected to know:
Activities
For this lab we will be extending the "Joke List" application that you created in Lab3. This version of the app will add a persistence layer to the previous version. It will allow Jokes that are added to be saved to a database as well as maintain application state throughout the life cycle of the application. All tasks for this lab will be based off of this application. Over the course of the lab you will be iteratively refining and adding functionality to the Joke List app. With each iteration you will be improving upon the previous iteration's functionality.
IMPORTANT:
You will be given a Skeleton Project to work with. This project contains all of the java and resource files you will need to complete the lab. Some method stubs, member variables, and resource values and ids have been added as well. It is important that you not change the names of these methods, variables, and resource values and ids. These are given to you because there are unit tests included in this project as well that depend on these items being declared exactly as they are. These units test will be used to evaluate the correctness of your lab. You have complete access to these test cases during development, which gives you the ability to run these tests yourself. In fact, you are encouraged to run these tests to ensure that your application is functioning properly.
1. Setting Up...
1.1 Creating the Project
To begin, download and extract the skeleton project for the JokeList application from Blackboard (under Course Materials). ***Non-Cal Poly students may contact Dr. Janzen (djanzen at calpoly.edu) directly for these files.
Next you will need to setup a "Joke List" Android project for this app. Since the skeleton project was created in Eclipse, the easiest thing is to import this project into Eclipse.
1.2 Familiarize Yourself with the Source Code
The skeleton project that has been given to you for this lab contains a fully functional solution to Lab3. In particular, the AdvancedJokeList, JokeListAdapter, Joke, and JokeView classes are fully functioning classes from Lab3. Additionally, completed advanced.xml and joke_view.xml layout files have been supplied as well.
It is a good idea, and good practice, to read through the source code. See how it compares to your implementation of Lab3. It is especially important to do this if there were any parts of Lab3 that you were not able to complete. The rest of the Lab will require you to update this source code to make use of Data Persistence so it is critical that you are familiar with it.
1.3 Fill in the Joke Class
You must fill in a few new areas of the Joke Class. Some new method stubs and a member variable have been added that you will have to fill in and make use of. Make sure not to delete or change these. In particular, a member variable named m_nID has been added to the class which will contain the unique id assigned to the Joke from the Database:
Run the JokeTest.java Unit Tests to ensure that you have properly filled in this class. The AdvancedJokeListTest2.java Unit Tests should run here at the beginning, but they will start failing later in the lab. You do not need to modify them to continue passing.
2. Maintaining Application State
You will now enable your application to maintain basic settings in the event that your Activity is destroyed and re-created, as well as maintain UI state across different runs of your application. In particular, the filter value for m_nFilter should be maintained if the Activity is destroyed and re-created for any reason. In this case the jokeList should be re-filtered to display only the jokes specified by m_nFilter. The filter value will be saved as Instance State. Additionally, the text in the m_vwJokeEditText will be saved and restored across separate runs of your programs via the Preferences mechanism. You can begin familiarizing yourself with the subject matter by reading the Android Documentation on Saving Persistent State in Activities.
2.1 Instance State
Instance state is data that is private to a single instance of an Activity. This data is not shared across the application or made available to any other instances of the same Activity. Instance state can be stored in a Bundle object when the Activity becomes inactive, or destroyed and dumped from memory. The Bundle object is saved even though the Activity no longer resides in memory. When an Activity becomes inactive, its onSaveInstanceState(...) method is called. This is where saving of instance state happens. It is important to note that this only happens when the Activity is saved in the Application History stack, not when the Activity is closed or finalized.
The Bundle object is a map data-structure that uses key-value pairs to store data. The type of data that can be stored is limited to basic data types like primitives and strings. You can retrieve the data from the Bundle object when the Activity is re-created. The Bundle object containing the instance state can be accessed by overriding two separate methods. The first, which you are already familiar with, is the onCreate(...) method. The second is the onRestoreInstanceState(...) method, which gets called immediately after the onStart(...) method. Both of these methods take a Bundle argument that contains the instance state if it was previously saved.
It is important to note that this Bundle object might not contain anything if this is the initial creation of the activity. The onRestoreInstanceState won't even get called on the initial creation of the Activity. Restoring instance state can be done in either method. However, using the onRestoreInstanceState(...) method instead of the onCreate(...) method has the benefit of logically separating initialization tasks from restoration tasks.
In these next two sections you will be persisting the Filtering mechanism as instance state. To start you will save the filter state of your AdvancedJokeList Activity in the onSaveInstanceState(...) method. You will then restore the state in the onRestoreInstanceState(...) methods. More background on this can be found in the Android Developer Guide on Application State and the Android Documentation on the Bundle Class. When finished, your application should function as depicted by the figure below:
2.1.1 Saving Instance Data.
Begin by familiarizing yourself with the Android Documentation on the Activity.onSaveInstanceState(...) method. It is good to understand what the default implementation does before modifying it.
2.1.3 Restoring Instance Data.
Begin by familiarizing yourself with the Android Documentation on the Activity.onRestoreInstanceState(...) method. It is good to understand what the default implementation does before modifying it.
2.2 SharedPreferences
The SharedPreferences mechanism operates in a manner similar to saving Instance State in that primitive data is stored in a map of key/value pairs. The difference is that SharedPreference data can be shared across Application components running in the same Context and that the data persists across separate runs of the application. Alternatively, you can make the SharedPreferences data private to a single instance of an Activity.
In this next section you will persist the text in m_vwJokeEditText across multiple runs of an Application. This allows the user to work on a new joke across multiple sessions, and guarantees that if the process is killed for some reason, the user won't lose a joke they were working on. To start, you will override the default implementation of onPause() and save data in a private SharedPreferences object. You will then retrieve and restore the data in the onCreate(...) method. Saving your data in onPause() guarantees that if your process is killed, the data will still be available in a subsequent call to onCreate(...). This is because onPause() is the earliest point at which an Activity can be killed by the system. You should be able to see this from the Activity Lifecycle Diagram. When finished, your application should function as depicted by the figure below:
2.2.1 Saving SharedPreference Data
Begin by reading the Android Developer guide on using SharedPreferences to save data, as well as the Android Documentation on the Activity.getPreferences(...) method and the SharedPreferences class . It is good to understand how these work before using them.
2.2.2 Restoring SharedPreference Data
The following should be done in the onCreate() method.
3. Binding Views to a Database
Your ultimate goal at the end of this lab is to be able to persist Jokes in a Database. In section 4 of this lab, you will be implementing the Database. Before you do that, however, it seems more pertinent to update your application to be able to use a database first. Proceeding in this manner will allow you to incrementally add Database functionality and then test it in section 4. Since the Database implementation will be more tricky, this seems like a better route than the alternative of implementing the Database in one-shot and incrementally updating your application to use it.
With that said, it may seem a little odd that you will be making use of Database related classes and methods that you either haven't implemented or haven't learned about yet. Fear not, you will learn about and implement the Database related stuff in the next section. For the time being, this section of the lab will act like these features are already implemented (i.e. there will be lots of hand-waving and urging you not to ask questions about the strange-man in the corner of the room).
In this section of the Lab you will be updating your application to bind the list of Jokes that are displayed, to the results of a database query, as well as write any new Jokes and changes to existing Jokes back to the Database. In particular, you will be creating a call back mechanism in the JokeView class which will notify the AdvancedJokeList Activity when the internal state of a Joke has changed and needs to be written back to the Database. You will then implement the JokeCursorAdapter. This class extends the CursorAdapter class and is very similar to the Adapter that you wrote in Lab 3. The main difference being that JokeCursorAdapter is bound to a Cursor object, which represents the results of a Database query. You will then update the AdvancedJokeList Activity class to use this new Adapter instead of the JokeListAdapter class that you wrote in Lab 3. Lastly, you will update the AdvancedJokeList to add Jokes to the Database instead of to an ArrayList<Joke> and to monitor and write back Joke ratings to the database. The last part will allow you to persist a list of Jokes across multiple application sessions.
3.1 JokeView.OnJokeChangeListener
I apologize in advance, this is a bit long winded, but it's a complicated subject. Especially for those who aren't well practiced with the Model-View-Controller, Observer/Observable, and Data Persistance patterns. If you already know this stuff, then feel free to skip this introduction.
When the list of Jokes is displayed, Jokes are copied from the Database into Joke Objects, which are then wrapped in JokeViews. These JokeViews allow the user to modify the internal state of the Joke, in particular the ratings for a joke. Once the user has modified the rating for a Joke, the Joke object stops being an exact copy of the data in the database. If we want to make sure that rating gets preserved we must be able to write that rating change back to the database.
The question is then whose job is it to write that change back to the database? Is it the job of the Joke object, the JokeView object, the Adapter, or the Activity? The JokeView and the Adapter are two very specialized classes, charged with specific tasks that don't really care about the persistent state of a Joke. The Adapter's job is to provide View objects for Jokes, the JokeView's job is to show what a Joke looks like and provide controls for manipulating the state of the Joke.
That leaves two possibilities, the Joke and the Activity. While it is entirely acceptable to put the responsibility of persisting state on the Joke object, in this case we are going to follow the Model-View-Controller pattern and place that responsibility in the hands of the Activity. By doing this, we decouple the Joke class from the idea of a Database entirely. This allows the same Joke class to be used with or without a database.
So now we need some way for the JokeView to signal to whomever is responsible, that its Joke object has changed and needs to be written back to the database. We could just add an arbitrary method to AdvancedJokeList and call it from the JokeView, however, this is a bit hacky. It is also foreseeable that we might even have a third highly specialized class whose only responsibility is persisting the state of Jokes, like a Data Transfer Object (DTO). With this in mind, its easy to see that the JokeView shouldn't even care who handles persistence or how they handle it since its not the JokeView's job to worry about such things. Why should the JokeView cater to the needs of arbitrary classes?
Instead, the JokeView is just going to put its foot down and say:
"If you want to know when the Joke changes, then you've got to tell me you're interested in such things. Furthermore, when the joke changes I'm gonna tell you which method I'm going to call to notify you."
Thus, a static interface named OnJokeChangeListener has been added to the JokeView class. This interface specifies a callback method that gets called when a Joke's internal state changes. Additionally, each JokeView class now holds a single member variable reference to an OnJokeChangeListener. Any class that is interested in receiving notifications that a Joke has changed should implement the OnJokeChangeListener and register itself with the corresponding JokeView.
3.1.1 Registering & Notifying OnJokeChangeListeners
The JokeView class now contains a member variable reference to a single OnJokeChangeListener, m_onJokeChangeListener. It should notify m_onJokeChangeListener any time the internal state of its Joke object changes. You must now maintain this reference and make sure it gets notified:
3.2 JokeCursorAdapter
The JokeCursorAdapter class will be responsible for providing JokeViews for Jokes in the same way that your JokeListAdapter did in Lab3. The key difference here is that the JokeCursorAdapter will use a Cursor object as its data source instead of an ArrayList<Joke>. A Cursor object is a set of results from a Database query. If you've ever worked with a Database, you can think of the Cursor as an iterator for a list of rows returned from a database query. You can move the cursor around in the list and retrieve data from a particular row. Each row contains all the data for a single joke, the text of the joke, the author, the rating and a unique id.
I strongly encourage you to read the sections of the Android Developer Guide on the CursorAdapter Class before beginning.
3.2.1 Fill in Basic JokeCursorAdapter Methods
3.2.2 Make JokeCursorAdapter Extend CursorAdapter
3.3 Updating AdvancedJokeList
For this section you will be updating AdvancedJokeList to use the the JokeDBAdapter Database class and JokeCursorAdapter class that you just implemented. You will start by initializing your database connection and updating the types on both m_jokeAdapter and m_arrJokeList to JokeCursorAdapter and Cursor respectively.
AdvancedJokeList will query the database for a list of Jokes to display, which will return a Cursor. This Cursor is what you will use in place of the ArrayList<Joke> that you used in Lab 3. Its worth noting that this will make filtering much easier, since any time a filter is changed you simple have to re-query the database and get a new Cursor. Consequently, you will have to update your filtering functionality.
Lastly, you will be updating all of the JokeList maintenance methods. Specifically, you will update the addJoke(...) method, the Remove Joke Context-MenuItem, the Upload Joke Context-MenuItem, and make AdvancedJokeList save changes to Jokes in the database. You will do this last part by making AdvancedJokeList implement the onJokeChangeListener interface so that it can monitor and write back Joke changes to the database.
3.3.1 Initializing JokeDBAdapter, JokeCursorAdapter, & Cursor
3.3.2 Update Your Filtering Functionality
Since the master copy of all your jokes will now live in the Database, and m_arrJokeList is now a copy of that list, there is no need for an extra mechanism to keep track of which jokes should be displayed to the user. You need to remove the extra mechanism being used to filter Jokes so that all the jokes in m_arrJokeList will get displayed to the user. When you need to change the filter, you simply retrieve a different Cursor from the Database and update m_arrJokeList to reference the new cursor.
In this section you will be updating the filtering logic to retrieve a new Cursor from the database instead of manually filtering the Jokes. The filtering logic will be self-contained in a single method called AdvancedJokeList.setAndUpdateFilter(...). Perform the following modifications in that method:
3.3.3 Maintaining the JokeList
Update the addJoke(...) method so that it inserts the Joke parameter passed into the Database and have the Cursor refresh itself.
Update the Remove Joke Context-MenuItem so that the Joke is properly removed from the Database and have the Cursor refresh itself. These changes should be made to the Remove MenuItem's OnMenuItemClickListener.
Update the Upload Joke Context-MenuItem to get the Joke from m_jokeDB instead of m_arrJokeList. These changes should be made to the Upload MenuItem's OnMenuItemClickListener.
Make AdvancedJokeList monitor changes to any Joke and save them back into the database. Do this by making AdvancedJokeList implement the JokeView.OnJokeChangeListener interface and setting the m_jokeAdapter's OnJokeChangeListener to AdvancedJokeList.
And you are done updating your JokeList Application to persist Jokes to a Database. Now all thats left is to implement your JokeDBAdapter Database class. Before we go on, I encourage you to test your application using the JokeCursorAdapterTest provided. This set of unit tests uses a provided database implementation (the same as what you will be completing in the next section). To use it, you will need to import the JokeDBAdapter (import edu.calpoly.android.lab4_key.JokeDBAdapter;) from the key in your AdvancedJokeList and JokeCursorAdapter classes. After testing, you can remove these imports so you use your own versions of these classes.
4. Databases & Using Persistent Data
I strongly encourage you to read the sections of the Android Developer Guide on Data Storage. In this fourth and final section you will be implementing the class that actually saves your Jokes to a database, making them persistent across multiple user sessions. The class is named the JokeDBAdapter class. JokeDBAdapter wraps all of the functionality necessary to save, update, delete, and retrieve Jokes. In addition to this, it also wraps the functionality necessary to create and update the Database itself and manage connections to it.
You will now be tasked with implementing all of this functionality. The goal of this section of the lab is teach you how to work with an SQLiteDatabase and the general approach for wrapping data-persistence into a single class. The goal is not to teach you SQL. With this in mind, most of the actual SQL statements necessary to create and work with a Database have been written for you and placed into static-final-strings that you can use.
4.1 Creating an SQLiteDatabase & Managing Connections
The JokeDBAdapter class provides an interface for performing common database operations like inserting data, updating data, removing data, and retrieving data. Additionally, it allows you to open a connection to the database as well. You can think of opening a database connection like opening a file. You can open a file for reading or you can open a file for writing. The JokeDBAdapter manages these connections as well.
Also inside the JokeDBAdapter class is a static inner class called JokeDBHelper. This helper solves a problem that arises when trying to open a database connection. When you install the application and run it for the first time your database will not exist. You need to execute database creation code on this first run of the application before you can use the database and open a connection. However, all subsequent runs of the application don't need to execute this database creation code. A similar problem occurs if you need to upgrade your database to a new version.
So how do you know whether your database already exists, or whether you need to execute the database creation code? The SQLiteOpenHelper abstract class solves this problem for you. By extending this class and implementing the abstract onCreate(...) and onUpgrade(...), you can test whether the database exists and conditionally execute creation or upgrade code before opening a connection. So, if you haven't guessed it by now, the static inner JokeDBHelper realizes the abstract SQLiteOpenHelper class.
4.1.1 Fill in the JokeDBAdapter.JokeDBHelper Static Inner Class
JokeDBAdapter will request database connections from JokeDBHelper. When a connection is requested, JokeDBHelper will test whether the database exists or needs to be updated, and take whatever actions are necessary before opening the connection. If the database needs to be created, then it will call its JokeDBHelper.onCreate(...) method. If the database needs to upgraded, it will call its JokeDBHelper.onUpgrade(...) method. For more details on this method see the Android Documentation on SQLiteOpenHelper. You will now implement these methods.
4.1.2 Fill JokeDBAdapter Constructor, Open, & Close
Now that the JokeDBHelper class has been established, JokeDBAdapter can use it to open database connections. JokeDBAdapter has a JokeDBHelper member variable aptly name m_dbHelper. You will have to maintain this member variable via the constructor. JokeDBAdapter also has an SQLiteDatabase member variable named m_db that represents your connection to the database. You will manage this connection via the open() and close() methods.
Try running your application to make sure that you don't get any errors. You won't be able to view, add, remove, or update jokes, but you shouldn't get any errors at this point.
4.2 Adding Data to an SQLiteDatabase
You will now implement functionality for adding Jokes. You won't be able to test this until you finish section 4.3.1 which enables you to retrieve jokes, but it's coming up quick so sit tight. Functionality for adding Jokes to the database is encapsulated in the JokeDBAdapter.insertJoke(...) method which you used when updating your AdvancedJokeList.addJoke(...) method.
Jokes are stored in the Database in a single table. The table has an attribute, or column, for each member variable of the Joke. Uniqueness of a Joke is determined by its ID member variable. This value is maintained by the database. When a Joke gets added to the database, the database generates a unique id for it. You can see what the table looks like in the diagram below:
When inserting Jokes into the database you will use an android.content.ContentValues object. For complete background on this class see the Android Documentation on ContentValues. Essentially, a ContentValues object is a key/value map in which you store each piece of the Joke using its corresponding database table attribute name, or column name, as the key. For example, you would store the Joke's rating value under a key of "rating".
4.2.1 Fill in insertJoke(Joke joke)
4.3 Querying an SQLiteDatabase
In order to retrieve jokes from the database, you must execute what is known as a query. A query tells the database to find specific rows in the database as well as which columns should be returned in the result of the query. The result of an SQLiteDatabase query is a Cursor object, which you used in previous changes you made to the JokeList Application. Another way to think of the Cursor is as a subset of the Joke database table. This subset may contain all, none, or some of the rows from the actual Joke database table. Additionally, it may contain all, none, or some of the columns from the Joke database table.
The result of the query depends entirely on the query itself. To perform a query, you use the overloaded SQLiteDatabase.query(...) method. The simplest form of this method takes in the name of the table you want to query and an array containing the names of the columns for which you want information returned. It takes a number of other arguments as well, which have the option of being null. See the Android Documentation on SQLiteDatabase for a complete description of this method.
There is one other argument that this method takes that is of importance. This is the selection string argument. The selection argument allows you to tell the database to only return joke rows that meet a certain criteria. In particular, you can build a string to tell the database to only return joke rows that have a certain rating.
4.3.1 Fill in getAllJokes(String ratingFilter)
This method returns all rows of Jokes in the database that have a rating equal to ratingFilter. However, if the ratingFilter parameter is null, this method should return every Joke row in the database with no exclusions.
JOKE_KEY_RATING + "=" + ratingFilter;
4.3.2 Fill in getAllJokes()
This is an overloaded version of the previous method that returns a cursor containing all Joke rows in the database. Fill in this method so that it returns the result of getAllJokes(null).
4.3.3 Fill in getJokeFromCursor(Cursor cursor)
This static helper method is used to translate a Joke database table row into a Joke object. It should translate the row where the Cursor is currently pointing.
You can now run your application. You should now be able to add Jokes and have them appear in your application. If you close the application and restart it, the Jokes that you previously entered should still be there.
4.3.4 Fill in getJoke(long id)
This method retrieves a single Joke table row from the database. The row retrieved is the row whose JOKE_KEY_ID column value equals the id parameter passed in. This is guaranteed to return one row since no two rows share the same JOKE_KEY_ID column value. This method should return a Joke object constructed from the row returned by the query.
Fill in this method on your own.
After filling this method in, you should be able to upload jokes to the Server, which is the only piece of functionality that uses this method.
4.4 Editing Data in an SQLiteDatabase
You are almost done. You only need to implement two more pieces of functionality. You need to be able to update the contents of a Joke in the database and you need to be able to remove a Joke from the Database.
4.4.1 Fill in removeJoke(Joke joke)
The remove method operates similarly to the getJoke(...) method in that your goal is to identify a single Joke row in the database.
4.4.2 Fill in updateJoke(Joke joke)
The update method should identify a single Joke row in the database and replace all of its column values with the values in the Joke parameter passed into updateJoke(...).
5. Deliverables
To complete this lab you will be required to:
Primary Author: James Reed
Adviser: Dr. David Janzen