Managing Data This week, we'll be looking at managing information. There are actually many ways to store information for later retrieval. In fact, feel free to take a look at the Android Developer pages: https://developer.android.com/guide/topics/data/data-storage.html We could store records in 'the cloud' and retrieve them later, say with some RESTful API. We could create a datafile for our application, and manipulate that. Both have their uses. However, we'll be looking at two other forms of persistence today: (shared) preferences, and databases. (Shared) Preferences Why? Consider the following: you load up your preferred calculator app, and switch to 'formula mode'. The next time you open your calculator, wouldn't it be nice if it remembered that you preferred formula mode last time? For that matter, if you had saved a particular value last time, perhaps it would be useful to still have it available upon the next execution? For other apps, think about all of the customizations you typically have: whether or not to silence when you're using the application, whether you want haptic feedback, how often to update weather information, what tune to play for notifications, accessibility preferences, etc. All of these are things that you wouldn't necessarily want to need to reset for each execution. They also have something else in common that we'll explain very soon. What? A 'shared preference' file is a very basic data management file that stores tuples of key-value pairs. There are different use cases (e.g. a single preferences file for the application that's mostly managed for you, a PreferenceActivity to make changing user settings more consistent across applications, multiple explicitlynamed preference files, 'world-readable' preference files so one application can access the user's preferences from another, etc.), but they all have the same basic mechanisms: Open a preference file Read by querying on a key Write by updating a new value for a key Because of the tuple-nature of the data, preferences are not for storing things like complex data, records, etc. Why could the calculator's saved memory be an exception? Because there's always only one memory bank (or always a fixed-size set of memory values). There's no table of records for selecting.
There are some very good reads here: https://developer.android.com/guide/topics/ui/settings.html https://developer.android.com/reference/android/preference/preferenceactivity.html https://developer.android.com/training/basics/data-storage/shared-preferences.html https://developer.android.com/reference/android/content/sharedpreferences.html Let's try a trivial example, just to demonstrate the mechanism. Let's start with a layout: <?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:layout_height="match_parent" tools:context="ca.brocku.efoxwell.a2017_sixthstage.mainactivity" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:text="do we have a preference?" android:id="@+id/preferentialdisplay" <EditText android:id="@+id/preferentialinput" android:onclick="buttony" <Button android:layout_width="wrap_content" android:text="save" android:id="@+id/preferentialretain" android:onclick="buttony" <Button android:layout_width="wrap_content" android:text="load" android:id="@+id/preferentialrestore" android:onclick="buttony" <Button android:id="@+id/doneprefs" android:text="next!" android:onclick="buttony" </LinearLayout> As you can see, it's just a couple buttons to trigger the basic actions we'll be wanting: loading and saving. On the code side, I think I'll abstract those behaviours out into separate methods. Upon creating the Activity, I'll have it invoke the retrieval behaviour, to auto-initialize the display. We've already seen this style of retrieving data. If we want something that doesn't exist, we'll need to provide a default value. In this case, we'll be storing a String, and if it doesn't exist, we'll just go with the empty string. Finally, empty strings are boring, so our code will only display a value if it has a 'real' one.
Let's look at retrieving data first: private void retrieve() { //Of course, we don't normally use this for widgets! SharedPreferences memories=getpreferences(context.mode_private); //we could use getsharedpreferences if we wanted to choose the preference file //Also, 0 would have been just fine for the mode TextView display=(textview)findviewbyid(r.id.preferentialdisplay); String value=memories.getstring("somekey","");//the second term is the default value if (!value.equals("")) display.settext(value); So let's explain the pieces here: The SharedPreferences object, used this way, loads the default preferences file for the application If you want to try opening in different modes, know that many access mechanisms have been deprecated by recent versions of Android (basically, if you want to share preferences across applications, it's possible, but less pleasant) The query syntax is basically the same as our 'extras' Of course, our oncreate can just call this at the end. Next, how can we save? //It's often a good idea to attach this kind of thing to onstop private void store() { TextView entry=(edittext)findviewbyid(r.id.preferentialinput); String value=entry.gettext().tostring(); if (!value.equals("")) { SharedPreferences memories=getpreferences(context.mode_private); SharedPreferences.Editor editor=memories.edit(); editor.putstring("somekey",value); //Please don't forget this part: editor.commit(); Note that, to make changes, we need to use an Editor. And please, pretty please, don't forget to commit the changes! By now, the buttons should be trivial: public void buttony(view v) { switch (v.getid()) { case R.id.preferentialRetain: store(); break; case R.id.preferentialRestore: retrieve(); break; case R.id.donePrefs: //start next activity break;
Well, let's give it a try! Reminders about preferences Though our simple example obviously worked, don't forget that this is not how one would typically interact with the data in a preference file. Most commonly, there are premade Activities/Fragments for managing actual user preferences. That's why most Android applications have effectively identical designs for their user settings: because it's provided for you. Also, though I'm sure you could find a way to jury-rig them into storing arbitrary data, you really shouldn't try to move outside the paradigm of single set of key-value tuples. Storing records Okay, so we shouldn't be using preference files to store arbitrary data/records. Well, if that's what we shouldn't use, then what should we? Let's pull back from Mobile and ask the same question for any other computer: I have several records, and wish to store them in an organized fashion, that will allow for later searching, retrieval, and updates; what do I do? A database? Then I guess that's our answer here, as well! SQLite Android includes SQLite. As the name implies, SQLite is a database management system for lightweight databases. How lightweight? Well, you probably wouldn't want ot use it on a server for most tasks, but it's just spiffy for single-user operation, which makes it perfect for storing data for a single application. That's why many programs use it for storing user settings, personal data, etc. (e.g. web browsers often use it for your cookies, bookmarks, and settings). It's worth noting that, between Android and Android Studio, you're also provided with a couple more tools for managing and inspecting SQLite databases, but we don't need to worry about that today. Source of database In order to user a database, we'll of course need to have said database. Depending on your needs, you could create it completely in advance, and distribute it with your application. Alternatively, your application can build it (e.g. on the first execution). Our example will be doing the latter. Operation The operation isn't terribly difficult. Android includes an abstract SQLiteOpenHelper class to help with opening and accessing the database. Queries use a Cursor to keep track of where you are within the query results.
First example Let's start very small. We'd like: To create a database if we don't already have one An activity for creating a new entry An activity to let us view one piece of data Some way of removing the data Creating the database As mentioned earlier, Android includes an SQLiteOpenHelper to help you manage your databases. It actually already does nearly all of the work for you. There's just one catch: If the desired database doesn't yet exist, it needs to be able to create it. But how would it know what to create? That's where you come in. Let's create a simple Java class: public class DataHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 2; public static final String DB_NAME = "sophia"; public static final String DB_TABLE = "wisdom"; public static final int DB_VERSION = 1; private static final String CREATE_TABLE = "CREATE TABLE " + DB_TABLE + " (rule INTEGER PRIMARY KEY, subject TEXT, lesson TEXT);"; DataHelper(Context context) { super(context,db_name,null,database_version); public void oncreate(sqlitedatabase db) { db.execsql(create_table); public void onupgrade(sqlitedatabase db, int oldversion, int newversion) { //How to migrate or reconstruct data from old version to new on upgrade Note that I skipped the package/import lines. So, what's going on here? This particular database is called sophia We have a single table, wisdom Each entry under wisdom is indexed by a rule number, and has both a subject and a lesson text
So far, so good. We can now use that whenever we want to access the database! We're going to have three basic operations for now: adding entries, showing a single entry, and wiping out the database. In a real application, there'd be more (e.g. editing existing entries; showing multiple entries simultaneously, probably using an adapter; and deleting individual records), but this is enough to see it working. Making a new entry Let's make our first Activity, NewWisdom, and have our initial activity start it upon the button press. First, the layout: <?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:layout_height="match_parent" tools:context="ca.brocku.efoxwell.a2017_sixthstage.newwisdom" android:orientation="vertical"> <EditText android:id="@+id/editsubject" <EditText android:id="@+id/editlesson" <Button android:text="save!" android:onclick="save" </LinearLayout> and now the Java code: public class NewWisdom extends AppCompatActivity { protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_new_wisdom); public void save(view v) { EditText subwidget=(edittext)findviewbyid(r.id.editsubject); EditText leswidget=(edittext)findviewbyid(r.id.editlesson); DataHelper dh=new DataHelper(this); SQLiteDatabase datachanger=dh.getwritabledatabase(); ContentValues newwisdom=new ContentValues(); newwisdom.put("subject",subwidget.gettext().tostring()); newwisdom.put("lesson",leswidget.gettext().tostring()); datachanger.insert(datahelper.db_table,null,newwisdom); datachanger.close(); //startactivity(new Intent(this,ShowWisdom.class));
Let's look at what's new: First, see how our DataHelper actually helps us: we use it to open the database for us. If we want to make changes, get a writable database; otherwise get a readable one The way we encapsulate field values within a record is as a ContentValues We aren't explicitly specifying the rule number here, because we'd rather SQLite handle that for us After we're done, we explicitly close the database Starting the next Activity isn't important right now. It'll just be a temporary convenience thing. Let's give it a sample run, and verify that it works. More specifically, verify that it doesn't crash for some reason. Try adding an entry or two. Viewing an entry Queries are pretty easy in SQLite. Most of you already have some experience with databases, so this should be trivial. There's one thing worth noting: when we query for a selection of records, what we'll receive will be a Cursor. In this context, a Cursor is effectively a combination of a data structure, and your position within that data structure (think: cursored lists from 1P03). If our focus were on elaborate queries, we'd cover the finer points of SQL syntax, but you already know all that, so we're going to do this the easy way: we'll query everything, and simply ignore everything that isn't the first line. Let's make a new Activity, ShowWisdom. Our layout will be as follows: <?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:layout_height="match_parent" tools:context="ca.brocku.efoxwell.a2017_sixthstage.showwisdom" android:orientation="vertical"> <TextView android:id="@+id/showrule" <TextView android:id="@+id/showsubject" <TextView android:id="@+id/showlesson" <Button android:onclick="click" </LinearLayout>
The code will be pretty easy: public class ShowWisdom extends AppCompatActivity { protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_show_wisdom); query(); private void query() { String[] fields=new String[]{"rule","subject","lesson"; TextView rulwidget=(textview)findviewbyid(r.id.showrule); TextView subwidget=(textview)findviewbyid(r.id.showsubject); TextView leswidget=(textview)findviewbyid(r.id.showlesson); DataHelper dh=new DataHelper(this); SQLiteDatabase datareader=dh.getreadabledatabase(); Cursor cursor=datareader.query(datahelper.db_table,fields, null,null,null,null,null); cursor.movetofirst(); if (!cursor.isafterlast()) { rulwidget.settext(""+cursor.getint(0)); subwidget.settext(cursor.getstring(1)); leswidget.settext(cursor.getstring(2)); if (cursor!=null &&!cursor.isclosed()) cursor.close(); public void click(view v) { //startactivity(new Intent(this,BobbyTables.class)); If we did want to inspect more of the results, the Cursor can be advanced via movetonext(). You'll have good reason to look into this again very soon, so for now, ensure that you can get this simpler example working, and then work on expanding later. Deletion Selective deletion is left as an exercise for now, but let's just take a gander at the nuclear option. I had one more Activity, BobbyTables. In it, behind a confirmation button, I have this: deletedatabase(datahelper.db_name); It kills records dead. One additional observation for our 'DataHelper': note that it's very minimal. Currently, we have code for insertion in one Activity, code for viewing in another, code for deletion in another, etc. Since those are all database-related actions, it would be entirely reasonable (and normal) to shift those over into the helper class.
Give everything one last test, to see what's going on. It should be easy to see how readily expandable it is. This is ridiculous Yup, it is. Why? Because we keep shimmying back and forth between Activities. To say that's less than ideal would be an understatement. If we wanted to, we could easily create a single Activity to act as our primary launch point to other tasks. We could even put each of the tasks into separate Fragments. However, what we're really talking about is an application that has different modes, into which we'd like to jump. How would we normally expect to achieve something like that? Exactly: menus. (If we have time) Menus Menus are our default go-to whenever we have a multitude of things we could do, and a long strip of buttons would be a bit unwieldy. There are different usages for menus, but they mostly fall under one of the following: Options menus Or an Activity menu. This is what you typically have that's always available to you (e.g. the 'three dots' in the corner of the Activity), to let you switch into a different mode, or initiate an action within the current display Context menus When you 'long-press' an entry in Android, you'll sometimes bring up a popup menu. That's also Submenus known as a context menu. It provides actions or information, normally specific to the exact widget/entry you just long-pressed Sometimes you can't fit everything into a single menu (or simply don't want to). If you have multiple entries that all tie to the same topic, then you might wish to bundle them all up together, and have them appear as a collection of menu options, under a single entry in a parent menu It's worth noting that, depending on your API level, you may have different options, or even the same options but behaving differently. For example, the Action Bar (a special widget to replace the normal title bar) was added quite a while ago.
Defining a menu (Prepare for deja vu) We could use Java to programmatically generate menus, and sometimes still might if there's a specific need to warrant it, but typically we'll define the menus in XML files. The default folder for a menu is res/menu. Depending on which entry you used in Android Studio to create your Activities, you might already have this folder (along with an accompanying sample menu). If not, just create the folder. We'll create our first menu, and just call it tasks. We'll start small, and expand it later: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_greet" android:title="about" android:icon="@mipmap/ic_launcher" </menu> For now, let's just deal with what we can do with our initial activity. We need to add two separate chunks of code: public boolean oncreateoptionsmenu(menu menu) { MenuInflater inflater=getmenuinflater(); inflater.inflate(r.menu.tasks,menu); return true; This tells it how to construct the options menu. Note that we're overriding the default behaviour of, don't. Next: public boolean onoptionsitemselected(menuitem item) { Toast.makeText(this,"Hello",Toast.LENGTH_SHORT).show(); return true; This tells it how to handle clicking on a menu item. Of course, after this, we'll be comparing against ids. Expanding the idea Let's make some changes: Our menu should have new options for the tasks we've already implemented I think I'd prefer a static class to handle those menu tasks We no longer need the buttons to start new activities (but still need two of them for confirmation)
The new menu: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_new" android:title="add entry" <item android:id="@+id/menu_show" android:title="show entry" <item android:id="@+id/menu_drop" android:title="purge database" <item android:id="@+id/menu_greet" android:title="about" android:icon="@mipmap/ic_launcher" </menu> Our first change to MainActivity: public boolean onoptionsitemselected(menuitem item) { return ModeSwitcher.handleMenuClicky(item,this); ModeSwitcher.java: public class ModeSwitcher { public static boolean handlemenuclicky(menuitem item, Context from) { //Activity? switch (item.getitemid()) { case R.id.menu_new: from.startactivity(new Intent(from,NewWisdom.class)); ((Activity)from).finish(); break; case R.id.menu_show: from.startactivity(new Intent(from,ShowWisdom.class)); ((Activity)from).finish(); break; case R.id.menu_drop: from.startactivity(new Intent(from,BobbyTables.class)); ((Activity)from).finish(); break; case R.id.menu_greet: Toast.makeText(from,"Hello",Toast.LENGTH_SHORT).show(); break; return true; In case it isn't readily apparent, the reason I'm doing this one this way is because we're going to be simply copying the menu to each of the activities. Since the code's entirely redundant, I preferred to have it centralized. But you wouldn't normally have each Activity share the same options menu. Normally, options menus show tasks specific to the displayed Activity.
Changes to the task Activities: public boolean oncreateoptionsmenu(menu menu) { MenuInflater inflater=getmenuinflater(); inflater.inflate(r.menu.tasks,menu); return true; public boolean onoptionsitemselected(menuitem item) { return ModeSwitcher.handleMenuClicky(item,this); Of course, we just tack this onto the end of each of them. Don't forget to also remove the startactivitys from any of them that still have it. Give it a try, and see how it works. Context Menus Let's say I want to have a long press menu attached to a displayed record. If there's time, I think this might be a good excuse to finally expand on the database query slightly. Let's create one more Activity, ListWisdom. For the layout, just add a single ListView, with an id of allentries. For the code: public class ListWisdom extends AppCompatActivity { protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_list_wisdom); query(); private void query() { String[] fields=new String[]{"rule","subject","lesson"; ListView lv=(listview)findviewbyid(r.id.allentries); ArrayList<String> entries=new ArrayList<>(); DataHelper dh=new DataHelper(this); SQLiteDatabase datareader=dh.getreadabledatabase(); Cursor cursor=datareader.query(datahelper.db_table,fields, null,null,null,null,null); cursor.movetofirst(); while (!cursor.isafterlast()) { entries.add(integer.tostring(cursor.getint(0))+", "+ cursor.getstring(1)+", "+cursor.getstring(2)); cursor.movetonext(); if (cursor!=null &&!cursor.isclosed()) cursor.close(); ArrayAdapter<String> adapter=new ArrayAdapter<>(this, android.r.layout.simple_list_item_1,entries);
lv.setadapter(adapter); registerforcontextmenu(lv); datareader.close(); public void oncreatecontextmenu(contextmenu menu, View v, ContextMenu.ContextMenuInfo menuinfo) { if (v.getid()==r.id.allentries) { ListView lv=(listview) v; AdapterView.AdapterContextMenuInfo cmi= (AdapterView.AdapterContextMenuInfo) menuinfo; String entry=(string)lv.getitematposition(cmi.position); menu.setheadertitle(entry); menu.add("agree"); menu.add("disagree"); public boolean oncontextitemselected(menuitem item) { Toast.makeText(this,item.getTitle(),Toast.LENGTH_SHORT).show(); return true; public boolean oncreateoptionsmenu(menu menu) { MenuInflater inflater=getmenuinflater(); inflater.inflate(r.menu.tasks,menu); return true; public boolean onoptionsitemselected(menuitem item) { return ModeSwitcher.handleMenuClicky(item,this); Some observations: When you want a context menu, it's handled nearly identically to any other menu. The difference is that you register individual widgets for menus, instead of the Activity as a whole For the sake of comparison, this menu is programmatically created. There are a few extra nifty options, but honestly if you need anything remotely complicated, you shouldn't be creating it via the Java code We're not actually making any significant use of the selected contextual command Before testing it out, remember to actually add this new entry to the menu, and add the corresponding code to the ModeSwitcher class.
Submenus And, finally, submenus. Programmatically, these can be a bit of a pain, so we'll stick to XML-defined ones. Submenus in general are very simple: they're just menus within menus. There's a quirk, but it's easy to spot: <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" > <item android:id="@+id/menu_new" android:title="add entry" <item android:title="query"> <menu> <item android:id="@+id/menu_show" android:title="show entry" <item android:id="@+id/menu_list" android:title="list all entries" </menu> </item> <item android:id="@+id/menu_drop" android:title="purge database" <item android:id="@+id/menu_greet" android:title="about" android:icon="@mipmap/ic_launcher" app:showasaction="ifroom" </menu> While we were at it, the About entry has also changed slightly. Of course, there are also additional menu options, like check boxes and such (use group, along with checkablebehavior), you can have the application icon appear in the corner for clicking, and with later API levels even more was added. But this is a good starting point. If you'd like to learn more, this is a good resource: https://developer.android.com/guide/topics/ui/menus.html huh... there's still space down here... Did we ever do anything about that spare button for showing a single record? Can we guess what getsupportactionbar() is for?