建立進階小工具

本頁說明建立更進階小工具,提供更優質的使用者體驗的建議做法。

小工具內容更新最佳化

更新小工具內容的運算費用可能非常高。如要節省耗電量,請將更新類型、頻率和時間最佳化。

小工具更新類型

更新小工具的方式有三種:完整的更新、部分更新;如果是集合小工具,則是資料重新整理。每種類型各有不同的運算費用和影響。

以下說明每種更新類型,並分別提供程式碼片段。

  • 完整更新:呼叫 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);
    

只要應用程式與對應的 AppWidgetProvider 類別使用相同的 UID,即可從應用程式中的任何位置呼叫這些方法。

決定小工具的更新頻率

updatePeriodMillis 屬性提供的值而定,小工具會定期更新。小工具可根據使用者互動和/或廣播更新進行更新。

定期更新

您可以在 appwidget-provider XML 中指定 AppWidgetProviderInfo.updatePeriodMillis 的值,藉此控制定期更新的頻率。每次更新都會觸發 AppWidgetProvider.onUpdate() 方法,您可以在該方法放置用於更新小工具的程式碼。不過,如果您的小工具需要以非同步方式載入資料,或需要超過 10 秒才能更新,請考慮按照下一節所述的廣播接收器更新替代方案,因為 10 秒後系統會將 BroadcastReceiver 視為無回應。

updatePeriodMillis 不支援小於 30 分鐘的值。但如要停用定期更新,則可指定 0。

您可以讓使用者調整設定中的更新頻率。例如,他們可能想讓股票股票行每 15 分鐘更新一次,或每天只更新四次。在本例中,請將 updatePeriodMillis 設為 0,並改用 WorkManager

更新以回應使用者互動

以下提供根據使用者互動來更新小工具的幾種建議方法:

  • 從應用程式的活動:直接呼叫 AppWidgetManager.updateAppWidget,以回應使用者互動 (例如輕觸)。

  • 透過遠端互動 (例如通知或應用程式小工具):建構 PendingIntent,然後從叫用的 ActivityBroadcastService 更新小工具。您可以自行選擇優先順序。舉例來說,如果為 PendingIntent 選取了 Broadcast,就可以選擇前景廣播,將其設為 BroadcastReceiver 的優先順序。

更新以回應廣播活動

例如使用者拍照時,需要更新小工具的廣播事件。在這個範例中,您想要在偵測到新相片時更新小工具。

您可以使用 JobScheduler 為工作排程,並使用 JobInfo.Builder.addTriggerContentUri 方法將廣播指定為觸發條件。

您也可以為廣播註冊 BroadcastReceiver,例如監聽 ACTION_LOCALE_CHANGED。不過,由於這項操作會耗用裝置資源,因此請謹慎使用,並只監聽特定廣播訊息。由於 Android 7.0 (API 級別 24) 和 Android 8.0 (API 級別 26) 的廣播限制問世,應用程式無法在資訊清單中註冊隱含廣播,但有某些例外狀況

從 BroadcastReceiver 更新小工具的注意事項

如果是從 BroadcastReceiver (包括 AppWidgetProvider) 更新小工具,請注意以下有關小工具更新時間長度和優先順序的考量事項。

更新作業持續時間

根據規則,系統會允許廣播接收器 (通常在應用程式主執行緒中執行) 執行最長 10 秒,若廣播接收器無回應,並觸發應用程式無回應 (ANR) 錯誤。如果更新小工具的時間過長,請考慮下列替代方案:

  • 使用 WorkManager 安排工作。

  • 使用 goAsync 方法讓接收器有更多時間。這可讓接收器執行 30 秒。

詳情請參閱「安全性考量和最佳做法」。

更新的優先順序

根據預設,廣播 (包括使用 AppWidgetProvider.onUpdate 製作的廣播訊息) 會以背景程序的形式執行。這表示超載的系統資源可能會導致廣播接收器叫用出現延遲。如要優先處理廣播訊息,請將廣播設為前景程序。

舉例來說,當使用者輕觸小工具的特定部分時,將 Intent.FLAG_RECEIVER_FOREGROUND 標記新增至傳遞至 PendingIntent.getBroadcastIntent

建立包含動態項目的準確預覽

圖 1: 小工具預覽畫面不會顯示任何清單項目。

本節說明針對具有集合檢視畫面的小工具 (也就是使用 ListViewGridViewStackView 的小工具),在小工具預覽畫面中顯示多個項目的建議方法。

如果您的小工具使用上述任一檢視畫面,只要直接提供實際小工具版面配置建立可擴充的預覽畫面,當小工具預覽畫面未顯示任何項目時,使用體驗就會下降。這是因為系統在執行階段以動態方式設定集合檢視畫面資料,且看起來與圖 1 中的圖片類似。

為了讓含有集合檢視的小工具預覽在小工具挑選器中正確顯示,建議您維護專為預覽另外指定的版面配置檔案。這個獨立的版面配置檔案包含實際小工具版面配置,以及含有假項目的預留位置集合檢視畫面。舉例來說,您可以提供預留位置 LinearLayout 並加入多個假清單項目,藉此模擬 ListView

如需 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>

提供 AppWidgetProviderInfo 中繼資料的 previewLayout 屬性時,指定預覽版面配置檔案。您仍然可以為 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>