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

Monday, July 15, 2013

תקשורת עם השרת - איך לעשות זאת נכון (Android, JSON, RESTful).

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

את הבלוג הראשון אני רוצה להקדיש לנושא עבורו יש המון מידע ברשת אבל רובו המוחלט חלקי, מיושן או פשוט לא נכון ולכן כאן אני ארצה להסתכל על הנושא בצורה הרחבה ביותר שאפשר תוך כדי שאני אמנה את האפשרויות הגלומות ואסביר את ההיגיון מאחורי כמה שיותר מהבחירות שלי.


בחירה ראשונה - פורמט.
כאשר ניגשים לבחירת פורמט יש בדרך כלל 2 אפשרויות סטנדרטיות עיקריות; JSON, XML, וההבדלים ביניהם נעשים ברורים ביותר אחרי דוגמא קטנה.

בדוגמא זו אני מצרין מידע (לא שלם) עבור 2 אירועים שעזרתי בארגונם השנה:

  1.  אירוע אפריל של GDG הרצליה.
  2. Google I/O 2013 reloaded.

JSON:
   {  
    "events": [  
     {  
      "name": "Herzeliya GDG April",  
      "lectures": [
            {
               title: "Google play services",
               speaker: "Royi Benyossef"
            },
            {
               title: "MVC in Android",
               speaker: "Mr. Ruslan Novikov"
            }
     },  
     {  
      "name": "Google I/O 2013 reloaded",  
      "lectures": [
            {
               title: "Google play services #2",
               speaker: "Royi Benyossef"
            },
            {
               title: "The pro-tip trilogy",
               speaker: "Ran Nachmany "
            }
     }  
    ]  
   }  
XML:

 <events>  
    <event>  
     <name>Herzliya GDG April</name>  
     <lectures>  
       <lecture>  
         <title>Google play services</title>  
         <speaker>Royi Benyossef</speaker>  
          <title>MVC in Android</title>  
         <speaker>Mr. Ruslan Novikov</speaker>  
       </lecture>  
     </lectures>  
    </event>  
    <event>  
     <name>Google I/O 2013 reloaded</name>  
     <lectures>  
       <lecture>  
         <title>Google play services #2</title>  
         <speaker>Royi Benyossef</speaker>  
          <title>The pro-tip trilogy</title>  
         <speaker>Ran Nachmany</speaker>  
       </lecture>  
     </lectures>  
    </event>  
   </events>  

תספרו כמה תווים לקח להצרין את אותו מידע ב-2 השיטות ותראו שב-XML זה לקח הרבה יותר, לעומת זאת אני מניח שלרובכם היה קל יותר להבין מה המידע המוצרן מדוגמת ה-XML ורק אחרי שקראתם אותה הבנתם את דוגמת ה-JSON.


מה אכפת לי כמה תווים לוקח המידע?

ובכן, מכיוון שאנחנו עוסקים בתקשורת שרת-קליינט באפליקציה זה חשוב מאוד, בניגוד למדיות אחרות, חלק גדול מן התקשורת ל-mobile היא ע"ג חיבורי 3G בו הלקוח משלם על תעבורה, יותר תווים לכל קריאה אומר שהאפליקציה שלכם יכולה להיות יקרה הרבה יותר למשתמש.


האם זה אומר כי JSON בהכרח הינו יותר יעיל בעבודה מול שרת?

התשובה היא לא! במקרים רבים מבחני benchmark הראו כי XML טוב יותר אך אלה היו מקרים אליהם רוב אפליקציות ה-mobile לא מגיעות בדרך כלל (אפשר לקרוא על כך יותר כאן: http://www.edwardawebb.com/tips/xml-json
או כאן: http://blog.mudynamics.com/2009/05/01/json-xml-performance/)


בחירה שניה - Service, Class או Singleton.

ברוב האפליקציות אותן אני מכיר יש מספר דרישות מ-Class שמנהל ומטפל בתקשורת אפליקציה-שרת:
  1. שיהיה זמין תמיד מכל מקום באפליקציה שצריך אותו.
  2. שיעשה את פעולתו ברקע כך שלא יפריע לפעולת ה-UI של האפליקציה.
  3. שלא ידרוש משאבים מרובים.
  4. שיוכל לסיים או להתחיל פעולות גם לא בזמן דרישה (by demand) אלא ע"פ חוקיות אחרת נדרשת.
כל הדרישות הללו או אפילו תת קבוצה שלהם למעשה דורש שימוש ב-Service וזה אכן מה שעשינו (סוף סוף קצת קוד :)):

אז קודם כל, מכיוון שאנחנו צריכים לגשת לאינטרנט, אסור לשכוח לבקש רשות מהמשתמש (גם אם הוא אתם כרגע):
  <uses-permission android:name="android.permission.INTERNET"/>  

והנה ההגדרה של ה-Service ב-Manifest:
  <service android:name="CommunicationService"/>  

והגרסא הבסיסית של ה-Service:
 public class CommunicationService extends Service{  
      public static final String RESULTS_ARE_IN = "RESULTS_ARE_IN";  
      private Thread mWorker;  
   private HttpClient mHttpClient;      
      private ResponseHandler<String> mResponseHandler;  
      @Override  
      public void onCreate() {  
           super.onCreate();  
           mHttpClient = new DefaultHttpClient();  
     HttpParams params = mHttpClient.getParams();  
     int serverConnectionTimeout = getResources().getInteger(R.integer.http_connection_timeout_in_seconds) * 1000;  
     HttpConnectionParams.setConnectionTimeout(params, serverConnectionTimeout);  
     HttpConnectionParams.setSoTimeout(params, serverConnectionTimeout);  
           mResponseHandler = new BasicResponseHandler();  
           initApiStrings();  
      }  
      @Override  
      public IBinder onBind(Intent intent) {  
           return null;  
      }  
      @Override  
      public int onStartCommand(Intent intent, int flags, int startId) {  
           if (null != mWorker && mWorker.isAlive()) {  
                //we are already running a transaction, nothing to do here  
                return Service.START_STICKY;  
           }  
           //start new job  
           mWorker = new Thread(new TransactionJob());  
           mWorker.start();  
           return Service.START_STICKY;  
      }  
 }  

שימו לב לכמה פרטים:


  1. חסרה לכם מימוש של פונקציה בשם initApiStrings בה השתמשנו ע"מ לסדר את הפורמט של קריאות ה-API בהן השתמשנו אך מכיוון שהפוסט הזה עמוס גם כך החלטתי להשמיט אותה אך אתם בהחלט מוזמנים להשלים אותה או כל חוסר אחר בעזרת הקוד המלא ששחררנו כאן.
  2. ה-Service הינו מסוג START_STICKY מה שפחות או יותר אומר שאם Android יהרוג את ה-Service הנ"ל מכל סיבה אזי הוא גם ינסה לאתחל אותו מחדש ברגע שישתחררו המשאבים לכך.
  3. ה-Service משתמש ב-Thread וזאת משום ש-Service (בניגוד ל-IntentService) רץ על התהליך הראשי של האפליקציה ולכן עלול להפריע לפעילות התקינה של ה-UI.
והנה המימוש של ה-Thread המדובר:

 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) {  
             //fire an intent  
             Intent intent = new Intent();  
                intent.setAction(RESULTS_ARE_IN);  
                CommunicationService.this.sendBroadcast(intent);  
           }  
                  }  
          } catch (Exception e) {  
               e.printStackTrace();  
          }  
           }  
      }  

