Monday, December 9, 2013

Modern talking - The guide to communicating with a server in Android applications

הפוסט זמין גם בעברית כאן - http://iandroid.co.il/dr-iandroid/archives/14894

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 and i'll start with the first one (I think you find it quite logical). 

For those of you who don't know, all of my posts until now (including this one) or 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.

To get started, I want to make a point on the fact that when you approach a project you have a few choices to make which will dictate the server-client communication state for all times, here are a few of them:

First choice - format.
There are usually two options; JSON and XML and the key differences between them are made very clear by a short example.

Here is some data about 2 events that i helped orgenize last year:

  1.  GDG Herzeliya's April event..
  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>  

I would imagine that most of you switched to reading the XML example to understand what's written there (i.e. human readability is in the pros column for the XML format), however, simply counting the amount of characters needed to represent the same amount of data in the two formats will give you the key advantage of the JSON format, but, you might be asking yourself why would i care how much characters are used?

Well, normally you wouldn't really care but since this post is about mobile server-client communication in which some (if not most) of the time, connectivity is provided by 3G which might be priced by used transportation volume and thus each char might make your application more costly to the end user, i would say you should care, don't you agree?

Right now you're probably wondering if this means that JSON is more efficient than XML, well if you were, the answer is NO. 

Many benchmark tests have shown that in many cases XML is more efficient but fortunately in the case of most mobile application, these states are rare and the benefit from XML is negligible.
You can read about these tests in the following links:
  1.   http://www.edwardawebb.com/tips/xml-json
  2.  http://blog.mudynamics.com/2009/05/01/json-xml-performance/


Second choice - Service, Class או Singleton.

In most applications there is a class which manages the server-client communication and it is usually designed with a subset of these requirements in mind:
  1. Must be accessible from everywhere in the application.
  2. Must perform all of its actions in the background so they will not affect the UI thread.
  3. Must be lean and not use many resources (runtime memory).
  4.  Must be able to commence and cease actions on demand as well as in other predetermined forms. 
these aforementioned requirements figuratively spell out Service which is exactly the approach we took (finally, some code :))).

So first of all, since we are trying to approach a remote server, you must ask permissions from the user (even if the user is you right now).
  <uses-permission android:name="android.permission.INTERNET"/>  

And we need to let the device and application know that we have a service by defining it in the Manifest like so:
  <service android:name="CommunicationService"/>  

And here's the basic form of the service class itself:
 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;  
      }  
 }  

Please notice a few details:
  1.  A function by the name of initApiStringsin which we used in order to sort out the API format is missing and because is post is long enough i decided against adding it but please feel free to fill in any gap by looking at the actual full version of the code here.
  2. The Service is of the START_STICKY form which pretty much states that Android will place it at the end of the queue of components to kill off in order to release resources when in need and it will restart it automatically once the crisis is everted and there are enough resources..
  3. The Service uses a thread because unlike IntentService it might interfere with the smooth operation of the main UI thread.
And now, here's the code for that  Thread which holds all functionality that has to do with; sending the request, receiving the response, and parsing the response from the format we selected (JSON) to POJO (Plain Old Java Object):

 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();  
          }  
           }  
      }  

For the actual parsing we chose the open source library Jackson , although we could have gone with any number of other open source solutions such as; gson, json-simple and more.
All of these libraries are good and each has it's own strong point as you can see on any number benchmarks that were done online but as i said we went with Jackson and.... that's that.
 The code for the parsing could be found here and will require you to download the appropriate .jar file.

And... that's about all for now, my next post to be translated will cover the likes of issues such as; connecting the server responses to the UI smoothly by using response caching in 2 tiers.

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/14894

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

Thursday, November 28, 2013

One code fits all (screens)

Hello,

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 but i wanted its debut will be with a fresh post so here it is, i hope you will like it. 

For those of you who don't know, all of my posts until now (including this one) or 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.

Motivation

Ladies and gentlemen, i'm proud to announce the demise of Android as a mobile phone OS and i am more than proud to announce the birth of Android as an all round OS.
How can i say that?
Simply!
Most of your potential clients have Android running on screens that range from the size of 2" (a smartwatch) all the way to 100" (a T.V. set) and from QHD to 4K.
In the past you could have dealt with this kind of situation in one of three ways:
  1. Ignore the issue (as most of you did).
  2. Create a vast and complicated mechanism to figure out and adjust your layouts dynamically at runtime.
  3. Create different apps per device and use the Google Play to slice up the market.

