Creare un widget avanzato

Questa pagina illustra le best practice per creare un widget più avanzato per un'esperienza utente migliore.

Ottimizzazioni per l'aggiornamento dei contenuti dei widget

L'aggiornamento dei contenuti dei widget può essere computazionalmente costoso. Per risparmiare batteria, ottimizza il tipo, la frequenza e la tempistica degli aggiornamenti.

Tipi di aggiornamenti dei widget

Esistono tre modi per aggiornare un widget: un aggiornamento completo, un aggiornamento parziale e, nel caso di un widget della raccolta, un aggiornamento dei dati. Ognuno ha costi e ramificazioni computazionali diversi.

Di seguito viene descritto ogni tipo di aggiornamento e vengono forniti snippet di codice per ciascuno.

  • Aggiornamento completo: chiama AppWidgetManager.updateAppWidget(int, android.widget.RemoteViews) per aggiornare completamente il widget. Il valore fornito in precedenza RemoteViews verrà sostituito da un nuovo RemoteViews. Si tratta dell'aggiornamento più costoso in termini di risorse di calcolo.

    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);
  • Aggiornamento parziale: chiama AppWidgetManager.partiallyUpdateAppWidget per aggiornare parti del widget. Il nuovo RemoteViews viene unito alRemoteViews fornito in precedenza. Questo metodo viene ignorato se un widget non riceve almeno un aggiornamento completo tramite 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);
  • Aggiornamento dei dati della raccolta: chiama AppWidgetManager.notifyAppWidgetViewDataChanged per invalidare i dati di una visualizzazione della raccolta nel widget. Questo attiva RemoteViewsFactory.onDataSetChanged. Nel frattempo, i vecchi dati vengono visualizzati nel widget. Con questo metodo puoi eseguire in modo sicuro attività complesse in modo sincrono.

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

Puoi chiamare questi metodi da qualsiasi punto dell'app, a condizione che l'app abbia lo stesso UID della classe AppWidgetProvider corrispondente.

Determinare la frequenza di aggiornamento di un widget

I widget vengono aggiornati periodicamente in base al valore fornito per l'attributo updatePeriodMillis. Il widget può aggiornarsi in risposta all'interazione dell'utente, trasmettere aggiornamenti o entrambe le cose.

Aggiorna periodicamente

Puoi controllare la frequenza dell'aggiornamento periodico specificando un valore per AppWidgetProviderInfo.updatePeriodMillis nel file XML appwidget-provider. Ogni aggiornamento attiva il metodo AppWidgetProvider.onUpdate(), dove puoi inserire il codice per aggiornare il widget. Tuttavia, valuta le alternative per gli aggiornamenti dei ricevitori di trasmissione descritte nella sezione successiva se il widget deve caricare i dati in modo asincrono o se l'aggiornamento richiede più di 10 secondi, perché dopo 10 secondi il sistema considera un BroadcastReceiver non rispondente.

updatePeriodMillis non supporta valori inferiori a 30 minuti. Tuttavia, se vuoi disattivare gli aggiornamenti periodici, puoi specificare 0.

Puoi consentire agli utenti di regolare la frequenza degli aggiornamenti in una configurazione. Ad esempio, potrebbe volere che un ticker azionario venga aggiornato ogni 15 minuti o solo quattro volte al giorno. In questo caso, imposta updatePeriodMillis su 0 e utilizza WorkManager.

Aggiornamento in risposta a un'interazione dell'utente

Ecco alcuni modi consigliati per aggiornare il widget in base all'interazione dell'utente:

  • Da un'attività dell'app: chiama direttamenteAppWidgetManager.updateAppWidget in risposta a un'interazione dell'utente, ad esempio un tocco.

  • Da interazioni remote, ad esempio una notifica o un widget dell'app: crea un PendingIntent, quindi aggiorna il widget dall'Activity, Broadcast o Service invocato. Puoi scegliere la tua priorità. Ad esempio, se selezioni un Broadcast per il PendingIntent, puoi scegliere una trasmissione in primo piano per dare priorità al BroadcastReceiver.

Aggiornamento in risposta a un evento di trasmissione

Un esempio di evento di trasmissione che richiede l'aggiornamento di un widget è quando l'utente scatta una foto. In questo caso, vuoi aggiornare il widget quando viene rilevata una nuova foto.

Puoi pianificare un job con JobScheduler e specificare una trasmissione come attivatore utilizzando il metodo JobInfo.Builder.addTriggerContentUri.

