Tạo tiện ích nâng cao

Thử dùng Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách tạo các tiện ích bằng API theo kiểu Compose.

Trang này giải thích các phương pháp nên dùng để tạo một tiện ích nâng cao hơn nhằm mang lại trải nghiệm tốt hơn cho người dùng.

Tối ưu hoá để cập nhật nội dung tiện ích

Việc cập nhật nội dung tiện ích có thể tiêu tốn nhiều tài nguyên tính toán. Để tiết kiệm pin, hãy tối ưu hoá loại, tần suất và thời gian cập nhật.

Các loại nội dung cập nhật tiện ích

Có 3 cách để cập nhật một tiện ích: cập nhật toàn bộ, cập nhật một phần và làm mới dữ liệu (trong trường hợp tiện ích bộ sưu tập). Mỗi phương thức đều có chi phí tính toán và hậu quả khác nhau.

Sau đây mô tả từng loại bản cập nhật và cung cấp các đoạn mã cho từng loại.

  • Cập nhật đầy đủ: gọi AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) để cập nhật đầy đủ tiện ích. Thao tác này sẽ thay thế RemoteViews đã cung cấp trước đó bằng một RemoteViews mới. Đây là bản cập nhật tốn nhiều tài nguyên tính toán nhất.

    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);
  • Cập nhật một phần: gọi AppWidgetManager.partiallyUpdateAppWidget để cập nhật các phần của tiện ích. Thao tác này sẽ hợp nhất RemoteViews mới với RemoteViews đã cung cấp trước đó. Phương thức này sẽ bị bỏ qua nếu một tiện ích không nhận được ít nhất một bản cập nhật đầy đủ thông qua 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);
  • Làm mới dữ liệu của bộ sưu tập: gọi AppWidgetManager.notifyAppWidgetViewDataChanged để làm mất hiệu lực dữ liệu của một khung hiển thị bộ sưu tập trong tiện ích. Thao tác này sẽ kích hoạt RemoteViewsFactory.onDataSetChanged. Trong thời gian chờ đợi, dữ liệu cũ sẽ xuất hiện trong tiện ích. Bạn có thể thực hiện an toàn các tác vụ tốn kém một cách đồng bộ bằng phương thức này.

    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);

Bạn có thể gọi các phương thức này ở bất kỳ vị trí nào trong ứng dụng, miễn là ứng dụng có cùng UID với lớp AppWidgetProvider tương ứng.

Xác định tần suất cập nhật tiện ích

Các tiện ích được cập nhật định kỳ tuỳ thuộc vào giá trị mà bạn cung cấp cho thuộc tính updatePeriodMillis. Tiện ích có thể cập nhật để phản hồi tương tác của người dùng, truyền tin cập nhật hoặc cả hai.

Cập nhật định kỳ

Bạn có thể kiểm soát tần suất cập nhật định kỳ bằng cách chỉ định một giá trị cho AppWidgetProviderInfo.updatePeriodMillis trong tệp XML appwidget-provider. Mỗi lần cập nhật sẽ kích hoạt phương thức AppWidgetProvider.onUpdate(). Đây là nơi bạn có thể đặt mã để cập nhật tiện ích. Tuy nhiên, hãy cân nhắc các lựa chọn thay thế cho việc cập nhật bộ nhận tín hiệu truyền tin được mô tả trong phần sau nếu tiện ích của bạn cần tải dữ liệu không đồng bộ hoặc mất hơn 10 giây để cập nhật, vì sau 10 giây, hệ thống sẽ coi BroadcastReceiver là không phản hồi.

updatePeriodMillis không hỗ trợ các giá trị nhỏ hơn 30 phút. Tuy nhiên, nếu muốn tắt tính năng cập nhật định kỳ, bạn có thể chỉ định 0.

Bạn có thể cho phép người dùng điều chỉnh tần suất cập nhật trong một cấu hình. Ví dụ: họ có thể muốn một mã chứng khoán cập nhật 15 phút một lần hoặc chỉ 4 lần một ngày. Trong trường hợp này, hãy đặt updatePeriodMillis thành 0 và sử dụng WorkManager.

Cập nhật để phản hồi một hoạt động tương tác của người dùng

Sau đây là một số cách nên dùng để cập nhật tiện ích dựa trên hoạt động tương tác của người dùng:

  • Từ một hoạt động của ứng dụng: gọi trực tiếp AppWidgetManager.updateAppWidget để phản hồi một hoạt động tương tác của người dùng, chẳng hạn như một lượt nhấn của người dùng.

  • Từ các hoạt động tương tác từ xa, chẳng hạn như thông báo hoặc tiện ích ứng dụng: tạo một PendingIntent, sau đó cập nhật tiện ích từ Activity, Broadcast hoặc Service được gọi. Bạn có thể chọn mức độ ưu tiên của riêng mình. Ví dụ: nếu chọn một Broadcast cho PendingIntent, bạn có thể chọn thông báo phát trên nền trước để ưu tiên BroadcastReceiver.

Cập nhật để phản hồi một sự kiện phát đi

Ví dụ về một sự kiện truyền tin cần có một tiện ích để cập nhật là khi người dùng chụp ảnh. Trong trường hợp này, bạn muốn cập nhật tiện ích khi phát hiện thấy ảnh mới.

Bạn có thể lên lịch cho một công việc bằng JobScheduler và chỉ định một thông báo truyền tin làm điều kiện kích hoạt bằng phương thức JobInfo.Builder.addTriggerContentUri.

