הפוסט זמין גם בעברית כאן - http://iandroid.co.il/dr-iandroid/archives/16066
Hello again,
As i stated in an earlier post, after my Hebrew blog posts started to get published on the wonderful iAndroid website i decided to transform this blog into the English edition.
Slowly but surely i'll translate my earlier posts to English.
For those of you who don't know, all of my posts until now (including this one) are derived from AndconLab; the developer code lab that +Ran Nachmany, +Amir Lazarovich and myself created last year for the great MultiScreenX convention we helped organize.
In the last post we showed how to use ContentProviders briefly without really diving in, this is where this post comes in and it is also is modeled after a session i gave.
In the last post we showed how to use ContentProviders briefly without really diving in, this is where this post comes in and it is also is modeled after a session i gave.
What?
You must be wondering why i'm taking a detour from my usual "best practices" posts and reverting to a seemingly simple, beginner-ish topic, well the answer to that is that although ContentProviders are available since API level 1 i come across developers more times than not that:
- Don't know what ContentProviders are.
- Don't use ContentProviders correctly.
- Don't use ContentProviders enough.
- Are afraid to use ContentProviders.
Why?
The motivation for using ContentProviders could be broken apart to 8 major reasons:
- Non-volatile data storage.
- Mass data storage with low runtime memory overhead.
- Data indexing for ultra-rapid search.
- Possible access from multiple processes (Activities, Services, Applications).
- Offline capabilities.
- Short and easy(-ish) configuration time.
- Flexibility deriving from the fact that all data in all forms is saved in one repository.
- Platform support.
- SharedPreferences will give you 1, 4, 5 and 8 but are lacking in flexibility and in search capabilities.
- Memory caching design patterns will grant you the capabilities embodied in 2,3,6 and 7 but are problematic offline.
- Cloud storage is a wonderful tool but will also not function when your end-user is offline.
- 3rd party ORM libraries such as greenDAO or ORMlite will possibly give you everything but item 8 and since they're essentially open-source libraries they update all the time, requiring more maintenance and possibly crippled in the future by security changes in Android.
How?
Now comes the fun part :)
Let's start with an application which has a data model, called Event, that looks like this:
public class Event{
//////////////////////////////////////////
// Members
//////////////////////////////////////////
@JsonProperty("id") private long mId;
@JsonProperty("name") private String mName;
@JsonProperty("description") private String mDescription;
private List<Lecture> mLectures;
@JsonProperty("logo_url") private String mLogoUrl;
@JsonProperty("website_url") private String mWebsiteUrl;
@JsonProperty("start_date") private String mStartDate;
@JsonProperty("end_date") private String mEndDate;
//////////////////////////////////////////
// Public
//////////////////////////////////////////
//Do something!
//////////////////////////////////////////
// Getters & Setters
//////////////////////////////////////////
//Do something more.
All we have now are the members, getters and setters, now we'll add:
- Support for serializing and deserializing the data so it could be manipulated by implementing the Serializable interface.
- Column definition for our SQLite table.
The result will look like this:
public class Event implements Serializable {
public static final String TABLE_NAME = "events";
public static final String COLUMN_NAME_ID = "_id";
public static final String COLUMN_NAME_NAME = "name";
public static final String COLUMN_NAME_DESCRIPTION = "description";
public static final String COLUMN_NAME_LOGO_URL = "logo_url";
public static final String COLUMN_NAME_WEBSITE_URL = "website_url";
public static final String COLUMN_NAME_START_DATE = "start_date";
public static final String COLUMN_NAME_END_DATE = "end_date";
//////////////////////////////////////////
// Members
//////////////////////////////////////////
@JsonProperty("id") private long mId;
@JsonProperty("name") private String mName;
@JsonProperty("description") private String mDescription;
private List<Lecture> mLectures;
@JsonProperty("logo_url") private String mLogoUrl;
@JsonProperty("website_url") private String mWebsiteUrl;
@JsonProperty("start_date") private String mStartDate;
@JsonProperty("end_date") private String mEndDate;
//////////////////////////////////////////
// Public
//////////////////////////////////////////
public ContentValues getContentValues() {
ContentValues cv = new ContentValues();
cv.put(COLUMN_NAME_DESCRIPTION, mDescription);
cv.put(COLUMN_NAME_END_DATE, mEndDate);
cv.put(COLUMN_NAME_ID, mId);
cv.put(COLUMN_NAME_LOGO_URL, mLogoUrl);
cv.put(COLUMN_NAME_NAME, mName);
cv.put(COLUMN_NAME_START_DATE, mStartDate);
cv.put(COLUMN_NAME_WEBSITE_URL, mWebsiteUrl);
return cv;
}
//////////////////////////////////////////
// Public
//////////////////////////////////////////
//////////////////////////////////////////
// Getters & Setters
//////////////////////////////////////////
Next phase is to define the SQLite table's constructor and its rules, this is done by extending the SQLiteOpenHelper class and overriding some methods, like so:
public class DatabaseHelper extends SQLiteOpenHelper{
public static final String DB_NAME = "db";
public static final int DB_VERSION = 7;
public static final String LECTURE_SPEAKER_PAIT_TABLE = "lecture_speaker_pair";
public static final String PAIR_LECTURE_ID = "lecture_id";
public static final String PAIR_SPEAKER_ID = "speaker_id";
public DatabaseHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
// create events table
StringBuilder sb = new StringBuilder();
try {
DBUtils.createTable(db, sb,
Event.TABLE_NAME,
Event.COLUMN_NAME_ID, "INTEGER PRIMARY KEY ",
Event.COLUMN_NAME_NAME, "TEXT",
Event.COLUMN_NAME_DESCRIPTION, "TEXT",
Event.COLUMN_NAME_START_DATE, "TEXT",
Event.COLUMN_NAME_END_DATE, "TEXT",
Event.COLUMN_NAME_LOGO_URL, "TEXT",
Event.COLUMN_NAME_WEBSITE_URL, "TEXT");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
StringBuilder sb = new StringBuilder();
DBUtils.dropTable(db,sb , Event.TABLE_NAME);
onCreate(db);
}
}
Now, please pay attention to the following:
- In the OnCreate we create a unique SQLite table specifically tailored to our Event model class and its columns are modeled after the members of that class.
- In every table upgrade we delete the table and start from scratch, this is a naive solution and it's not that efficient but it's good enough to start with.
- The name of the SQLite database is defined by us here:
public static final String DB_NAME = "db";
OK, so we have a database, some tables and an object, now it's time to implement some basic CRUD:
public class DBUtils {
private static final String TAG = "DBUtils";
//select lecturerImage from assets where lectureVideoId='Y4UMzOWcgGQ';
/**
* Create DB table
*
* @param db Reference to the underlying database
* @param sb Clears any existing values before starting to append new values
* @param tableName The name of the DB table
* @param columns Tuples of column names and their corresponding type and properties. This field must be even for that same
* reason. I.e. "my_column", "INTEGER PRIMARY KEY AUTOINCREMENT", "my_second_column", "VARCHAR(255)"
*/
public static void createTable(SQLiteDatabase db, StringBuilder sb, String tableName, String... columns) {
if (columns.length % 2 != 0) {
throw new IllegalArgumentException(
"Columns length should be even since each column is followed by its corresponding type and properties");
}
StringUtils.clearBuffer(sb);
// Prepare table
sb.append("CREATE TABLE ");
sb.append(tableName);
sb.append(" (");
// Parse all columns
int length = columns.length;
for (int i = 0; i < length; i += 2) {
sb.append(columns[i]);
sb.append(" ");
sb.append(columns[i + 1]);
if (i + 2 < length) {
// Append comma only if this isn't the last column
sb.append(", ");
}
}
sb.append(");");
// Create table
db.execSQL(sb.toString());
}
/**
* Drop table if exists in given database
*
* @param db Reference to the underlying database
* @param tableName The table name of which we try to drop
*/
public static void dropTable(SQLiteDatabase db, String tableName) {
dropTable(db, new StringBuilder(), tableName);
}
/**
* Drop table if exists in given database
*
* @param db Reference to the underlying database
* @param sb Clears any existing values before starting to append new values
* @param tableName The table name of which we try to drop
*/
public static void dropTable(SQLiteDatabase db, StringBuilder sb, String tableName) {
StringUtils.clearBuffer(sb);
sb.append("DROP TABLE IF EXISTS ");
sb.append(tableName);
// Drop table
db.execSQL(sb.toString());
}
/**
* Stores events and their lectures and speakers in db
* @param db - Writeable SQLITE DB
* @param events - events to be stored
* @return
*/
public static boolean storeEvents(SQLiteDatabase db, List<Event> events) {
db.beginTransaction();
ContentValues cv;
List<Lecture> lectures;
List<Speaker> speakers;
long eventId;
for (Event event : events) {
//store event
cv = event.getContentValues();
db.replace(Event.TABLE_NAME, null, cv);
eventId = event.getId();
//loop through all lectures
lectures = event.getLectures();
for (Lecture lecture : lectures) {
//set event id
lecture.setEventId(eventId);
cv = lecture.getContentValues();
db.replace(Lecture.TABLE_NAME, null, cv);
//remove all speakers from this lecture
clearLectureSpeakers(db, lecture);
//loop through all the speakers
speakers = lecture.getSpeakers();
for (Speaker speaker : speakers) {
//store speaker in db
cv = speaker.getContentValues();
db.replace(Speaker.TABLE_NAME, null, cv);
//add speaker to this lecture
addSpeakerToLecture(db, speaker, lecture);
}
}
}
db.setTransactionSuccessful();
db.endTransaction();
return true;
}
/**
* Fetch all events from DB
* @param db
* @return cursor holding id, name, description and logo url
*/
public static Cursor getEventsCurosr(SQLiteDatabase db) {
String[] cols = new String[] {
Event.COLUMN_NAME_ID,
Event.COLUMN_NAME_NAME,
Event.COLUMN_NAME_DESCRIPTION,
Event.COLUMN_NAME_LOGO_URL
};
Cursor c;
c = db.query(Event.TABLE_NAME, cols, null, null, null, null, Event.COLUMN_NAME_START_DATE + " DESC");
return c;
}
{
As you can see we implemented the following:
- Table creation.
- Table deletion.
- Saving a list of objects of type Event in the database.
- Find the cursor for a location in the SQLite table, which will hold the id and name of the object it is pointing too in the database (could also hold an image or a url with very small modifications).
That's it; we have successfully defined and implement a flexible, durable and versatile mechanism for data storage with ContentProviders, for a usage example please check out my earlier posts or the codelab we created.
I would like to conclude this post by saying thanks again to +Ran Nachmany and +Amir Lazarovich which wrote most of the code which I used in this post and to link you to the full source code for AndConLab here: https://github.com/RanNachmany/AndconLab
All we have now are the members, getters and setters, now we'll add:
Now, please pay attention to the following:
OK, so we have a database, some tables and an object, now it's time to implement some basic CRUD:
As you can see we implemented the following:
I would like to conclude this post by saying thanks again to +Ran Nachmany and +Amir Lazarovich which wrote most of the code which I used in this post and to link you to the full source code for AndConLab here: https://github.com/RanNachmany/AndconLab
- Support for serializing and deserializing the data so it could be manipulated by implementing the Serializable interface.
- Column definition for our SQLite table.
The result will look like this:
public class Event implements Serializable {
public static final String TABLE_NAME = "events";
public static final String COLUMN_NAME_ID = "_id";
public static final String COLUMN_NAME_NAME = "name";
public static final String COLUMN_NAME_DESCRIPTION = "description";
public static final String COLUMN_NAME_LOGO_URL = "logo_url";
public static final String COLUMN_NAME_WEBSITE_URL = "website_url";
public static final String COLUMN_NAME_START_DATE = "start_date";
public static final String COLUMN_NAME_END_DATE = "end_date";
//////////////////////////////////////////
// Members
//////////////////////////////////////////
@JsonProperty("id") private long mId;
@JsonProperty("name") private String mName;
@JsonProperty("description") private String mDescription;
private List<Lecture> mLectures;
@JsonProperty("logo_url") private String mLogoUrl;
@JsonProperty("website_url") private String mWebsiteUrl;
@JsonProperty("start_date") private String mStartDate;
@JsonProperty("end_date") private String mEndDate;
//////////////////////////////////////////
// Public
//////////////////////////////////////////
public ContentValues getContentValues() {
ContentValues cv = new ContentValues();
cv.put(COLUMN_NAME_DESCRIPTION, mDescription);
cv.put(COLUMN_NAME_END_DATE, mEndDate);
cv.put(COLUMN_NAME_ID, mId);
cv.put(COLUMN_NAME_LOGO_URL, mLogoUrl);
cv.put(COLUMN_NAME_NAME, mName);
cv.put(COLUMN_NAME_START_DATE, mStartDate);
cv.put(COLUMN_NAME_WEBSITE_URL, mWebsiteUrl);
return cv;
}
//////////////////////////////////////////
// Public
//////////////////////////////////////////
//////////////////////////////////////////
// Getters & Setters
//////////////////////////////////////////
Next phase is to define the SQLite table's constructor and its rules, this is done by extending the SQLiteOpenHelper class and overriding some methods, like so:
public class DatabaseHelper extends SQLiteOpenHelper{
public static final String DB_NAME = "db";
public static final int DB_VERSION = 7;
public static final String LECTURE_SPEAKER_PAIT_TABLE = "lecture_speaker_pair";
public static final String PAIR_LECTURE_ID = "lecture_id";
public static final String PAIR_SPEAKER_ID = "speaker_id";
public DatabaseHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
// create events table
StringBuilder sb = new StringBuilder();
try {
DBUtils.createTable(db, sb,
Event.TABLE_NAME,
Event.COLUMN_NAME_ID, "INTEGER PRIMARY KEY ",
Event.COLUMN_NAME_NAME, "TEXT",
Event.COLUMN_NAME_DESCRIPTION, "TEXT",
Event.COLUMN_NAME_START_DATE, "TEXT",
Event.COLUMN_NAME_END_DATE, "TEXT",
Event.COLUMN_NAME_LOGO_URL, "TEXT",
Event.COLUMN_NAME_WEBSITE_URL, "TEXT");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
StringBuilder sb = new StringBuilder();
DBUtils.dropTable(db,sb , Event.TABLE_NAME);
onCreate(db);
}
}
Now, please pay attention to the following:
- In the OnCreate we create a unique SQLite table specifically tailored to our Event model class and its columns are modeled after the members of that class.
- In every table upgrade we delete the table and start from scratch, this is a naive solution and it's not that efficient but it's good enough to start with.
- The name of the SQLite database is defined by us here:
public static final String DB_NAME = "db";
OK, so we have a database, some tables and an object, now it's time to implement some basic CRUD:
public class DBUtils {
private static final String TAG = "DBUtils";
//select lecturerImage from assets where lectureVideoId='Y4UMzOWcgGQ';
/**
* Create DB table
*
* @param db Reference to the underlying database
* @param sb Clears any existing values before starting to append new values
* @param tableName The name of the DB table
* @param columns Tuples of column names and their corresponding type and properties. This field must be even for that same
* reason. I.e. "my_column", "INTEGER PRIMARY KEY AUTOINCREMENT", "my_second_column", "VARCHAR(255)"
*/
public static void createTable(SQLiteDatabase db, StringBuilder sb, String tableName, String... columns) {
if (columns.length % 2 != 0) {
throw new IllegalArgumentException(
"Columns length should be even since each column is followed by its corresponding type and properties");
}
StringUtils.clearBuffer(sb);
// Prepare table
sb.append("CREATE TABLE ");
sb.append(tableName);
sb.append(" (");
// Parse all columns
int length = columns.length;
for (int i = 0; i < length; i += 2) {
sb.append(columns[i]);
sb.append(" ");
sb.append(columns[i + 1]);
if (i + 2 < length) {
// Append comma only if this isn't the last column
sb.append(", ");
}
}
sb.append(");");
// Create table
db.execSQL(sb.toString());
}
/**
* Drop table if exists in given database
*
* @param db Reference to the underlying database
* @param tableName The table name of which we try to drop
*/
public static void dropTable(SQLiteDatabase db, String tableName) {
dropTable(db, new StringBuilder(), tableName);
}
/**
* Drop table if exists in given database
*
* @param db Reference to the underlying database
* @param sb Clears any existing values before starting to append new values
* @param tableName The table name of which we try to drop
*/
public static void dropTable(SQLiteDatabase db, StringBuilder sb, String tableName) {
StringUtils.clearBuffer(sb);
sb.append("DROP TABLE IF EXISTS ");
sb.append(tableName);
// Drop table
db.execSQL(sb.toString());
}
/**
* Stores events and their lectures and speakers in db
* @param db - Writeable SQLITE DB
* @param events - events to be stored
* @return
*/
public static boolean storeEvents(SQLiteDatabase db, List<Event> events) {
db.beginTransaction();
ContentValues cv;
List<Lecture> lectures;
List<Speaker> speakers;
long eventId;
for (Event event : events) {
//store event
cv = event.getContentValues();
db.replace(Event.TABLE_NAME, null, cv);
eventId = event.getId();
//loop through all lectures
lectures = event.getLectures();
for (Lecture lecture : lectures) {
//set event id
lecture.setEventId(eventId);
cv = lecture.getContentValues();
db.replace(Lecture.TABLE_NAME, null, cv);
//remove all speakers from this lecture
clearLectureSpeakers(db, lecture);
//loop through all the speakers
speakers = lecture.getSpeakers();
for (Speaker speaker : speakers) {
//store speaker in db
cv = speaker.getContentValues();
db.replace(Speaker.TABLE_NAME, null, cv);
//add speaker to this lecture
addSpeakerToLecture(db, speaker, lecture);
}
}
}
db.setTransactionSuccessful();
db.endTransaction();
return true;
}
/**
* Fetch all events from DB
* @param db
* @return cursor holding id, name, description and logo url
*/
public static Cursor getEventsCurosr(SQLiteDatabase db) {
String[] cols = new String[] {
Event.COLUMN_NAME_ID,
Event.COLUMN_NAME_NAME,
Event.COLUMN_NAME_DESCRIPTION,
Event.COLUMN_NAME_LOGO_URL
};
Cursor c;
c = db.query(Event.TABLE_NAME, cols, null, null, null, null, Event.COLUMN_NAME_START_DATE + " DESC");
return c;
}
{
As you can see we implemented the following:
- Table creation.
- Table deletion.
- Saving a list of objects of type Event in the database.
- Find the cursor for a location in the SQLite table, which will hold the id and name of the object it is pointing too in the database (could also hold an image or a url with very small modifications).
That's it; we have successfully defined and implement a flexible, durable and versatile mechanism for data storage with ContentProviders, for a usage example please check out my earlier posts or the codelab we created.
I would like to conclude this post by saying thanks again to +Ran Nachmany and +Amir Lazarovich which wrote most of the code which I used in this post and to link you to the full source code for AndConLab here: https://github.com/RanNachmany/AndconLab
הפוסט זמין גם בעברית כאן - http://iandroid.co.il/dr-iandroid/archives/16066
Royi is a Google Developer Expert for Android in 2013, a mentor at Google's CampusTLV for Android and (last but not least) the set top box team leader at Vidmind, an OTT TV and Video Platform Provider. www.vidmind.com
No comments:
Post a Comment