Puoi anche registrare un BroadcastReceiver per la trasmissione, ad esempio ascoltare ACTION_LOCALE_CHANGED. Tuttavia, poiché questa operazione consuma le risorse del dispositivo, utilizzala con cautela e ascolta solo la trasmissione specifica. Con l'introduzione delle limitazioni per le trasmissioni in Android 7.0 (livello API 24) e Android 8.0 (livello API 26), le app non possono registrare trasmissioni implicite nei manifest, con alcune eccezioni.

Considerazioni sull'aggiornamento di un widget da un BroadcastReceiver

Se il widget viene aggiornato da un BroadcastReceiver, tra cui AppWidgetProvider, tieni presente le seguenti considerazioni relative alla durata e alla priorità di un aggiornamento del widget.

Durata dell'aggiornamento

Di norma, il sistema consente ai ricevitori di trasmissione, che in genere vengono eseguiti nel thread principale dell'app, di funzionare per un massimo di 10 secondi prima di considerarli non rispondenti e di attivare un errore Application Not Responding (ANR). Se l'aggiornamento del widget richiede più tempo, valuta le seguenti alternative:

  • Pianifica un'attività utilizzando WorkManager.

  • Concedi più tempo al destinatario con il metodo goAsync. In questo modo, i ricevitori vengono eseguiti per 30 secondi.

Per ulteriori informazioni, consulta Considerazioni e best practice per la sicurezza.

Priorità dell'aggiornamento

Per impostazione predefinita, le trasmissioni, incluse quelle realizzate utilizzando AppWidgetProvider.onUpdate, vengono eseguite come processi in background. Ciò significa che le risorse di sistema sovraccaricate possono causare un ritardo nell'invocazione del ricevitore di trasmissione. Per dare la priorità alla trasmissione, impostala come processo in primo piano.

Ad esempio, aggiungi il flag Intent.FLAG_RECEIVER_FOREGROUND al Intent passato al PendingIntent.getBroadcast quando l'utente tocca una determinata parte del widget.

Creare anteprime accurate che includono elementi dinamici

Figura 1: un'anteprima del widget che non mostra elementi dell'elenco.

Questa sezione illustra l'approccio consigliato per visualizzare più elementi nell'anteprima di un widget con una visualizzazione della raccolta, ovvero un widget che utilizza ListView, GridView o StackView.

Se il widget utilizza una di queste visualizzazioni, la creazione di un'anteprima scalabile fornendo direttamente il layout del widget peggiora l'esperienza quando l'anteprima del widget non mostra alcun elemento. Questo accade perché i dati delle visualizzazioni delle raccolte vengono impostati in modo dinamico in fase di esecuzione e hanno un aspetto simile all'immagine mostrata nella Figura 1.

Per visualizzare correttamente le anteprime dei widget con visualizzazioni delle raccolte nel selettore di widget, ti consigliamo di mantenere un file di layout separato destinato solo all'anteprima. Questo file di layout separato include il layout effettivo del widget e una vista della raccolta di segnaposto con elementi falsi. Ad esempio, puoi simulare un ListView fornendo un segnaposto LinearLayout con diversi elementi dell'elenco falsi.

Per illustrare un esempio di ListView, inizia con un file di layout separato:

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

Specifica il file di layout dell'anteprima quando fornisci l'attributo previewLayout dei metadati AppWidgetProviderInfo. Devi comunque specificare il layout effettivo del widget per l'attributo initialLayout e utilizzarlo quando crei un RemoteViews in fase di esecuzione.

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

Voci di elenco complesse

L'esempio nella sezione precedente fornisce voci di elenco false, perché le voci di elenco sono oggetti TextView. Può essere più complesso fornire elementi falsi se si tratta di layout complessi.

Prendi in considerazione un elemento dell'elenco definito in widget_list_item.xml e costituito da due oggetti 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>

Per fornire elementi dell'elenco falsi, puoi includere il layout più volte, ma in questo modo ogni elemento dell'elenco sarà identico. Per fornire elementi dell'elenco univoci, segui questi passaggi:

  1. Crea un insieme di attributi per i valori di testo:

    <resources>
        <attr name="widgetTitle" format="string" />
        <attr name="widgetContent" format="string" />
    </resources>
    
  2. Utilizza questi attributi per impostare il testo:

    <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. Crea tutti gli stili necessari per l'anteprima. Ridefinisci i valori in ciascun stile:

    <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. Applica gli stili agli elementi falsi nel layout di anteprima:

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