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

No comments:

Post a Comment