สร้างวิดเจ็ตขั้นสูง

ลองใช้ Compose
Jetpack Compose เป็นชุดเครื่องมือ UI ที่แนะนำสำหรับ Android ดูวิธีสร้างวิดเจ็ตโดยใช้ API รูปแบบ Compose

หน้านี้อธิบายแนวทางปฏิบัติที่แนะนำในการสร้างวิดเจ็ตขั้นสูงยิ่งขึ้นเพื่อ ประสบการณ์ของผู้ใช้ที่ดียิ่งขึ้น

การเพิ่มประสิทธิภาพสำหรับการอัปเดตเนื้อหาวิดเจ็ต

การอัปเดตเนื้อหาวิดเจ็ตอาจใช้การคำนวณสูง เพิ่มประสิทธิภาพประเภท ความถี่ และเวลาในการอัปเดตเพื่อประหยัดแบตเตอรี่

ประเภทการอัปเดตวิดเจ็ต

การอัปเดตวิดเจ็ตมี 3 วิธี ได้แก่ การอัปเดตทั้งหมด การอัปเดตบางส่วน และการรีเฟรชข้อมูล (ในกรณีของวิดเจ็ตคอลเล็กชัน) แต่ละวิธีมี ต้นทุนการคำนวณและผลที่ตามมาแตกต่างกัน

ต่อไปนี้จะอธิบายการอัปเดตแต่ละประเภทและแสดงข้อมูลโค้ดสำหรับแต่ละประเภท

  • อัปเดตทั้งหมด: โทรหา 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 ที่ระบุไว้ก่อนหน้านี้ ระบบจะละเว้นวิธีนี้หากวิดเจ็ต ไม่ได้รับการอัปเดตอย่างน้อย 1 ครั้งผ่าน 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 วิดเจ็ตสามารถอัปเดตเพื่อตอบสนองต่อการโต้ตอบของผู้ใช้ ออกอากาศ การอัปเดต หรือทั้ง 2 อย่าง

อัปเดตเป็นระยะๆ

คุณควบคุมความถี่ของการอัปเดตเป็นระยะได้โดยระบุค่าสำหรับ AppWidgetProviderInfo.updatePeriodMillis ใน XML ของ appwidget-provider การอัปเดตแต่ละครั้งจะทริกเกอร์เมธอด AppWidgetProvider.onUpdate() ซึ่งเป็นตำแหน่งที่คุณวางโค้ดเพื่ออัปเดตวิดเจ็ตได้ อย่างไรก็ตาม โปรดพิจารณาทางเลือกสำหรับ การอัปเดต Broadcast Receiver ที่อธิบายไว้ใน ส่วนต่อไปนี้ หากวิดเจ็ตต้องโหลดข้อมูลแบบไม่พร้อมกันหรือใช้เวลาอัปเดตนานกว่า 10 วินาที เนื่องจากหลังจาก 10 วินาที ระบบจะถือว่า BroadcastReceiver ไม่ตอบสนอง

updatePeriodMillis ไม่รองรับค่าที่น้อยกว่า 30 นาที อย่างไรก็ตาม หาก ต้องการปิดใช้การอัปเดตเป็นระยะ คุณสามารถระบุ 0 ได้

คุณอนุญาตให้ผู้ใช้ปรับความถี่ของการอัปเดตในการกำหนดค่าได้ เช่น อาจต้องการให้แถบเลื่อนหุ้นอัปเดตทุก 15 นาทีหรือวันละ 4 ครั้งเท่านั้น ในกรณีนี้ ให้ตั้งค่า 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) แอปจะลงทะเบียนการออกอากาศโดยนัยในไฟล์ Manifest ไม่ได้ โดยมีข้อยกเว้นบางประการ

ข้อควรพิจารณาเมื่ออัปเดตวิดเจ็ตจาก BroadcastReceiver

หากวิดเจ็ตได้รับการอัปเดตจาก BroadcastReceiver ซึ่งรวมถึง AppWidgetProvider โปรดคำนึงถึงข้อควรพิจารณาต่อไปนี้เกี่ยวกับ ระยะเวลาและความสำคัญของการอัปเดตวิดเจ็ต

ระยะเวลาการอัปเดต

ตามกฎแล้ว ระบบจะอนุญาตให้ตัวรับสัญญาณออกอากาศซึ่งมักทํางานในเทรดหลักของแอปทํางานได้นานสูงสุด 10 วินาทีก่อนที่จะถือว่าไม่ตอบสนองและทําให้เกิดข้อผิดพลาด Application Not Responding (ANR) หากไม่ต้องการบล็อกเทรดหลักขณะจัดการการออกอากาศ ให้ใช้วิธี 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 จะทำงานเป็นกระบวนการเบื้องหลัง ซึ่งหมายความว่า ทรัพยากรของระบบที่โอเวอร์โหลดอาจทำให้การเรียกใช้ Broadcast Receiver ล่าช้า หากต้องการให้ความสำคัญกับการออกอากาศ ให้เปลี่ยนเป็นกระบวนการที่ทำงานอยู่เบื้องหน้า

เช่น เพิ่มแฟล็ก Intent.FLAG_RECEIVER_FOREGROUND ลงใน Intent ที่ส่งไปยัง PendingIntent.getBroadcast เมื่อผู้ใช้แตะส่วนใดส่วนหนึ่งของวิดเจ็ต

สร้างตัวอย่างที่ถูกต้องซึ่งมีรายการแบบไดนามิก

รูปที่ 1: ตัวอย่างวิดเจ็ตที่แสดงว่าไม่มีรายการ

ส่วนนี้อธิบายแนวทางที่แนะนำสำหรับการแสดงหลายรายการในตัวอย่างวิดเจ็ตสำหรับวิดเจ็ตที่มีมุมมองคอลเล็กชัน ซึ่งก็คือวิดเจ็ตที่ใช้ ListView, GridView หรือ StackView

หากวิดเจ็ตใช้มุมมองใดมุมมองหนึ่งเหล่านี้ การสร้างตัวอย่างที่ปรับขนาดได้โดยระบุเลย์เอาต์วิดเจ็ตจริง โดยตรงจะทำให้ ประสบการณ์การใช้งานแย่ลงเมื่อตัวอย่างวิดเจ็ตไม่แสดงรายการใดๆ ซึ่งเป็นเพราะมีการตั้งค่าข้อมูลของมุมมองคอลเล็กชันแบบไดนามิกในขณะรันไทม์ และมีลักษณะคล้ายกับ รูปภาพที่แสดงในรูปที่ 1

เราขอแนะนำให้ดูแลไฟล์เลย์เอาต์แยกต่างหากที่กำหนดไว้สำหรับตัวอย่างเท่านั้น เพื่อให้ตัวอย่างวิดเจ็ตที่มีมุมมองคอลเล็กชันแสดงอย่างถูกต้องในเครื่องมือเลือกวิดเจ็ต ไฟล์เลย์เอาต์แยกนี้มีเลย์เอาต์วิดเจ็ตจริงและ มุมมองคอลเล็กชันตัวยึดตำแหน่งที่มีรายการปลอม เช่น คุณสามารถจำลอง ListViewได้โดยระบุตัวยึดตำแหน่ง 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 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="@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>