פיתוח: תקשורת עם השרת #2


פורסם ב 03/10/2013 ע"י Royi Benyossef

שלום שוב,

למי ששכח, הוקרה קצרה; הבלוג הזה מתבסס לחלוטין על עבודתי המשותפת עם +Ran Nachmany ו- +Amir Lazarovich על Developer lab בשם AndconLab שהעברנו בכנס MultiScreenX השנה.

בפוסט הקודם עסקנו במתן הסברים ודוגמאות לפעולת התקשורת עם השרת בעצמה ולכן קצת שיקרתי לכם, משום שנתתי לכם מימוש חלקי של ה-TransactionJob, הנה המימוש המלא:

private class TransactionJob implements Runnable {
private static final String TAG = "Service";
@Override
public void run() {
try {
HttpGet httpGet = new HttpGet(GET_EVENTS);
String responseBody = null;
try {
responseBody = mHttpClient.execute(httpGet, mResponseHandler);
} catch(Exception ex) {
ex.printStackTrace();
}
List<Event> events = null;
if(responseBody != null) {
events = JacksonUtils.sReadValue(responseBody, new TypeReference<List<Event>>() {}, false);
if (events != null) {
SQLiteDatabase db = new DatabaseHelper(CommunicationService.this.getApplicationContext(), DatabaseHelper.DB_NAME, null, DatabaseHelper.DB_VERSION).getWritableDatabase();
DBUtils.storeEvents(db, events);
//fire an intent
Intent intent = new Intent();
intent.setAction(RESULTS_ARE_IN);
CommunicationService.this.sendBroadcast(intent);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

למי שאין לו כח לחפש ע"מ להבין מה אני רוצה אז ההבדל הוא בתוספת של השורות האלה:



SQLiteDatabase db = new DatabaseHelper(CommunicationService.this.getApplicationContext(), DatabaseHelper.DB_NAME, null, DatabaseHelper.DB_VERSION).getWritableDatabase();
DBUtils.storeEvents(db, events);

לחלקיכם בוודאי עולה השאלה: מה זה משנה? ואולי אפילו המחשבה אתה צוחק עלי? למה לי להאט את הביצוע עם התעסקות ב-data base?

ובכן, על מנת לענות על השאלה הזו אני אקח צעד אחד אחורה.

בדרך כלל במדריכים מעין אלה אתם רגילים לראות את הדוגמא הטקטית, כלומר: AsyncTask שמבצע פעולת הבאה ופרסור של הבקשה מהשרת ופחות או יותר זהו, מה שאני רוצה להראות בסדרת הפוסטים הללו זו הדוגמא האסטרטגית שיכולה להיות מתוארת בצורה הבאה: "אל אילו קרבות עלי לוותר מראש על מנת לנצח במלחמה?"

זהו מקרה קלאסי בו רוב המפתחים בוחרים לוותר על שימוש ב-ContentProviders ע"מ להאיץ את ההורדה של המידע אבל בכך הם למעשה כופים על עצמם להוריד את המידע כל פעם מחדש ולכן האפליקציה עלולה להרגיש איטית ומסורבלת יותר ובנוסף הם גם מבזבזים את סוללת המכשיר של המשתמש תוך כדי שהם משתמשים ביותר תעבורת אינטרנט (שעלולה גם לעלות כסף ממשי למשתמש).

לכן, בואו נתחיל במתי אני קורא לתהליך הזה ומתי אני פשוט לוקח את המידע הזמין לי, לשם כך הנה מימוש ה-Activity:



public class MainActivity extends SherlockActivity implements OnItemClickListener{
private ListView mList;
private ProgressDialog mProgressDialog;
private BroadcastReceiver mUpdateReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
mList = (ListView) findViewById(R.id.list);
mList.setOnItemClickListener(this);
}
@Override
protected void onPause() {
super.onPause();
if (null != mUpdateReceiver) {
unregisterReceiver(mUpdateReceiver);
mUpdateReceiver = null;
}
}
@Override
protected void onResume() {
super.onResume();
mUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equalsIgnoreCase(CommunicationService.RESULTS_ARE_IN)) {
new lecturesLoader().execute((Void) null);
}
if (null != mProgressDialog)
mProgressDialog.dismiss();
}
};
final IntentFilter filter = new IntentFilter();
filter.addAction(CommunicationService.RESULTS_ARE_IN);
registerReceiver(mUpdateReceiver, filter);
new lecturesLoader().execute((Void)null);
}
@Override
public void onItemClick(AdapterView<?> list, View view, int position, long id) {
Intent i = new Intent(this,SingleLectureActivity.class);
i.putExtra(SingleLectureActivity.EXTRA_LECTURE_ID, id);
startActivity(i);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getSupportMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_refresh:
refreshList(true);
return true;
default:
return super.onOptionsItemSelected(item);
}
}

private void refreshList(boolean showProgressbar) {

if (showProgressbar) {

mProgressDialog = ProgressDialog.show(this, getString(R.string.progress_dialog_starting_title), getString(R.string.progress_dialog_starting_message));

}

Intent i = new Intent(this,CommunicationService.class);

startService(i);

}

////////////////////////////////

// Async task that queries the DB in background

////////////////////////////////

private class lecturesLoader extends AsyncTask<Void, Void, Cursor> {

private SQLiteDatabase db;

@Override

protected Cursor doInBackground(Void... params) {

db = new DatabaseHelper(MainActivity.this.getApplicationContext(), DatabaseHelper.DB_NAME,null , DatabaseHelper.DB_VERSION).getReadableDatabase();

return DBUtils.getAllLectures(db);

}

@Override

protected void onPostExecute(Cursor result) {

if (0 == result.getCount()) {

//we don't have anything in our DB, force network refresh

refreshList(true);

}

else {

LecturesAdapter adapter = (LecturesAdapter) mList.getAdapter();

if (null == adapter) {

adapter = new LecturesAdapter(MainActivity.this.getApplicationContext(), result);

mList.setAdapter(adapter);

}

else {

adapter.changeCursor(result);

}

}

db.close();

}

}
}

