Storing Objects in Android
One alternative to using SQLite on Android is to store Java objects in SharedPreferences. Here, we’ll look at two different ways to do that.
Why not go for SQLite for all storage needs? The reasons can be varied: besides the impedance mismatch between object orientation and relational databases, SQLite might be overkill (brings more overhead) for some simple use cases, or its use and syntax might be disliked altogether.
As an example, we’ll work with the following User class:
/** User object to be saved in db */ public class User{ private int id; // used for object storage private String userName; private boolean registered; private double score; /** Constructor */ public User(int id, String userName, boolean registered, double score){ this.id = id; this.userName = userName; this.registered = registered; this.score = score; } // getters and setters here... }
The unique identifier id would most likely be handed out by our server, though we could also compute it on the device itself once we create a User and store it separately in SharedPreferences, depending on the design of our application. Here, we’ll just use it as an object storage key.
Next, we need to write our preferences class:
/** stores the user object in SharedPreferences */ public class UserPrefs{ /** This application's preferences label */ private static final String PREFS_NAME = "com.our.package.UserPrefs"; /** This application's preferences */ private static SharedPreferences settings; /** This application's settings editor*/ private static SharedPreferences.Editor editor; /** Constructor takes an android.content.Context argument*/ public UserPrefs(Context ctx){ if(settings == null){ settings = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE ); } /* * Get a SharedPreferences editor instance. * SharedPreferences ensures that updates are atomic * and non-concurrent */ editor = settings.edit(); } //... }
Method 1: Flattened Objects with Field Keys
The idea here is that, although SharedPreferences only stores primitive types, an object construct is ultimately a “bag” of primitives. So we can decompose an object into a set of primitives and store each one of those individually with a unique key. However we do need to keep track of which saved field belongs to which object if we want to reconstruct our objects from storage.
So before writing the CRUD methods on a given User object in our UserPrefs class, we need to define unique keys on each User class member. For convenience we can write a method to ensure key uniqueness across the board:
/** The prefix for flattened user keys */ public static final String KEY_PREFIX = "com.our.package.KEY"; /** Method to return a unique key for any field belonging to a given object * @param id of the object * @param fieldKey of a particular field belonging to that object * @return key String uniquely identifying the object's field */ private String getFieldKey(int id, String fieldKey) { return KEY_PREFIX + id + "_" + fieldKey; }
Notice how getFieldKey() gives us a unique identifier per field name and per user.
We now can proceed to write the CRUD methods:
/** generic field keys */ private static final String KEY_USERNAME = "com.our.package.KEY_USERNAME"; private static final String KEY_REGISTERED = "com.our.package.KEY_REGISTERED"; private static final String KEY_SCORE = "com.our.package.KEY_SCORE"; /** Store or Update */ public void setUser(User user){ if(user == null) return; // don't bother int id = user.getId(); editor.putString( getFieldKey(id, KEY_USERNAME), user.getUsername() ); editor.putBoolean( getFieldKey(id, KEY_REGISTERED), user.isRegistered() ); editor.putFloat( getFieldKey(id, KEY_SCORE), user.getScore() ); editor.commit(); } /** Retrieve */ public User getUser(int id){ String name = settings.getString( getFieldKey(id, KEY_USERNAME), "" ); // default value boolean registered = settings.getBoolean( getFieldKey(id, KEY_REGISTERED), false); // default value double score = settings.getFloat( getFieldKey(id, KEY_SCORE), 0); // default value return new User(id, name, registered, score); } /** Delete */ public void deleteUser(User user){ if(user == null) return; // don't bother int id = user.getId(); editor.remove( getFieldKey(id, KEY_USERNAME) ); editor.remove( getFieldKey(id, KEY_REGISTERED) ); editor.remove( getFieldKey(id, KEY_SCORE) ); editor.commit(); }
We can now Create, Retrieve, Update and Delete any User object from anywhere in our application:
// get a SharedPreferences instance UserPrefs prefs = new UserPrefs( this.getApplicationContext() ); // get id from server or local storage // then find User with that id User user = prefs.getUser(id); // operations on User, e.g. user.setRegistered(true); user.setScore(new_score); // save prefs.setUser(user); // ...or delete prefs.deleteUser(user),
For embedded objects, we apply the same logic recursively and call their getters/setters from our User CRUD methods. This method of flattening objects is a simple SQL-less alternative storage on the handset. It has of course its limits as the number of embedded objects increases.
Method 2: Google Gson
Gson is a Java library that provides simple toJson() and fromJson() methods to convert Java objects to the JSON format and vice-versa.
Then we simply store the JSON format as a whole String in SharedPreferences. This method involves adding a new library to our projects, but is considerably more convenient:
// convert User object user to JSON format Gson gson = new Gson(); String user_json = gson.toJson(user); // store in SharedPreferences String id = "" + user.getId(); // get storage key editor.putString(id, user_json); editor.commit(); // time flies... // do the reverse operation user_json = settings.getString(id, ""); user = gson.fromJson(user_json, User.class);
The previous CRUD methods can now be rewritten in our UserPrefs class without the need to use other keys than the User ids. That should be straightforward, and is left to the reader as an exercise.
Conclusion
We’ve looked at a couple of ways of using SharedPreferences to store objects. Other options for working with objects are a number of ORMs for Android (ORMLite, greenDAO, Sugar ORM, ActiveAndroid, etc.), or NOSQL mobile databases such as Couchbase Lite (Beta release at this time). Couchbase Lite is basically a JSON database, while the various ORMs can substantially simplify our code when the data model gets complex and we find ourselves writing lots of boiler-plate SQLite operations. As with all abstractions aiming to reduce underlying complexity, the downside of ORMs is the Law of Leaky Abstractions:
All non-trivial abstractions, to some degree, are leaky.
ORMs and NOSQL alternatives will be the subjects of upcoming articles.