יצירת ווידג'ט מתקדם

בדף הזה מוסבר איך ליצור ווידג'ט מתקדם יותר כדי לשפר את חוויית המשתמש.

אופטימיזציה לעדכון תוכן הווידג'ט

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

סוגי העדכונים לווידג'טים

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

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

  • עדכון מלא: קוראים ל-AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) כדי לעדכן את הווידג'ט באופן מלא. הפעולה הזו תחליף את RemoteViews שסיפקתם בעבר ב-RemoteViews חדש. זהו העדכון הכי יקר מבחינת משאבי המחשוב.

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout1, "Updated text1")
    setTextViewText(R.id.textview_widget_layout2, "Updated text2")
    }
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews)

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout1, "Updated text1");
    remoteViews.setTextViewText(R.id.textview_widget_layout2, "Updated text2");
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
  • עדכון חלקי: קוראים ל-AppWidgetManager.partiallyUpdateAppWidget כדי לעדכן חלקים מהווידג'ט. הפעולה הזו תמזג את RemoteViews החדש עם הערך הקודם של RemoteViews. המערכת מתעלמת מהשיטה הזו אם הווידג'ט לא מקבל לפחות עדכון מלא אחד דרך updateAppWidget(int[], RemoteViews).

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    val remoteViews = RemoteViews(context.getPackageName(), R.layout.widgetlayout).also {
    setTextViewText(R.id.textview_widget_layout, "Updated text")
    }
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews)

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widgetlayout);
    remoteViews.setTextViewText(R.id.textview_widget_layout, "Updated text");
    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews);
  • רענון נתוני האוסף: קוראים ל-AppWidgetManager.notifyAppWidgetViewDataChanged כדי לבטל את התוקף של הנתונים בתצוגת האוסף בווידג'ט. הפעולה הזו מפעילה את RemoteViewsFactory.onDataSetChanged. בינתיים, הנתונים הישנים יוצגו בווידג'ט. באמצעות השיטה הזו אפשר לבצע משימות יקרות באופן סינכרוני ובבטחה.

    Kotlin

    val appWidgetManager = AppWidgetManager.getInstance(context)
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)

    Java

    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview);

אפשר להפעיל את השיטות האלה מכל מקום באפליקציה, כל עוד לאפליקציה יש את אותו מזהה UID כמו לכיתה המתאימה של AppWidgetProvider.

קביעת התדירות של עדכון ווידג'ט

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

עדכון מדי פעם

כדי לקבוע את תדירות העדכון הקבוע, מציינים ערך בשדה AppWidgetProviderInfo.updatePeriodMillis בקובץ ה-XML של appwidget-provider. כל עדכון מפעיל את השיטה AppWidgetProvider.onUpdate(), שבה אפשר למקם את הקוד לעדכון הווידג'ט. עם זאת, כדאי לשקול את החלופות לעדכונים של מקלט השידור שמתוארות בקטע הבא, אם הווידג'ט שלכם צריך לטעון נתונים באופן אסינכרוני או שהעדכון שלו נמשך יותר מ-10 שניות, כי אחרי 10 שניות המערכת מחשיבת את BroadcastReceiver כלא מגיב.

updatePeriodMillis לא תומך בערכים של פחות מ-30 דקות. עם זאת, אם רוצים להשבית עדכונים תקופתיים, אפשר לציין 0.

אתם יכולים לאפשר למשתמשים לשנות את תדירות העדכונים בהגדרה. לדוגמה, יכול להיות שהם רוצים שהתג של מניות יעודכן כל 15 דקות או רק ארבע פעמים ביום. במקרה כזה, מגדירים את updatePeriodMillis כ-0 ומשתמשים במקום זאת ב-WorkManager.

עדכון בתגובה לאינטראקציה של משתמש

ריכזנו כאן כמה שיטות מומלצות לעדכון הווידג'ט על סמך אינטראקציה של משתמשים:

  • מפעילות של האפליקציה: קוראים ישירות ל-AppWidgetManager.updateAppWidget בתגובה לאינטראקציה של משתמש, כמו הקשה של משתמש.

  • מאינטראקציות מרחוק, כמו התראה או ווידג'ט של אפליקציה: יוצרים PendingIntent ומעדכנים את הווידג'ט מהקריאה ל-Activity, ל-Broadcast או ל-Service. אתם יכולים לבחור את רמת העדיפות הרצויה. לדוגמה, אם בוחרים Broadcast עבור PendingIntent, אפשר לבחור שידור בחזית כדי לתת ל-BroadcastReceiver עדיפות.

עדכון בתגובה לאירוע שידור

דוגמה לאירוע שידור שמחייב עדכון של ווידג'ט היא כשהמשתמש מצלם תמונה. במקרה כזה, רוצים לעדכן את הווידג'ט כשזוהתה תמונה חדשה.

אפשר לתזמן משימה באמצעות JobScheduler ולציין שידור כטריגר באמצעות השיטה JobInfo.Builder.addTriggerContentUri.

אפשר גם לרשום BroadcastReceiver לשידור – לדוגמה, להאזין ל-ACTION_LOCALE_CHANGED. עם זאת, מכיוון שהפעולה הזו צורכת משאבים במכשיר, כדאי להשתמש בה בזהירות ולהאזין רק לשידור הספציפי. בעקבות ההשקה של מגבלות שידור ב-Android 7.0 (רמת API‏ 24) וב-Android 8.0 (רמת API‏ 26), אפליקציות לא יכולות לרשום שידורים משתמעים במניפסטים שלהן, מלבד חריגים מסוימים.

שיקולים בעדכון ווידג'ט מ-BroadcastReceiver

אם הווידג'ט מתעדכן מ-BroadcastReceiver, כולל AppWidgetProvider, חשוב לדעת את השיקולים הבאים לגבי משך הזמן והעדיפות של עדכון הווידג'ט.

משך העדכון

ככלל, המערכת מאפשרת למקלטי שידורים, שבדרך כלל רצים ב-thread הראשי של האפליקציה, לרוץ למשך עד 10 שניות לפני שהם רואים אותם כשאינם מגיבים ומפעילים שגיאת האפליקציה לא מגיבה (ANR). אם הווידג'ט מתעדכן באיטיות, כדאי לנסות את החלופות הבאות:

  • לתזמן משימה באמצעות WorkManager.

  • אפשר לתת למקבל התשלום עוד זמן באמצעות השיטה goAsync. כך המכשירים המקבלים יכולים לבצע את הפעולה במשך 30 שניות.

למידע נוסף, ראו שיקולי אבטחה ושיטות מומלצות.

העדיפות של העדכון

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

לדוגמה, מוסיפים את הדגל Intent.FLAG_RECEIVER_FOREGROUND ל-Intent שמוענק ל-PendingIntent.getBroadcast כשהמשתמש מקייש על חלק מסוים בווידג'ט.

יצירת תצוגות מקדימות מדויקות שכוללות פריטים דינמיים

איור 1: תצוגה מקדימה של ווידג'ט ללא פריטים ברשימה.

בקטע הזה מוסבר על הגישה המומלצת להצגת מספר פריטים בתצוגה המקדימה של ווידג'ט לווידג'ט עם תצוגת אוסף – כלומר, בווידג'ט שנעשה בו שימוש ב-ListView, ב-GridView או ב-StackView.

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

כדי שהתצוגות המקדמות של ווידג'טים עם תצוגות של אוספים יוצגו בצורה תקינה בבורר הווידג'טים, מומלץ לשמור קובץ פריסה נפרד שמיועד רק לתצוגה המקדימה. קובץ הפריסה הנפרד הזה כולל את הפריסה בפועל של הווידג'ט ותצוגת אוסף של placeholder עם פריטים מזויפים. לדוגמה, אפשר לחקות את ListView על ידי הוספת placeholder LinearLayout עם כמה פריטים מזויפים ברשימה.

כדי להמחיש דוגמה לקובץ ListView, מתחילים בקובץ פריסה נפרד:

// res/layout/widget_preview.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:background="@drawable/widget_background"
   android:orientation="vertical">

    // Include the actual widget layout that contains ListView.
    <include
        layout="@layout/widget_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    // The number of fake items you include depends on the values you provide
    // for minHeight or targetCellHeight in the AppWidgetProviderInfo
    // definition.

    <TextView android:text="@string/fake_item1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

    <TextView android:text="@string/fake_item2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="?attr/appWidgetInternalPadding" />

</LinearLayout>

מציינים את קובץ הפריסה של התצוגה המקדימה כשמוסיפים את המאפיין previewLayout של המטא-נתונים AppWidgetProviderInfo. עדיין צריך לציין את הפריסה בפועל של הווידג'ט למאפיין initialLayout, ולהשתמש בפריסה בפועל של הווידג'ט בזמן היצירה של RemoteViews בסביבת זמן הריצה.

<appwidget-provider
    previewLayout="@layout/widget_previe"
    initialLayout="@layout/widget_view" />

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

בדוגמה שבקטע הקודם מוצגים פריטים מזויפים ברשימה, כי פריטים ברשימה הם אובייקטים מסוג TextView. אם הפריטים מעוצבים בפריסה מורכבת, יכול להיות מורכב יותר כדי לספק פריטים מזויפים.

נניח שפריט ברשימה מוגדר ב-widget_list_item.xml וכולל שני אובייקטים מסוג TextView:

<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    <TextView android:id="@id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_title" />

    <TextView android:id="@id/content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/fake_content" />
</LinearLayout>

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

  1. יוצרים קבוצת מאפיינים לערכי הטקסט:

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. משתמשים במאפיינים הבאים כדי להגדיר את הטקסט:

    <LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
        <TextView android:id="@id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetTitle" />
    
        <TextView android:id="@id/content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="?widgetContent" />
    </LinearLayout>
    
  3. יוצרים כמה סגנונות שרוצים לקטע לדוגמה. מגדירים מחדש את הערכים בכל סגנון:

    <resources>
    
        <style name="Theme.Widget.ListItem">
            <item name="widgetTitle"></item>
            <item name="widgetContent"></item>
        </style>
        <style name="Theme.Widget.ListItem.Preview1">
            <item name="widgetTitle">Fake Title 1</item>
            <item name="widgetContent">Fake content 1</item>
        </style>
        <style name="Theme.Widget.ListItem.Preview2">
            <item name="widgetTitle">Fake title 2</item>
            <item name="widgetContent">Fake content 2</item>
        </style>
    
    </resources>
    
  4. מחילים את הסגנונות על הפריטים המזויפים בפריסה של התצוגה המקדימה:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" ...>
    
        <include layout="@layout/widget_view" ... />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview1" />
    
        <include layout="@layout/widget_list_item"
            android:theme="@style/Theme.Widget.ListItem.Preview2" />
    
    </LinearLayout>