הסבר קצר למה שאתם רואים הוא כזה:

  1. האפליקציה עולה.

  2. ה-Broadcast intent עבור הפעולה "RESULTS_ARE_IN" נרשם.

  3. ה-AsyncTask בשם LectureLoader מאותחל והוא בודק האם יש לנו מידע שמור ב-database כאשר אם כן האפליקציה מאותחלת מהמידע הקיים אחרת היא קוראת ל-refreshList שמתחילה את ה-Service עליו דיברתי בפוסט הקודם.

  4. (באם אותחל ה-Service העונה לשם CommunicationService אזי בקבלת המידע הוא אינו מטריח את ה-UI כי אם מעדכן את ה-database ושולח את ה-Broadcast intent שנרשם בסעיף #2.

עכשיו אחרי שהבנתם למה אז הנה המימוש של הטיפול ב-database:


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; } private static void addSpeakerToLecture(SQLiteDatabase db, Speaker speaker, Lecture lecture) { ContentValues cv = new ContentValues(); cv.put(DatabaseHelper.PAIR_LECTURE_ID, lecture.getId()); cv.put(DatabaseHelper.PAIR_SPEAKER_ID, speaker.getId()); db.insert(DatabaseHelper.LECTURE_SPEAKER_PAIT_TABLE, null, cv); } private static void clearLectureSpeakers(SQLiteDatabase db, Lecture lecture) { db.delete(DatabaseHelper.LECTURE_SPEAKER_PAIT_TABLE, DatabaseHelper.PAIR_LECTURE_ID + "=" + lecture.getId(), null); } /** * 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; } public static Cursor getLecturesByEventId(SQLiteDatabase db, long eventId) { String[] cols = new String[] { Lecture.COLUMN_NAME_ID, Lecture.COLUMN_NAME_NAME, Lecture.COLUMN_NAME_DESCRIPTION, }; Cursor c; c = db.query(Lecture.TABLE_NAME, cols, Lecture.COLUMN_NAME_EVENT_ID +" = " +eventId, null, null, null, Lecture.COLUMN_NAME_NAME); return c; } public static Cursor getAllLectures (SQLiteDatabase db) { String[] cols = new String[] { Lecture.COLUMN_NAME_ID, Lecture.COLUMN_NAME_NAME, Lecture.COLUMN_NAME_DESCRIPTION, Lecture.COLUMN_NAME_DURATION }; return db.query(Lecture.TABLE_NAME, cols, null, null, null, null, Lecture.COLUMN_NAME_EVENT_ID + " DESC"); } /** * Fetches a lecture from db * @param db * @param id * @return Lecture object or null if no lecture found. */ public static Lecture getLectureById (SQLiteDatabase db, long id) { Cursor c = db.query(Lecture.TABLE_NAME, null, Lecture.COLUMN_NAME_ID + "=" + id, null, null, null, null); Lecture lecture = new Lecture(); if (c.moveToNext()) { lecture.buildFromCursor(c); c.close(); return lecture; } return null; } public static ArrayList<Speaker> getSpeakersByLectureId (SQLiteDatabase db, long id) { ArrayList<Speaker> speakers = new ArrayList<Speaker>(); String select = "SELECT * FROM " + Speaker.TABLE_NAME +" WHERE " + Speaker.COLUMN_NAME_ID +" IN ("+ " SELECT " + DatabaseHelper.PAIR_SPEAKER_ID + " FROM " + DatabaseHelper.LECTURE_SPEAKER_PAIT_TABLE + " WHERE " + DatabaseHelper.PAIR_LECTURE_ID + " = " +id +")"; Cursor c = db.rawQuery(select, null); Speaker speaker; while (c.moveToNext()) { speaker = new Speaker(); speaker.buildFromCursor(c); speakers.add(speaker); } c.close(); return speakers; } }

