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 נותן לך על מנת לבצע זאת בדיוק כמו: Fragmentsmultiple 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 שיש לנו ברחבי הארץ: