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

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

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

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

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

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

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

  • עדכון מלא: קוראים ל-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. עם זאת, מכיוון שהפעולה הזו צורכת משאבים במכשיר, חשוב להפעיל שיקול דעת ולהאזין רק לשידור הספציפי. עם ההשקה של broadcast מגבלות ב-Android 7.0 (רמת API 24) ו-Android 8.0 (רמת API 26), אפליקציות לא יכולות לבצע רישום משתמע משודרים במניפסטים שלהם, חריגים.

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

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

משך העדכון

ככלל, המערכת מאפשרת למקלטי השידור, שפועלים בדרך כלל בשרשור הראשי של האפליקציה, לפעול למשך עד 10 שניות לפני שהיא מתייחסת אליהם כאל רכיבים שלא מגיבים ומפעילה שגיאה מסוג Application Not Responding (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>