והנה ה-database helper:


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"); //create lectures table DBUtils.createTable(db, sb, Lecture.TABLE_NAME, Lecture.COLUMN_NAME_ID, "INTEGER PRIMARY KEY ", Lecture.COLUMN_NAME_DESCRIPTION, "TEXT", Lecture.COLUMN_NAME_DURATION, "TEXT", Lecture.COLUMN_NAME_EVENT_ID, "INTEGER", Lecture.COLUMN_NAME_NAME, "TEXT", Lecture.COLUMN_NAME_SLIDES_URL, "TEXT", Lecture.COLUMN_NAME_VIDEO_URL, "TEXT", Lecture.COLUMN_NAME_YOUTUBE_ASSET_ID, "TEXT"); //create speakers table DBUtils.createTable(db, sb, Speaker.TABLE_NAME, Speaker.COLUMN_NAME_ID, "INTEGER PRIMARY KEY", Speaker.COLUMN_FIRST_NAME, "TEXT", Speaker.COLUMN_LAST_NAME, "TEXT", Speaker.COLUMN_NAME_BIO, "TEXT", Speaker.COLUMN_NAME_IMAGE_URL, "TEXT"); //create lecture <-> speaker pair table DBUtils.createTable(db, sb, LECTURE_SPEAKER_PAIT_TABLE, PAIR_LECTURE_ID, "TEXT" , PAIR_SPEAKER_ID, "TEXT"); } catch (Exception e) { e.printStackTrace(); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { StringBuilder sb = new StringBuilder(); DBUtils.dropTable(db,sb , Event.TABLE_NAME); DBUtils.dropTable(db,sb , Lecture.TABLE_NAME); DBUtils.dropTable(db,sb , Speaker.TABLE_NAME); DBUtils.dropTable(db,sb , LECTURE_SPEAKER_PAIT_TABLE); onCreate(db); } }

לא הבנתם כל-כך את החלק של התפעול של ה-database? ובכן… בשביל זה יהיה הפוסט הבא :)

אני רוצה להודות שוב ל- +Ran Nachmany ו- +Amir Lazarovich אשר רוב הקוד שראיתם הוא שלהם ולהפנות אתכם לקוד המקור המלא של AndConLab כאן: https://github.com/RanNachmany/AndconLab

חוצמזה אני אשמח להשתמש בבמה הזו ע"מ להזמין אתכם להצטרף לרשת קבוצות ה-GDG שיש לנו ברחבי הארץ:

הבלוג הזה הופיע במקור בבלוג האישי שלי, כאן – http://royiby.blogspot.com/2013/07/2-android-response-caching-content.html

FacebookTwitterGoogle+EmailPinterestWhatsAppLinkedInשתפו אותי

Royi Benyossef

I have been an Android developer since before the first Android phone existed and proceeded to work on development for Android based tablets and TVs long before they were launched by Google so you might say i'm somewhat Android pioneer whose expertise is in non-standard Android platforms and products which involve Android internals as well as application development. As an avid Open source enthusiast i have been Organizing events as well as blogging, speaking and mentoring on Android related topics since 2010 and it is due to the above that i am a Google developer expert since the program's inception which means that i'm not a Google employee but that Google believes that i know what i'm talking about as well as how to say it. Outside of Android i have been a full stack developer at a projects company where i was developing mobile applications in Android as well as iOS (Objective-c) and also dabbled in server side development in PHP, Jquery and more including a few projects of mobile applications using cross platform push technologies using socket.io, XMPP and other similar protocols as a server-side, i also had other wonderful projects which granted me experience in widgets, live wallpapers, network and battery efficiency and many more exciting topics.

1 Comment

  1. yoavst
    05/10/2013 בשעה 08:54

    מה היתרון של זה על Jsoup שניגש לדף PHP שמפעיל בעצמו את ה-Database?
    הרי זה קצת יותר מאובטח, כי בהנדסה לאחור לא ניתן לגלות את שם המשתמש ל-DB…

    נ.ב – ראיתי שאתה משתמש ב-SQLite, ויש שמה יוזר אחד (אני אישי משתמש ב-Mysql), אז מה היתרונות של זה על השיטה שהצעתי?

להגיב