All of the above had great disadvantages and when Google figured that out (2 years ago) it created a better way, called Fragments, to adjust yourself to any situation with grace, ease and not a lot of coding. 

How does it work?

Android has always "known" what the device's screen size and resolution is but until Honeycomb (3.X, which was made specifically for tablets) it had a limited use "out of the box", the smallest Android unit was the Activity which had a .java implementation and an .xml layout and it symbolized 1 full screen of the device (no matter what size it was).
The concept of Fragments is that it's an implementation until which is smaller or equal to the activity in terms of screen usage and that it has a dynamic manager which can push or pop Fragments dynamically from an Activity so an Activity could display one or more Fragments at the same time while adjusting to any screen with a low memory and runtime overhead and with the minimum amount of development.

Current market state (why are you not using it?!)

Warning: the author of this post is about to use name dropping to make himself look important, please disregard that :)

I recently met Mr. Reto Meier who claimed that developers categorize most tasks into two categories:
  1. "This will take less than a minute"
  2. "This will take forever".
He continued to claim that most people will put off items from category #2 until they will check-off all the items in category #2 but category #1 will never be empty so they will only do an item from #2 when it's absolutely impossible to continue without it.
Sounds familiar?
The only problem with that is that he argued that the real-life definitions of the aforementioned categories are closer to these:  
  1. "This will take roughly 30 minutes".
  2. "This will be longer than 30 minutes".
which would implies that:
     a. forever = more than 30 minutes :)
     b. tasks that seem longer than half an hour to execute will never get done.
Now, given that most tasks that will improve your product in a big way will probably take more than 30 minutes would suggest that a rational developer will do these ASAP rather than putting them off for later, right?
Wrong?
Even if you're reading this while mumbling to yourselves, "what is this non-sense?",at least you're thinking about it so that's good.

Step 0 - What are we starting with?

A classic example of a ListView 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() {
            //TODO: [Ran] handle network failure
            @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);
//  ServerCommunicationManager.getInstance(getApplicationContext()).startSearch("Android", 1);
 }

 ////////////////////////////////
 // Async task that queries the DB in background
 ////////////////////////////////
 private class lecturesLoader extends AsyncTask {
  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();
  }
 }
}
The layout file which is called main_activity.xml looks like this (simple and familiar, right?) :

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".EventsListActivity" >

    <ListView
        android:id="@+id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:cacheColorHint="#000000"
        android:divider="@color/divider_color"
        android:dividerHeight="1dp"
        tools:listitem="@layout/event_list_item" >

    </ListView>

</RelativeLayout>

Step 1 - From Activity to FragmetActivity

Replace this line:
public class MainActivity extends SherlockActivity implements OnItemClickListener
With this line:
public class MainActivity extends SherlockFragmentActivity implements LecturesListFragment.callback{
Which also means that the familiar OnItemClickListener is replaced with the following code:

////////////////////////////
 // Fragment interface
 ///////////////////////////
 @Override
 public void onLectureClicked(long lectureId) {
  if (mTwoPanes) {
   SingleLectureFragment f = new SingleLectureFragment();
   Bundle b = new Bundle();
   b.putLong(SingleLectureFragment.LECTURE_ID, lectureId);
   f.setArguments(b);
   getSupportFragmentManager().beginTransaction().replace(R.id.lecture_details_container, f).commit();
  }
  else {
   Intent i = new Intent(this,SingleLectureActivity.class);
   i.putExtra(SingleLectureActivity.EXTRA_LECTURE_ID, lectureId);
   startActivity(i);
  }
 }

 @Override
 public void fetchLecturesFromServer() {
  Intent i = new Intent(this,CommunicationService.class);
  startService(i);
  mProgressDialog = ProgressDialog.show(this, getString(R.string.progress_dialog_starting_title), getString(R.string.progress_dialog_starting_message));
 }
Please focus at the first method which has a naive implementation of an interesting concept which is that there are some scenarios in which we would like to force a single Fragment display even if there's room and definition of a 2-Fragment display, that if clause distinguishes between the 2 possible scenarios to determine 2 possible courses of action:


  • start a new Activity (same as always).
  • use the FragmentManager to perform an inter-fragment operation (in this case; replace).

  • That implicit freedom is the basis to why i like the concept of Fragments so much.

    Step 2 - Creating the Fragment.

