- Practice with SQLite as a database
- Create nice and intuitive layouts
- Use the Floating Action Button
For this project, you will create an app that represents a journal. In this app, you should be able to view a list of your journal entries, and of course also add new ones. A journal entry should typically consist of at least a few fields, such as the date/timestamp of the entry, the entry contents and some addition, such as an emoji reprenting your mood at the time of the entry.
For this app, we will make use of the Floating Action Button, which is in accordance with Material Design standards for the Android Platform. The app will consist of three activities, one that holds a list with a possibility to add a new entry. Upon clicking the Floating Action button, the user should be directed to a second activity containing fields that will allow them to add a new journal entry. Finally, a third activity will show details of the selected journal entry,
-
Create a new Android studio project called Journal, using these settings:
- Choose API 24 (Nougat) unless your own phone has an older operating system
- Start with an Empty Activity which is called
MainActivity - Leave all other settings unchanged
-
Create a new, empty repository on the Github website. Name your repository
Journal. -
Now, add a git repository to the project on your computer. Go to Android Studio, and in the menu choose VCS -> Enable Version Control Integration. Choose git as the type and confirm. This will not change much, but sets us up for the next steps.
Note: if you get a popup to ask whether you would like to add some file to the repository, answer "No" for now. If you answer "Yes", things may get complicated later on.
-
Link the local repository to your Github project. Choose VCS -> Git -> Remotes.... Add a remote with name "origin".
-
Android Studio has generated quite a few files for your project already. To add these, let's commit and push those files to Github. Press Cmd-K or Ctrl-K to show the Commit Changes screen. There, you should see a long list of "unversioned files". Make sure all checkboxes are selected, enter a commit message
Initial projectand then press the commit button. Turn off code analysis. -
Press Cmd-Shift-K or Ctrl-Shift-K to show the Push Commits dialog. Press the Push button to send everything to Github.
Your project files should now be visible on Github. If not, ask for help!
Here's a general overview of the app architecture. There will be three activities, as well as a couple of classes that handle database access. Specifically, there's a model class, which represents the core concept of this app: a journal entry.
-
MainActivitywill contain the list of items and a floating action button.-
Change the root layout of the
activity_main.xmlfile to aCoordinatorLayoutas this allows us to add the few components we need and position those in a very simple way. -
Our journal entries will be contained in a list, so add a
ListView, to be found under the Container section of the palette. -
Then, add a floating action button, which is listed under the Design section of the palette. After you have added it, the button is most likely hovering in the upper left corner. Set the
layout_gravitytobottom+endto attach it to the bottom right corner.Setting gravity to
endinstead ofrightensures that the button will actually be attached to the left if used on a phone that is set to a language that is read from right to left, like Arabic. Useful!
-
-
We need to show items in our
ListViewlater, and we'll create a separate layout for those.-
Create a new layout resource file (remember how?), called
entry_row.xml, and add the views that will need to be shown for each journal entry. -
Think about what your list should show and how. Probably the title of the journal entry is most important. Will you show the "mood", too? And the timestamp?
-
-
The second activity, called
InputActivity, should allow the user to input the contents of the journal entry. You might want to change the root layout to a more appropriate choice. Add severalEditTextelements, as well as a button to allow for submission of the entry. -
The third activity, called
DetailActivity, should show the full contents of a journal entry in a visually pleasing way. Build the layout as you like, as long as all four attributes of the entry are represented on-screen.
Let's now add the listeners needed to handle user interactions. Make sure that your listeners are never anonymous. Either have them use their own inner class (remember how?), or simply define a method to be called via the onClick attribute of the layout.
-
Add a regular
onClicklistener to the floating action button. Use anIntentto direct the user to another activity to create a journal entry. -
Create another listener for the confirmation button in this activity, but leave its body empty for now as we do not have our database implemented yet.
-
Add an
OnItemClickListenerto theListView, as well as anOnItemLongClickListener. Again you can leave the actual functionality of the listeners blank for now, as we will implement this later on, when our database is all set up!
Note that the method implemented by
OnItemLongClickListenerreturns aboolean. This boolean indicates whether any further actions should be taken on this layout item after the long click, in other words, whether the regular click should trigger as well. Since we do not want this, we should returntrueat the end of the method implementation, indicating the click was handled by theonItemLongClickmethod and no further action is needed.
To hold the data of our journal entries, we will create a new class that represents them. This class will be called JournalEntry and should have the following fields:
idtitlecontentmoodtimestamp
Also generate a constructor, getters and setters for your class using Cmd+N/Alt+Ins. (For more detailed instructions, have a look at "Modeling friends" in the Friendsr project.)
Also, since we want to pass instances of this class using an Intent, we want to make the class implement Serializable to facilitate this.
To store our journal entries, we will make use of a SQLite database. Let's create a database helper class for this purpose:
-
Create a new Java class named
EntryDatabaseusing the superclassandroid.database.sqlite.SQLiteOpenHelper. This superclass provides a lot of database functionality, but we will need to provide some, too. -
The file will not compile currently, because we haven't provided implementations for all required methods. Press CTRL-I to open up the Implement Methods dialog. Two methods are already selected: press OK to add them both.
-
Finally, we need to create the right constructor for our class. Then press CTRL-O to open up the Override Methods dialog. Choose the simplest constructor (the topmost) and press OK to create it.
We now have the very basic functionality of our database class, but we still need to define what onCreate() will do. Since we are going to store our Entry objects in the database, we need a table that represents these fields accurately. Our table should look like the example below, with columns that represent each field of our Entry model.
| _id | title | content | mood | timestamp |
|---|---|---|---|---|
| 1 | ... | ... | ... | ... |
| 2 | ... | ... | ... | ... |
| 3 | ... | ... | ... | ... |
Now that we know what our database structure should look like, we can create the appropriate SQL query to generate the table. A proper SQL query to create a table should contain the name of the table and the names and data types of the columns in the table.
create table TABLE_NAME (COLUMN1_NAME COLUMN1_TYPE, COLUMN2_NAME COLUMN2_TYPE, COLUMN3_NAME COLUMN3_TYPE);
Since we want to keep track of a timestamp, it's practical to make our table automatically generate a timestamp for each journal entry when it's inserted into the database. It should also auto-increment the entry _id, since each row should have a unique identifier to be able to retrieve it.
Note that due to the structure of the query, you should avoid spaces in your column names. Protected words that denote data types or SQL keywords (such as JOIN, ADD, ACTION, CROSS) should also be avoided as column names, as these might cause your query to be wrongly interpreted.
If you are unsure about your query, you can verify it using services like sqlfiddle which allows you to check whether your query is syntactically correct.
-
Implement
onCreate(): write code that creates a table calledentrieswith columnstitle,content,moodandtimestamp. Also add an_idcolumn of typeINTEGER PRIMARY KEY. You can create a variable of typeStringthat holds your SQL query and then execute the SQL query using thesqliteDatabase.execSQL()method. -
Implement
onUpgrade(): write code that drops the entries table (if it exists) and recreates it by callingonCreate(). This is so we can start with a clean slate in case we want to, or if we want to change the schema (table structure) of our database later on. -
Finally, to your
onCreate(), add some code that creates sample items, so that we can use these to test.
We'll now convert the EntryDatabase class into a Singleton. This means that only one instance of the class can exist at the same time: there can never be multiple instances of the EntryDatabase class. Instead of calling the constructor directly, we ask whether there is currently an instance of EntryDatabase that exists. If so, this instance will be returned to us. Only if it does not exist yet, it will be created.
-
First, make the constructor
privateinstead ofpublic. -
Then, add a private static variable called
instanceof typeEntryDatabase. This is where the unique instance of the class is stored, once made. -
Then, add a public static method called
getInstance()which should accept a parameter of typeContext. This method should return the value ofinstanceif available, and otherwise call the constructor that is now private, providing the right parameters (see SQLite), and storing that ininstance. -
To ensure that everything is in order, place the following line at the bottom of your
MainActivity'sonCreate()method:
EntryDatabase db = EntryDatabase.getInstance(getApplicationContext());You project should now compile and run successfully, though data is not yet displayed.
- Write a method called
selectAll()inEntryDatabase. First, usegetWritableDatabase()to open up the connection with the database. Use the methodrawQueryfrom that object to run aSELECT * FROM entriesquery andreturntheCursor.
The rawQuery method takes two arguments, the first one is the query with placeholders, the second a string array with the strings that should go in place of the placeholders:
rawQuery("SELECT id, example_column FROM table WHERE name = ? AND example_column = ?", new String[] {"2", "column_value"});
Of course since we are selecting everything, we have no placeholders or arguments, so the second argument of rawQuery can be null!
-
Use your custom
entry_row.xmland create a new classEntryAdapterinheriting fromResourceCursorAdapter. Implement a constructorpublic EntryAdapter(Context context, Cursor cursor). Callsuperand pass on thecontextand thecursor, and also theidof the layout that you just made. Tip: layout IDs start withR.layoutand notR.id! -
Implement the abstract method
bindView(), which takes aViewand fills the right elements with data from the cursor.- Use
Cursor.getInt(columnIndex)to retrieve the value of one column as an integer. - Use
Cursor.getColumnIndex(name)to get the column index for a column namedname. - Call
view.findViewById()to get references to the controls in the row layout.
- Use
-
In the
onCreate()of theMainActivity, use theEntryDatabaseto get all records from the database, make a newEntryAdapterand link theListViewto the adapter.
The app should now display all example entries from the database!
- First, link the confirmation button in the
EntryActivityto a new method calledaddEntrythrough its onClick attribute. - In the database class, add a public method
insert()which accepts anEntryobject as its parameter. - In that method, open a connection to the database (see the instructions for select all), and create a new
ContentValuesobject. Use theputmethod to add values fortitle,contentandmood. Then, callinserton the database connection, passing in the right parameters (nullColumnHackmay simply benull).
The ContentValues class offers an easy way to bind values to columns for SQLite. It also prevents user input from directly appearing in the SQL string unescaped and unchecked, making your application less vulnerable to SQL injection.
- Now go back to the activity, use
EntryDatabase.getInstance()to get to the database instance, and call theinsertmethod that we just defined.
Your app should now allow you to insert new entries into the database, which should also show up in your MainActivity when you go back to it.
- In your database class, write a method that accepts a
long idas a parameter. - Using this parameter, call a query that removes the entry with that id from the database. Why do you think we are removing by id and not by title, for example?
- For Android, delete actions are usually tied to long clicking items. Add code to your
OnItemLongClickListenerclass fromMainActivitythat deletes the selected item from the database. If unsure how to retrieve what item was clicked, refer to the section 'Extract what actually was clicked on' from the Friendsr project.
Since we have a list of Cursor objects in our adapter, the item returned by getItemAtPosition is of the type Cursor. Then, when you have the cursor object, you can extract the values of the columns like you did in the bindView() method of your adapter.
To make sure that the list view always displays the most up-to-date information from the database, we are going to update it every time we change something. Since we cannot edit items, this mostly applies to deleting and adding items.
-
In
MainActivity, create aprivatemethod calledupdateData(). -
You will need access to the database, as well as to the adapter. Add private instance variables to your class:
EntryDatabase dbandEntryAdapter adapter. -
In your
onCreate()you already create an instance of theEntryDatabaseand of theEntryAdapter. Change the code to save these instances to the instance variables that we just created.
When determining the scope of your variables, always ask yourself if the scope in which they are available matches the scope in which they are needed. While sometimes it's efficient to declare a variable for the whole class to use, it's certainly not always necessary.
-
Now we can write the body for the method
updateData(). You can use the methodswapCursor()on the adapter to put in a new cursor for the updated data. Where do you get that new cursor? Just callselectAll()on the database again, as you did inonCreate(). -
Call your new method right after calling
delete()from theOnItemLongClickimplementation, so the new list is rendered, without the deleted item. -
When adding items, we are coming back from the previous activity, so we probably need to do something in the
MainActivity'sonResume()!
Finally, we want to be able to access the content of a journal entry, and not just its title, timestamp and associated mood.
- In the
onItemClickof yourListView, write code that fires anIntentto the third activity that shows the entry details. - Add the instance of
Entrythat was clicked on to the intent using aBundle. If unsure how to do this, again refer to the 'Extract what actually was clicked on' section from the Friendsr project. - In the third activity, retrieve the
Intentand the associatedEntryobject and show the contents of the entry in the appropriate views.
-
Have a look at the class diagram at the top of this assignment. Are all classes present in your project? What's the difference?
-
As always, consider this week's assessment criteria and make sure your app works well and the code looks nice.
-
Make sure the app remembers at which point the user was in the listview, so they don't have to rescroll upon rotation or coming back to the list.
-
Allow the user to mark certain entries as 'favorites'.