Bạn cũng có thể đăng ký một BroadcastReceiver cho thông báo truyền tin, ví dụ: nghe ACTION_LOCALE_CHANGED. Tuy nhiên, vì thao tác này tiêu tốn tài nguyên thiết bị, nên hãy sử dụng một cách cẩn thận và chỉ nghe nội dung phát sóng cụ thể. Khi giới hạn truyền phát được giới thiệu trong Android 7.0 (API cấp 24) và Android 8.0 (API cấp 26), các ứng dụng không thể đăng ký thông báo truyền phát ngầm trong tệp kê khai của mình, với một số trường hợp ngoại lệ.

Những điều cần cân nhắc khi cập nhật tiện ích từ BroadcastReceiver

Nếu tiện ích được cập nhật từ BroadcastReceiver, bao gồm cả AppWidgetProvider, hãy lưu ý đến những điểm cần cân nhắc sau đây liên quan đến thời lượng và mức độ ưu tiên của một bản cập nhật tiện ích.

Thời lượng của bản cập nhật

Theo nguyên tắc, hệ thống cho phép các broadcast receiver (thường chạy trong luồng chính của ứng dụng) chạy tối đa 10 giây trước khi coi chúng là không phản hồi và kích hoạt lỗi Ứng dụng không phản hồi (ANR). Để tránh chặn luồng chính trong khi xử lý thông báo truyền tin, hãy sử dụng phương thức goAsync. Nếu mất nhiều thời gian hơn để cập nhật tiện ích, hãy cân nhắc lên lịch cho một tác vụ bằng cách sử dụng WorkManager.

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

Hãy xem phần Các yếu tố cần cân nhắc về bảo mật và các phương pháp hay nhất để biết thêm thông tin.

Mức độ ưu tiên của bản cập nhật

Theo mặc định, các thông báo truyền tin (bao gồm cả những thông báo được thực hiện bằng AppWidgetProvider.onUpdate) sẽ chạy dưới dạng quy trình trong nền. Điều này có nghĩa là tài nguyên hệ thống bị quá tải có thể gây ra sự chậm trễ trong việc gọi broadcast receiver. Để ưu tiên thông báo truyền tin, hãy biến thông báo đó thành một quy trình trên nền trước.

Ví dụ: thêm cờ Intent.FLAG_RECEIVER_FOREGROUND vào Intent được truyền đến PendingIntent.getBroadcast khi người dùng nhấn vào một phần nhất định của tiện ích.

Tạo bản xem trước chính xác có chứa các mục động

Hình 1: Bản xem trước tiện ích không hiển thị mục nào trong danh sách.

Phần này giải thích phương pháp được đề xuất để hiển thị nhiều mục trong bản xem trước tiện ích cho một tiện ích có chế độ xem bộ sưu tập, tức là một tiện ích sử dụng ListView, GridView hoặc StackView.

Nếu tiện ích của bạn sử dụng một trong các khung hiển thị này, thì việc tạo bản xem trước có thể mở rộng bằng cách trực tiếp cung cấp bố cục tiện ích thực tế sẽ làm giảm trải nghiệm khi bản xem trước tiện ích không hiển thị mục nào. Điều này xảy ra vì dữ liệu của khung hiển thị bộ sưu tập được đặt linh động trong thời gian chạy và trông tương tự như hình ảnh minh hoạ trong hình 1.

Để bản xem trước của các tiện ích có khung hiển thị tập hợp hiển thị đúng cách trong bộ chọn tiện ích, bạn nên duy trì một tệp bố cục riêng biệt chỉ dành cho bản xem trước. Tệp bố cục riêng biệt này bao gồm bố cục tiện ích thực tế và một khung hiển thị bộ sưu tập phần giữ chỗ với các mục giả. Ví dụ: bạn có thể mô phỏng một ListView bằng cách cung cấp một phần giữ chỗ LinearLayout với một số mục danh sách giả.

Để minh hoạ ví dụ về ListView, hãy bắt đầu bằng một tệp bố cục riêng biệt:

// 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>

Chỉ định tệp bố cục xem trước khi cung cấp thuộc tính previewLayout của siêu dữ liệu AppWidgetProviderInfo. Bạn vẫn chỉ định bố cục tiện ích thực tế cho thuộc tính initialLayout và sử dụng bố cục tiện ích thực tế khi tạo RemoteViews trong thời gian chạy.

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

Các mục phức tạp trong danh sách

Ví dụ trong phần trước cung cấp các mục giả trong danh sách, vì các mục trong danh sách là các đối tượng TextView. Việc cung cấp các mục giả có thể phức tạp hơn nếu các mục đó là bố cục phức tạp.

Hãy xem xét một mục trong danh sách được xác định trong widget_list_item.xml và bao gồm 2 đối tượng 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>

Để cung cấp các mục giả trong danh sách, bạn có thể thêm bố cục nhiều lần, nhưng điều này khiến mỗi mục trong danh sách đều giống hệt nhau. Để cung cấp các mục duy nhất trong danh sách, hãy làm theo các bước sau:

  1. Tạo một nhóm thuộc tính cho các giá trị văn bản:

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. Hãy sử dụng các thuộc tính này để đặt văn bản:

    <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. Tạo bao nhiêu kiểu tuỳ ý cho bản xem trước. Xác định lại các giá trị trong từng kiểu:

    <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. Áp dụng các kiểu cho các mục giả trong bố cục xem trước:

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