Persisting Data

Intro

Using Intent extras to pass data works great as long as the values you pass don't need to persist once your app closes.  Sometimes we need to store values for later use.  As with any Java application, when an Android application stops running, items created at run-time in memory for that program are no longer available.  There are many occasions where you might need your application to save data on the device.  On these occassions, you have three options:

  1. Shared Preferences
  2. Flat Files
  3. SQLite Database

This lesson will focus on the first two options.  Use the following sample Android Studio Project as you progress through the lesson:

Shared Preferences

One of the ways to accomplish file persistence is by using something called Shared Preferences.  Shared Preferences are Singeltons, which means that the same instance is accessible to all activities in the app.  Values are stored as key/value pairs in XML format in the application's built-in data space.  Shared Preferences are best suited for persisting small bits of data...like preferences (thus the name). Because of the manner in which data is stored in Shared Preferences, this technique can be only used to save primitive data type: booleans, floats, ints, longs, and strings.  

To get an instance of an application's Shared Prefences, you use the Preference Manager:

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

 

Reading From Shared Preferences

Once you have an instance, you can use it to read and write data to file.  To read a stored value, you use the method call corresponding to the type of data being retrieves.  The first parameter in the method call is teh data's key, and the second value is the default value to return if no item exists for the given key:   

// get String
String colorString = prefs.getString("color", "ff69b4");

 

Writing To Shared Preferences

To save something to the application's shared preferences, you must first get an instance of the preferences Editor.  You then use this editor to "put" items into Shared Preferences. as with retrieving data, there's a corresponsing method for each data type:

String colorString
...
Editor editor = prefs.edit();
editor.putString("color", colorString);

Once you have called the putX() method for any data you want to persist (where X == the data type), you must finally commit those changes to file using the commit() method.

editor.commit();

For our purposes, editor.commit() is the preferred method for saving SharedPreference values to file.  However, it's not the only way.  If you look at the Developer documentation, there's another method:

editor.apply();

Calling apply accomplishes the same thing in the long run (assuming no errors occur).  So what's the difference? Calling commit applies the change immediately... and synchronously.  Calling apply causes the change to be stored to the in-memory SharedPreferences right away, with final persistence to file happening asynchronously.  It also does not notify you if an error has occurred when saving.

As mentioned above, the Shared Preferences file is stored in the application's data space, and this, is only accessible to the application your application.  We can see what's stored for our app in the AVD's by using the Android Device Monitor.  I'll show you in class, so pay attention!

Flat File Storage

Saving/Retrieving Java Strings

Storing data in flat files allows the application to save larger amounts of data of any type.  Assuming you use the proper file mode, some modecum of protection is provided because all data stored on the device's internal storage is only available to the application (remember sandboxing?).  Often times, a String is saved to file, often times in a format that is easily parsed in Java, such as JSON or XML.  

Saving

To save a String to a file, we'll use a FileOutputStream provided to us by the openFileOutput method of the Context class, :

// what we'll call the file
String FILENAME = "filename1";

// the contents we're saving to file
String string = "this is a string that i want to store.";

FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());

// always close the output stream when finished
fos.close();

The second parameter of the openFileOutput method defines how the file is treated.  The default option is "MODE_PRIVATE", which means the file is only accessible to the app that created it.  Using "MODE_APPEND"  instead appends to an existing file, as opposed to overwriting it, if it exists.  

When looking at the developer page, you'll see two other options: MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE.  These modes are deprecated and should NOT be used because of their obvious security flaws!

Retrieving

To retrieve a String stored in a file, you use an InputStream in a manner very similar to our ourput stream:

private String readFromFile() {
         
        String retrievedString = "";
         
        try {
            InputStream inputStream = openFileInput(FILENAME);
             
            if ( inputStream != null ) {
                InputStreamReader isReader = new InputStreamReader(inputStream);
                BufferedReader bufferedReader = new BufferedReader(isReader);
                
                String tempString = "";
                StringBuilder stringBuilder = new StringBuilder();
                 
                while ( (tempString = bufferedReader.readLine()) != null ) {
                    stringBuilder.append(tempString);
                }
                 
                inputStream.close();
                retrievedString = stringBuilder.toString();
            }
        }
        catch (FileNotFoundException e) {
            Log.e(TAG, "File not found: " + e.toString());
        } catch (IOException e) {
            Log.e(TAG, "Can not read file: " + e.toString());
        }
 
        return retrievedString;
}

 

Saving/Retrieving Java Objects

Java Objects can also be saved to a file.  In order to do this, the Object must be capable of being serialized.  Serialization is a process wherein the data is represented as a sequence of bytes that represent a given Object, it's sub-elements, and all associated type and value data.  This will allow a new Object to be created with the exact same attributes at a later time.  In order to make your Object serializable in Java, it must implement the Android Serializable class.  

This sample method saves a Java Object to file.

public static void saveObjectToFile(Context context, String fileName, Object obj) {
 
    try {
        FileOutputStream fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj); 
        oos.close();

    } catch (FileNotFoundException e) {
        Log.e(LOG_TAG, "saveObjectToFile FileNotFoundException: " + e.getMessage());
    } catch (IOException e) {
        Log.e(LOG_TAG, "saveObjectToFile IOException: " + e.getMessage());
    } catch (Exception e) {
        Log.e(LOG_TAG, "saveObjectToFile Exception: " + e.getMessage());
    }
}

 Here's a sample method that reads an object from a file.  Keep in mind that you would need to cast the object to a type, so you must be careful to implement the proper error checking to ensure the proper type:

public static Object getObjectFromFile(Context context, String filename) {

    try {      
        FileInputStream fis = context.openFileInput(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        
        Object object = ois.readObject();
        ois.close();
  
        return object;

    } catch (FileNotFoundException e) {
        Log.e(LOG_TAG, "getObjectFromFile FileNotFoundException: " + e.getMessage());
        return null;
    } catch (IOException e) {
        Log.e(LOG_TAG, "getObjectFromFile IOException: " + e.getMessage());
        return null;
    } catch (ClassNotFoundException e) {
        Log.e(LOG_TAG, "getObjectFromFile ClassNotFoundException: " + e.getMessage());
        return null;       
    } catch (Exception e) {// Catch exception if any
        Log.e(LOG_TAG, "getBookmarksFromFile Exception: " + e.getMessage());
        return null;
    }
}

Using Serializable is discouraged in many use cases because of the inflexible nature of Serializable objects.  If you modify the class structure in any way at some point in the future (e.g. during debugging or upgrading), the saved Object could become unusable.  A more preferred method is to represent the data in an Object in JSON or XML form (see later lessons), and save the resulting String to file as shown above.

It's also important to note that when retrieving objects from file, you'll have to explicitly cast the object to the expected type.  When doing this, you should ALWAYS implement error checking to verify that the object is actually the type you think it is.