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

אנסה לכתוב
‫Jetpack Compose היא ערכת הכלים המומלצת לבניית ממשק משתמש ל-Android. איך יוצרים ווידג'טים באמצעות ממשקי API בסגנון Compose

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

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

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

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

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

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

  • עדכון מלא: מתקשרים אל 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.

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

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

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

משך העדכון

ככלל, המערכת מאפשרת ל-broadcast receivers, שפועלים בדרך כלל בשרשור הראשי של האפליקציה, לפעול עד 10 שניות לפני שהיא מחשיבה אותם כלא מגיבים ומפעילה שגיאת Application Not Responding (ANR). כדי להימנע מחסימה של ה-main thread בזמן הטיפול בשידור, משתמשים בשיטה goAsync. אם לוקח יותר זמן לעדכן את הווידג'ט, כדאי לתזמן משימה באמצעות WorkManager.

Caution: Any work you do here blocks further broadcasts until it completes,
so it can slow the receiving of later events.

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

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

כברירת מחדל, שידורים – כולל שידורים שמתבצעים באמצעות 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>