Monday, July 22, 2013

תקשורת עם השרת - איך לעשות זאת נכון פרק 2 (Android, response caching, content providers, cursor adapter).

שלום שוב,
למי ששכח, הוקרה קצרה; הבלוג הזה מתבסס לחלוטין על עבודתי המשותפת עם +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://iandroid.co.il/dr-iandroid/archives/14998

No comments:

Post a Comment