ה-Thread הנ"ל מחזיק את הפונקציונליות הקשורה לשליחת הבקשה, קבלת התגובה מהשרת והפרסור של התגובה מן הפורמט שבחרנו (JSON) ל-POJO או Plain Old Java Object.
למען הפרסור אנחנו בחרנו להשתמש ב-Jackson שהינה ספריית קוד פתוח הבנויה לשם כך, ישנן עוד מספר אפשרויות בהן יכולנו לבחור כמו gsonjson-simple ועוד, שכולן ספריות טובות שמבחני benchmark כאלה ואחרים טוענים שהן טובות יותר במצבים כאלה ואחרים אבל... אנחנו בחרנו ב-Jackson ו...זה מה יש!

הקוד של הפרסור עצמו נמצא פה וידרוש מכם להוריד גם את ה-.jar המתאים.

ו... זהו. אני חושב שעשינו הרבה לפוסט אמיתי ראשון ואני מקווה שנכסה את כל השאר בפוסטים הבאים :)

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

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

הבלוג פורסם גם כאן - http://iandroid.co.il/dr-iandroid/archives/14894

Monday, July 8, 2013

פוסט ראשון! מקווה שאחד מני רבים.

שלום לכולם,
עבר הרבה זמן מאז שכתבתי בלוג וזו פעם ראשונה שאני מנסה זאת בבלוג אישי משלי אז אני מקווה שתהנו.

מטרת הבלוג:
ליצור תוכן איכותי של תכנון ותכנות Android בעברית תוך שימוש בהרצאות וב-live labs שעמיתי ואני יצרנו (לא אשכח להודות להם אישית בתחילת וסוף כל פוסט).

מי אני? (או יותר נכון - למה שתקשיבו לי?)
שמי רועי בן יוסף, אני מפתח mobile מזה כ-3 שנים עם התמחות בפרויקטים על פלטפורמות א-סטנדרטיות, כמו כן אני מנהל קבוצות חברתיות ומעביר הרצאות ואירועים אחרים ב-Android מזה 3 שנים, בשנה האחרונה היה לי הכבוד להבחר כ-GDE ב-Android בישראל לשנת 2013 ולשמש כמנטור בתכנית של campusTLV ואני עובד כראש צוות פיתוח Android STB בחברת סטארט-אפ ישראלית בשם Vidmind.

למה בעברית?
כי רוב הנושאים שאותם אסקר פה כתובים באנגלית ובשפות רבות אחרות ולמרות זאת מעולם לא ראיתי אותם כתובים בעברית כמו שאני רוצה ובנוסף באירועי קהילה אני רואה יותר ויותר מקרים של מתכנתים שמפספסים מידע חשוב כי הם אינם מרגישים בנח לקרוא טקסטים ארוכים באנגלית ואת זה אני מעוניין לשנות.

אז מה הסידור?
אני מקווה להעלות פוסט פעם בשבוע - שבועיים ולנסות לענות לכל השאלות שיכולות להגיע מאזור התגובות ו...זהו בערך.