- 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 project
and 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.
-
MainActivity
will contain the list of items and a floating action button.-
Change the root layout of the
activity_main.xml
file to aCoordinatorLayout
as 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_gravity
tobottom
+end
to attach it to the bottom right corner.Setting gravity to
end
instead ofright
ensures 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
ListView
later, 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 severalEditText
elements, 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
onClick
listener to the floating action button. Use anIntent
to 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
OnItemClickListener
to 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
OnItemLongClickListener
returns 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 returntrue
at the end of the method implementation, indicating the click was handled by theonItemLongClick
method 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:
id
title
content
mood
timestamp
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
EntryDatabase
using 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 calledentries
with columnstitle
,content
,mood
andtimestamp
. Also add an_id
column of typeINTEGER PRIMARY KEY
. You can create a variable of typeString
that 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
private
instead ofpublic
. -
Then, add a private static variable called
instance
of 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 ofinstance
if 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 methodrawQuery
from that object to run aSELECT * FROM entries
query andreturn
theCursor
.
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.xml
and create a new classEntryAdapter
inheriting fromResourceCursorAdapter
. Implement a constructorpublic EntryAdapter(Context context, Cursor cursor)
. Callsuper
and pass on thecontext
and thecursor
, and also theid
of the layout that you just made. Tip: layout IDs start withR.layout
and notR.id
! -
Implement the abstract method
bindView()
, which takes aView
and 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 theEntryDatabase
to get all records from the database, make a newEntryAdapter
and link theListView
to the adapter.
The app should now display all example entries from the database!
- First, link the confirmation button in the
EntryActivity
to a new method calledaddEntry
through its onClick attribute. - In the database class, add a public method
insert()
which accepts anEntry
object as its parameter. - In that method, open a connection to the database (see the instructions for select all), and create a new
ContentValues
object. Use theput
method to add values fortitle
,content
andmood
. Then, callinsert
on the database connection, passing in the right parameters (nullColumnHack
may 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 theinsert
method 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 id
as 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
OnItemLongClickListener
class fromMainActivity
that 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 aprivate
method calledupdateData()
. -
You will need access to the database, as well as to the adapter. Add private instance variables to your class:
EntryDatabase db
andEntryAdapter adapter
. -
In your
onCreate()
you already create an instance of theEntryDatabase
and 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 theOnItemLongClick
implementation, 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
onItemClick
of yourListView
, write code that fires anIntent
to the third activity that shows the entry details. - Add the instance of
Entry
that 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
Intent
and the associatedEntry
object 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'.