    The aforementioned LectureListFragment looks like this:
    
    public class LecturesListFragment extends SherlockFragment implements OnItemClickListener{
    
     private ListView mList;
     private View mRootView;
     private BroadcastReceiver mUpdateReceiver;
     private callbacks mListener;
    
     public interface callbacks {
      public void onLectureClicked(long lectureId);
      public void fetchLecturesFromServer();
     }
    
     @Override
     public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
    
      mUpdateReceiver = new BroadcastReceiver() {
       //TODO: [Ran] handle network failure
       @Override
       public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equalsIgnoreCase(CommunicationService.RESULTS_ARE_IN)) {
         reloadLecturesFromDb();
        }
       }
      };
     }
    
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
       Bundle savedInstanceState) {
    
      mRootView = inflater.inflate(R.layout.single_list_layout, null);
      mList = (ListView) mRootView.findViewById(R.id.list);
      mList.setOnItemClickListener(this);
    
      reloadLecturesFromDb();
    
      return mRootView;
     }
    
     @Override
     public void onAttach(Activity activity) {
      super.onAttach(activity);
      if (!(activity instanceof callbacks)) {
       throw new IllegalStateException("Activity must implement callback interface in order to use this fragment");
      }
      mListener = (callbacks) activity;
     }
    
     @Override
     public void onPause() {
      super.onPause();
      if (null != mUpdateReceiver) {
       getActivity().unregisterReceiver(mUpdateReceiver);
      }
     }
    
     @Override
     public void onResume() {
      super.onResume();
      if (null != mUpdateReceiver) {
       IntentFilter filter = new IntentFilter();
       filter.addAction(CommunicationService.RESULTS_ARE_IN);
       getActivity().registerReceiver(mUpdateReceiver, filter);
      }
     }
    
     @Override
     public void onItemClick(AdapterView list, View view, int position, long id) {
      if (null != mListener) {
       mListener.onLectureClicked(id);
      }
     }
    
     public void reloadLecturesFromDb() {
      new lecturesLoader().execute((Void) null);
     }
    
     ////////////////////////////////
     // Async task that queries the DB in background
     ////////////////////////////////
     private class lecturesLoader extends AsyncTask {
      private SQLiteDatabase db;
    
      @Override
      protected Cursor doInBackground(Void... params) {
       db = new DatabaseHelper(getActivity().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
        if (null != mListener) {
         mListener.fetchLecturesFromServer();
        }
       }
       else {
        LecturesAdapter adapter = (LecturesAdapter) mList.getAdapter();
        if (null == adapter) {
         adapter = new LecturesAdapter(getActivity().getApplicationContext(), result);
         mList.setAdapter(adapter);
        }
        else {
         adapter.changeCursor(result);
        }
       }
    
       db.close();
      }
     }
    
    }
    
    
    Please notice that the LectureLoader class has migrated from the MainActivity to here and that we have new (and exciting) life-cycle events to use in order to intercept events and modify our behavior accordingly.
    The Fragment's layout looks like this (suspiciously close to the old main_activity.xml):
    
    
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context=".EventsListActivity" >
    
        <ListView
            android:id="@+id/list"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:cacheColorHint="#000000"
            android:divider="@color/divider_color"
            android:dividerHeight="1dp"
            tools:listitem="@layout/event_list_item" >
    
        </ListView>
    
    </RelativeLayout>
    The change is in the main_activity.xml which looks like this by default:
    
    
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:background="@color/LecturesListBackground"
        tools:context=".EventsListActivity" >
    
        <fragment
            android:name="com.gdg.andconlab.ui.LecturesListFragment"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:id="@+id/lectures_fragment"
            />
    
    </RelativeLayout>
    And when the device has a screen with more than 600dp (under the res/layout-sw600dp) will look like this (automatically):
    
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >
    
        <fragment
            android:id="@+id/lectures_fragment"
            android:name="com.gdg.andconlab.ui.LecturesListFragment"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="3" />
    
        <LinearLayout
            android:id="@+id/lecture_details_container"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_weight="2"
            >
    
        </LinearLayout>
    
    </LinearLayout>
    And... that's it, i hope you noticed that the "scary" immigration to Fragments is not that scary at all and that you've understood the enormous benefit that you get from it.

    * This post is also available in Hebrew here: http://iandroid.co.il/dr-iandroid/archives/15644
    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

    Tuesday, August 27, 2013

    6 עצות על איך להיות featured ב-Google Play

    6 עצות על איך להיות featured ב-Google Play

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

    עם יותר מ-1,000,000 אפליקציות ב-Google Play, המשימה של משיכת תשומת הלב של משתמש הטלפון החכם הממוצע נעשית קשה יותר ויותר.
    מהצד השני Google נעשית מודאגת יותר ויותר בשאלה; "איך המשתמש שלנו ימצא את האפליקציות הטובות ביותר בצורה המהירה ביותר בין שלל האפליקציות הקיימות?"


    ע"מ לנסות ולפתור את הבעיה הזו Google החליטה על מנגנון בו אפליקציות יעברו פילטור ע"פ סט של חוקים ואלה העומדות בתנאי הפילטר יכולות לקבל מעמד מיוחד של featured או staff picked.
    הנה ציטוט של Google כפי שמופיע כאן:
    Each week the Google Play editorial staff selects a new set of apps to promote in its popular Featured and Staff Picks collections.The Featured collections highlight the latest and greatest app and game titles available for Android. Category featuring highlights the best and most popular apps in the top categories. Staff Picks collects all recently featured apps and games on Google Play. To better reach tablet users, there’s a special Staff Picks collection that highlights the best apps for Android tablets.
    אז השאלה היחידה היא איך עוברים את המבחן להיות featured... ובכן זו בדיוק הסיבה שבגללה אנחנו כאן!
    הכללים נסובים סביב 6 נושאים עיקריים:

    1. תאימות למערכת ההפעלה:

    בעיקרון Google מבקשת ליצור כללי "כללי משחק" לכל האפליקציות כך שהמשתמשים ידעו איך לנווט באפליקציה שלך מההתחלה וכל חוויית השימוש שלהם תהיה אינטואיטיבית וקלה יותר ולכן נעימה יותר.

    כמה דוגמאות עיקריות כוללות:
    1. אל תשנה או תתערב בפעילות הניווט הבסיסי של Android שהינו: back, home, recents.
    2. השתמש בתבנית הניווט של back ו-up.
    3. על כל הדיאלוגים והפופ-אפים להיות ניתנים לביטול באמצעות כפתור ה-back.

    2. יציבות ועמידות:

    1. אל תתן למצב בו המשתמש לא בטוח מה קורה באותו רגע לקרות, עשה זאת כך שהמשתמש יודע בוודאות סבירה מה יקרה כאשר המשתמש יגע, ילחץ או יחליק את האצבע שלו על כל חלק מהמסך.
    2. נסה לגרום לכך שתהליכי אתחול מסך וטעינת ותוכן תהיה שקופה למשתמש ככל הניתן ובמקרים בו אינך מסוגל לעשות כן, דאג שתהיה הודעה הולמת.
    3. אל תחכה שהשוק יבדוק את האפליקציה שלך - כתוב לעצמך תכנית בדיקות מסודרת ובצע בדיקות רבות על כל מכשיר עליו אתה יכול לשים את ידייך עליו ושאתה מתכנן לתמוך בו כולל שימוש ב-"strict mode" ובשאר הכלים שמערכת ה-Android מעמידה לרשותך.
    4. מצב בו האפליקציה; קופאת, קורסת, נסגרת או מתנהגת בצורה לא צפויה הינו באופן כללי לא טוב :)

    3. עיצוב:

     עיצוב ממשק משתמש ייחודי, צבעוני, חדשני ומעניין זה נפלא אבל זה ממש לא הכל.
    1. שים דגש על עיצוב אינטואיטיבי, כיפי וקליל ע"י שימוש בתבניות רבות כמו:
      1. השתמש באנימציות ע"מ להגיד למשתמש דברים שאינם ברורים כמו אנימציית ה"זכוכית הדבוקה" ב-Nexus vanila launcher כאשר אתה מגיע למסך הבית האחרון מכל צד (אם מתיאור הטקסט לא הבנתם על מה אני מדבר אז כך בדיוק המשתמש שלכם מרגיש תסתכלו על התמונה  :) ).
      2. העדף תמונות על פני תגיות טקסט (עוזר גם ללוקאליזציה) והעדף אייקונים הנראים כמה שיותר כמו משהו אמיתי.
      3. חשוב הרבה על מתן כמה שיותר אפשרויות קסטומיזציה כך שהמשתמשים יוכלו לעשות את האפליקציה שלך לשלהם וכך יקשרו אליה וישתמשו בה יותר.
      4. למד את המשתמשים שלך - השתמש ברשתות חברתיות ע"מ להציע למשתמשים שלך מה שיש סיכוי טוב בירצו בלי שיבקשו.
      5. זכור מה המשתמשים שלך אוהבים - אם המשתמשים שלך ביקשו משהו, אל תגרום להם לבקש זאת פעם שניה.
    2. תמוך בכמה שיותר רזולוציות עם העדפה ברורה לתמיכה ברזולוציות גבוהות של 720P ומעלה.
    3. תמוך בכל גדלי המסך אותם אתה יכול לבדוק! (השתמש בכל מה ש-Android נותן לך על מנת לבצע זאת בדיוק כמו: Fragments, multiple resource folders ועוד...).
    4. כשאתה מעלה את האפליקציה שלך ל-Google Play קח את הזמן והשקע בכל התוכן האפשרי:
      1. צור והעלה את כל התמונות וכל האייקונים בכל הגדלים האפשריים.
      2. תמונות מסך של מסכים נבחרים.
      3. השקע זמן ומחשבה בכתיבת תיאור ממצה, תיאורי וטוב.
      4. דירוג בגרות (maturity rating) הוא חשוב מאוד משפטית ולכן יש להשקיע בו מחשבה רבה.

    4. הוגנות והגינות:

    1. יש מספר פעולות שמהוות "דגל אדום" אל מול בוחני והתוכן והעורכים של Google Play והצעתי היא להתחמק מהן בכל מחיר, המכנה המשותף של פעולות אלה הן פעולות שנתפשות ע"י Google כתחמון של משתמשי הקצה ע"מ לקבל משהו שלא כפי שהם התכוונו אליו, רשימה של דוגמאות יכולה להיות:
      1. בקשה עבור permission שאין צורך בה עבור הפעולה המוצהרת של האפליקציה (גם אם בפועל אתה לא עושה איתה דבר).
      2. הורדה והתקנה של אפליקציות אחרות מתוך האפליקציה שלך.
      3. הצעה של "טובות הנאה" כמו badges או משהו דומה עבור מתן 5 כוכבים או תגובה תומכת באפליקציה.
      4. אי תאימות בין ה-maturity rating והתוכן של האפליקציה.
      5. ביצוע כל פעולה ללא רשות מהמשתמש (בעזרת permissions או בקשה ישירה מהמשתמש) וללא איזכור הפעולה בתיאור האפליקציה ב-Google Play.

    5. כבוד למשתמש וידיעת מקום המכשיר בחיים האמיתיים:

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

    6. עשה שיעורי בית!

    מוכשר ככל שאהיה, אני לא יכול לכסות את כל המידע שקיים בכל הדפים, מדריכים, רשימות כללים ובלוג פוסטים אותם איזכרתי בפוסט זה ובוודאי ובוודאי לא אוכל לכסות את הר המידע הקיים בנושא זה ברשת אז עליכם להיות מעודכנים ברמה סבירה ולחפש מידע חדש שיוצא בנושא, נקודה טובה להתחיל היא מכאן:
    ככל שתתעניינו ותקראו יותר כל תהיה לכם אפליקציה מוצלחת יותר באופן כללי וכתוצר לוואי תוכלו להגיע לתשומת ליבם של עורכי ה-Google Play ולהיות featured.
    כרגיל אני אשמח להשתמש בבמה הזו ע"מ להזמין אתכם להצטרף לרשת קבוצות ה-GDG שיש לנו ברחבי הארץ:

     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



    Saturday, August 3, 2013

    ContentProviders - מוסברים.

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

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

    מה?

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

    ובכן התשובה לכך היא נקודה שהעברתי גם בהרצאה והיא שלמרות ש-ContentProviders הינם איתנו החל מרמת API 1 אני נתקל שוב ושוב במקרים בהם מפתחים:
    • לא יודעים מה זה ContentProviders.
    • לא משתמשים נכון ב-ContentProviders.
    • לא משתמשים מספיק ב-ContentProviders.
    • מפחדים להשתמש ב-ContentProviders.
    ואת זה אני פה כדי לשנות, אז בואו נתחיל.

    למה?

    המוטיבציה להשתמש בכלי הזה יכולה להפרט ל-8 סעיפים:
    1. שמירה של מידע בצורה א-נדיפה.
    2. תמיכה בשמירה של כמות מידע גדולה בתקורה נמוכה יחסית.
    3. אינדוקס וחיפוש מהיר במידע השמור. 
    4. גישה אפשרית מתהליכים חיצוניים (Services, Applications, Activities).
    5. אי-תלות בגישה לרשת.
    6. יכולת הקמה קצרה יחסית במעט ידע וניסיון.
    7. גמישות הנובעת בשמירה של כמעט כל סוג מידע במנגנון אחד.
    8. תמיכה של הפלטפורמה.
    שימו לב שתתי קבוצות של 8 הסעיפים הללו יכולות להגדיר מספר רב של מנגנונים בהם משתמשים רבים מן המפתחים, כך למשל:
    • SharedPreferences - עונה על 1, 4 ,5, 8 אבל הן אינן נותנות את הגמישות, ויכולת החיפוש במידע השמור.
    • תבניות פיתוח של memory caching יענו בצורה נפלאה על 2, 3, 6, 7 אבל לא יתנו מענה בשימוש ללא רשת.
    • Cloud storage יכול להיות פתרון נפלא אבל גם הוא לא יעבוד ללא אינטרנט.
    • ספריות ORM צד שלישי כמו - greenDAO או ORMlite למעשה כל הסעיפים מלבד # ואולי 6 אך עדכון תמידי של הגרסאות הוא כורח המציאות ויש אף אפשרות ששינויים עתידיים במערכת ההפעלה ישברו את הפונקציונליות כלל מטעמים של Security.
     אבל רק ContentProviders מספקים את כל 8 הסעיפים (למיטב ידיעתי). 

    איך?

    בואו נתחיל ממצב בוא יש לכם אפליקציה שמודל הנתונים שלה מכיל אוביקט מסוג item שנראה כך:

     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. 
    

    מה שיש לנו כרגע הם השדות שמרכיבים את האובייקט ו- getters & setters.
    מה שצריך להוסיף זה:

    •  תמיכה בהמרה של המידע לפורמט סריאלי כך שניתן לשרשר אותו ולכן ה-Event יצטרך לממש את הממשק       Serializable. 
    • הגדרה של העמודות בטבלת הנתונים שלנו שתשמר ב-sqlite.
    התוצאה תראה כך:
     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  
       //////////////////////////////////////////  
    

    השלב הבא הוא להגדיר את הבנאי של טבלת ה-sqlite שלכם ומה החוקים שלה, זה נעשה ע"י יצירת extension            ל-SQLiteOpenHelper ודריסת כל המתודות שתרצו שיתנהגו בצורה מיוחדת, וזה יראה כך:
     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);  
          }  
     }  
    

    שימו לב למספר דברים:

    1. ב-OnCreate של ה-sqlite אנחנו יוצרים טבלה ייחודית ל-Event כאשר העמודות הן מה שהגדרנו מקודם באובייקט מסוג Event.
    2. בכל upgrade של ה-sqlite אנחנו מוחקים את הטבלה ומתחילים מאפס, זה פתרון נאיבי ויש יותר טובים אבל בשביל ההתחלה הוא טוב מספיק.
    3. שם ה-sqlite (כמו כל השאר) נקבע על ידינו פה בשורה הזו: 

    public static final String DB_NAME = "db"; 

    כעת, אחרי שיש לנו אובייקט לשים בטבלה והגדרה של מסד נתונים (sqlite) ושל טבלה, הגיע הזמן לממש את פעולות ה-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;  
          }  
     {  
    
    כמו שניתן לראות אנחנו מימשנו כמה פעולות:
    1. יצירת טבלה.
    2. מחיקת טבלה.
    3. שמירת רשימה של אובייקטים מסוג Event בטבלה של sqlite.
    4. מציאת הסמן של מיקום בטבלה (הסמן יכיל את מספר הזהות של האובייקט עליו הוא מצביע, כמו גם את שמו, תיאורו וה-URL לתמונה אותה הוא מחזיק) בשינוי קל מאוד נשתמש במתודה הזו ע"מ לחפש בטבלאות ה-sqlite ע"פ כל אחד מן העמודות בטבלה.
    ו...זהו למעשה סיימנו להגדיר את ה-ContentProvider שלנו על כל חלקיו ויש לנו מנגנון גמיש ויעיל לשמירת המידע ולחיפוש בו, דוגמת שימוש תוכלו למצוא בפוסטים קודמים או בקוד המלא שקישור אליו מופיע למטה.

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

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

    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