Cómo compilar un host de widgets de la app

La pantalla principal de Android que está disponible en la mayoría de los dispositivos Android permite al usuario incorporar widgets de apps para acceder al contenido de forma rápida. Si estás compilando un reemplazo de la pantalla principal o una app similar, también puedes permitir que el usuario incorpore widgets de la app mediante la implementación de un AppWidgetHost. Esto no es algo necesario para la mayoría de las apps, pero, si vas a compilar tu propio host, es importante comprender las obligaciones contractuales que un host acepta de manera implícita.

El enfoque principal de este documento son las responsabilidades relacionadas con la implementación de un AppWidgetHost personalizado. Para ver un ejemplo de cómo implementar un AppWidgetHost, consulta el código fuente del selector de la pantalla principal de Android.

A continuación, se incluye una descripción general de las clases y los conceptos clave relacionados con la implementación de un AppWidgetHost personalizado:

  • Host de widgets de la app: AppWidgetHost proporciona la interacción con el servicio AppWidget para apps, como la pantalla principal, que tienen como objetivo incorporar widgets de apps en la IU. Un AppWidgetHost debe tener un ID único dentro del propio paquete del host. Este ID sigue siendo persistente en todos los usos del host. Por lo general, el ID es un valor hard-coded que asignas en tu app.
  • ID de widgets de la app: A cada instancia de widget de la app se le asigna un ID único en el momento de la vinculación (consulta bindAppWidgetIdIfAllowed(), que se analiza con más detalles en Cómo vincular widgets de la app). El host obtiene el ID único utilizando allocateAppWidgetId(). Este ID es persistente durante toda la vida útil del widget, es decir, hasta que se quita del host. El paquete de hosting debe conservar cualquier estado específico del host (como el tamaño y la ubicación del widget) y asociarlo con el ID del widget de la app.
  • Vista del host de widgets de la app: AppWidgetHostView se puede interpretar como un marco en el que se une el widget cada vez que es necesario que aparezca. Se asigna un widget de la app a un AppWidgetHostView cada vez que el host aumenta el widget.
  • Paquete de opciones: El AppWidgetHost usa el paquete de opciones para enviar información al AppWidgetProvider sobre cómo se muestra el widget (por ejemplo, rango de tamaños y si el widget está en una pantalla de bloqueo o en la pantalla principal). Esta información permite que AppWidgetProvider adapte el contenido y la apariencia del widget en función de cómo y dónde se muestra. Puedes usar updateAppWidgetOptions() y updateAppWidgetSize() para modificar el paquete de widgets de una app. Ambos métodos activan una devolución de llamada a AppWidgetProvider.

Vinculación de widgets de apps

Cuando un usuario agrega un widget de la app a un host, se produce un proceso llamado vinculación. La vinculación consiste en asociar un ID de un widget de la app en particular a un host y a un AppWidgetProvider específicos. Puedes hacer esto de diferentes maneras, según la versión de Android en la que se ejecuta tu app.

Cómo vincular widgets de apps en Android 4.0 y versiones anteriores

En los dispositivos que ejecutan Android 4.0 y versiones anteriores, los usuarios agregan widgets de apps mediante una actividad del sistema que les permite seleccionar un widget. Así, se realiza una comprobación de permisos de manera implícita; es decir, cuando el usuario agrega el widget de la app, otorga implícitamente permiso a tu app para agregar widgets al host. A continuación, se muestra un ejemplo en el que se ilustra este enfoque, tomado del Selector original. En este fragmento, un controlador de eventos invoca a startActivityForResult() con el código de solicitud REQUEST_PICK_APPWIDGET en respuesta a una acción del usuario:

Kotlin

    val REQUEST_CREATE_APPWIDGET = 5
    val REQUEST_PICK_APPWIDGET = 9
    ...
    override fun onClick(dialog: DialogInterface?, which: Int) {
        when (which) {
            ...
            AddAdapter.ITEM_APPWIDGET -> {
                ...
                val appWidgetId: Int = appWidgetHost.allocateAppWidgetId()
                val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK).apply {
                    putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                }
                ...
                startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
            }
            ...
        }
    }
    

Java

    private static final int REQUEST_CREATE_APPWIDGET = 5;
    private static final int REQUEST_PICK_APPWIDGET = 9;
    ...
    public void onClick(DialogInterface dialog, int which) {
        switch (which) {
        ...
            case AddAdapter.ITEM_APPWIDGET: {
                ...
                int appWidgetId =
                        Launcher.this.appWidgetHost.allocateAppWidgetId();
                Intent pickIntent =
                        new Intent(AppWidgetManager.ACTION_APPWIDGET_PICK);
                pickIntent.putExtra
                        (AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
                ...
                startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET);
                break;
        }
        ...
    }
    

Cuando la actividad del sistema finaliza, se muestra un resultado con el widget de la app elegido por el usuario para tu actividad. En el siguiente ejemplo, la actividad responde llamando a addAppWidget() para agregar el widget de la app:

Kotlin

    class Launcher : Activity(), View.OnClickListener, View.OnLongClickListener {
        ...
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
            waitingFroResult = false

            if (resultCode == RESULT_OK && addItemCellInfo != null) {
                when (requestCode) {
                    ...
                    REQUEST_PICK_APPWIDGET -> addAppWidget(data)
                    REQUEST_CREATE_APPWIDGET ->
                        completeAddAppWidget(data, addItemCellInfo, !desktopLocked)
                    ...
                }
            }
            ...
        }
    }
    

Java

    public final class Launcher extends Activity
            implements View.OnClickListener, OnLongClickListener {
        ...
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            waitingForResult = false;

            if (resultCode == RESULT_OK && addItemCellInfo != null) {
                switch (requestCode) {
                    ...
                    case REQUEST_PICK_APPWIDGET:
                        addAppWidget(data);
                        break;
                    case REQUEST_CREATE_APPWIDGET:
                        completeAddAppWidget(data, addItemCellInfo, !desktopLocked);
                        break;
                    }
            }
            ...
        }
    }
    

El método addAppWidget() verifica si se debe configurar el widget de la app antes de agregarlo:

Kotlin

    fun addAppWidget(data: Intent?) {
        if (data != null) {
            val appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)

            val customWidget = data.getStringExtra(EXTRA_CUSTOM_WIDGET)
            val appWidget: AppWidgetProviderInfo? = appWidgetManager.getAppWidgetInfo(appWidgetId)

            appWidget?.configure?.apply {
                // Launch over to configure widget, if needed.
                val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
                intent.component = this
                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                startActivityForResult(intent, REQUEST_CREATE_APPWIDGET)
            } ?: run {
                // Otherwise, finish adding the widget.
            }
        }
    }
    

Java

    void addAppWidget(Intent data) {
        int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);

        String customWidget = data.getStringExtra(EXTRA_CUSTOM_WIDGET);
        AppWidgetProviderInfo appWidget =
                appWidgetManager.getAppWidgetInfo(appWidgetId);

        if (appWidget.configure != null) {
            // Launch over to configure widget, if needed.
            Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
            intent.setComponent(appWidget.configure);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            startActivityForResult(intent, REQUEST_CREATE_APPWIDGET);
        } else {
            // Otherwise, finish adding the widget.
        }
    }
    

Para obtener más información sobre la configuración, consulta Cómo crear una actividad de configuración del widget de una app.

Una vez que el widget de la app está listo, el siguiente paso es agregarlo al espacio de trabajo. Para hacer esto, el Selector original utiliza un método llamado completeAddAppWidget().

Cómo vincular widgets de apps en Android 4.1 y versiones posteriores

En Android 4.1, se incorporaron API para lograr un proceso de vinculación más optimizado. Estas API también permiten que un host proporcione una IU personalizada para la vinculación. Para usar este proceso mejorado, tu app debe declarar el permiso BIND_APPWIDGET en su manifiesto:

<uses-permission android:name="android.permission.BIND_APPWIDGET" />
    

Pero este es solo el primer paso. Durante el tiempo de ejecución, el usuario debe otorgar de manera explícita permiso a tu app para que pueda agregar widgets al host. Si quieres probar si tu app tiene permiso para agregar el widget, usa el método bindAppWidgetIdIfAllowed(). Si bindAppWidgetIdIfAllowed() muestra false, tu app debe mostrar un cuadro de diálogo en el que se le pide al usuario que otorgue permiso ("permitir" o "permitir siempre", a fin de cubrir todos los widgets de apps que agregues en el futuro). En este fragmento, se proporciona un ejemplo de cómo mostrar el diálogo:

Kotlin

    val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName)
        // This is the options bundle discussed above
        putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options)
    }
    startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
    

Java

    Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName);
    // This is the options bundle discussed above
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options);
    startActivityForResult(intent, REQUEST_BIND_APPWIDGET);
    

El host también debe verificar si el usuario agregó el widget de una app que necesita configuración. Para obtener más información sobre este tema, consulta Cómo crear una actividad de configuración del widget de una app.

Responsabilidades del host

Los desarrolladores pueden especificar una serie de parámetros de configuración para los widgets mediante los metadatos AppWidgetProviderInfo. El host puede recuperar estas opciones de configuración, que se analizan con más detalle a continuación, desde el objeto AppWidgetProviderInfo asociado con un proveedor de widgets.

Sin importar la versión de Android a la que se oriente tu app, todos los hosts tienen las siguientes responsabilidades:

  • Cuando agregas un widget, debes asignar el ID de este como se describió anteriormente. Además, cuando se quite un widget del host, debes asegurarte de llamar a deleteAppWidgetId() para anular la asignación del ID del widget.
  • Cuando agregues un widget, asegúrate de iniciar su actividad de configuración, si existe, como se describe en Cómo actualizar el widget de la app desde la actividad de configuración. Este paso es necesario para que muchos widgets de apps se muestren de manera correcta.
  • Cada widget de una app especifica un ancho y un alto mínimos en dps, como se define en los metadatos AppWidgetProviderInfo (usando android:minWidth y android:minHeight). Asegúrate de que el widget esté diseñado con al menos esta cantidad de dps. Por ejemplo, muchos hosts alinean íconos y widgets en una cuadrícula. En este caso, de forma predeterminada, el host debe agregar el widget de la app con la cantidad mínima de celdas que cumplan con las restricciones minWidth y minHeight.

Además de los requisitos enumerados arriba, las versiones específicas de la plataforma introducen funciones que otorgan nuevas responsabilidades al host.

¿A qué versión se orienta tu app?

El enfoque que utilices para implementar tu host debe depender de la versión de Android a la que se orienta tu app. Muchas de las funciones que se describen en esta sección se introdujeron en la versión 3.0 o en versiones posteriores. Por ejemplo:

  • Android 3.0 (API nivel 11) incluye un comportamiento de avance automático para widgets.
  • Android 3.1 (API nivel 12) permite cambiar el tamaño de los widgets.
  • Android 4.0 (API nivel 15) incluye un cambio en la política de relleno, por el que se responsabiliza al host de administrar el relleno.
  • Android 4.1 (API nivel 16) incorpora una API que permite que el proveedor de widgets obtenga información más detallada sobre el entorno en el que se alojan las instancias de widgets.
  • Android 4.2 (API nivel 17) incluye el paquete de opciones y el método bindAppWidgetIdIfAllowed(). También incluye widgets de pantalla de bloqueo.

Si vas a orientar tu app a dispositivos anteriores, consulta el Selector original a modo de ejemplo.

En las siguientes secciones, se proporciona información adicional sobre las funciones que otorgan nuevas responsabilidades al host.

Android 3.0

Android 3.0 (API nivel 11) incorpora la capacidad de un widget de especificar autoAdvanceViewId(). Esta ID de vista debe orientarse a una instancia de un Advanceable, como StackView o AdapterViewFlipper. Esto indica que el host debe llamar a advance() en esta vista en un intervalo que el host considere apropiado (teniendo en cuenta si tiene sentido hacer avanzar el widget; por ejemplo, es probable que el host no quiera hacer avanzar un widget si estaba en otra página o si la pantalla estaba apagada).

Android 3.1

Android 3.1 (API nivel 12) permite cambiar el tamaño de los widgets. Un widget puede especificar que se puede cambiar su tamaño mediante el atributo android:resizeMode en los metadatos AppWidgetProviderInfo e indicar si admite el cambio de tamaño horizontal o vertical. El widget, que se introdujo en Android 4.0 (API nivel 14), también puede especificar un android:minResizeWidth y/o android:minResizeHeight.

Es responsabilidad del host hacer que el widget se pueda cambiar de tamaño en forma horizontal o vertical según lo especificado por el widget. En el caso de un widget que especifica que se puede cambiar su tamaño, este se puede cambiar de forma arbitraria, pero no se debe reducir el tamaño a valores inferiores a los especificados por android:minResizeWidth y android:minResizeHeight. Para ver una implementación de ejemplo, consulta AppWidgetResizeFrame en Launcher2.

Android 4.0

Android 4.0 (API nivel 15) incluye un cambio en la política de relleno, mediante el cual se responsabiliza al host de administrar el relleno. A partir de la versión 4.0, los widgets de aplicaciones ya no incluyen su propio relleno. En cambio, el sistema agrega relleno para cada widget según las características de la pantalla actual. Esto genera una presentación más uniforme y consistente de widgets en una cuadrícula. Para ayudar a las apps que alojan widgets, la plataforma proporciona el método getDefaultPaddingForWidget(). Las apps pueden invocar este método para obtener el relleno definido por el sistema y tenerlo en cuenta cuando calculan la cantidad de celdas que se asignarán al widget.

Android 4.1

Android 4.1 (API nivel 16) incorpora una API que permite que el proveedor de widgets obtenga información más detallada sobre el entorno en el que se alojan las instancias de widgets. En particular, el host sugiere al proveedor del widget el tamaño en el que se debe mostrar el widget. Es responsabilidad del host proporcionar la información del tamaño.

El host proporciona esta información mediante updateAppWidgetSize(). Se especifica el tamaño como ancho/altura mínimos y máximos en dps. Se especifica un rango (en lugar de un tamaño fijo) porque el ancho y la altura de un widget pueden cambiar según la orientación. No es conveniente que el host tenga que actualizar todos los widgets durante la rotación, ya que esto podría causar demoras graves del sistema. Se deben actualizar estos valores cuando se coloca el widget, cada vez que se cambia su tamaño y cuando el selector aumenta el widget por primera vez durante un inicio determinado (ya que los valores no son persistentes durante todos los inicios).

Android 4.2

Android 4.2 (API nivel 17) incorpora la capacidad de especificar el paquete de opciones durante la vinculación. Se trata de una manera ideal de especificar opciones del widget de una app, ya que se otorga al AppWidgetProvider acceso inmediato a los datos de opciones durante la primera actualización. Esto se puede lograr usando el método bindAppWidgetIdIfAllowed(). Para obtener más información sobre este tema, consulta Vinculación de widgets de apps.

Android 4.2 también incluye widgets de pantalla de bloqueo. Cuando se alojan widgets en la pantalla de bloqueo, el host debe especificar esta información en el paquete de opciones del widget de la app (el AppWidgetProvider puede usar esta información para diseñar el widget de manera adecuada). Para designar un widget como widget de pantalla de bloqueo, usa updateAppWidgetOptions() e incluye el campo OPTION_APPWIDGET_HOST_CATEGORY con el valor WIDGET_CATEGORY_KEYGUARD. Esta opción predeterminada es WIDGET_CATEGORY_HOME_SCREEN, por lo que no es necesario de manera explícita configurarla para un host de pantalla principal.

Asegúrate de que tu host solo agregue widgets para tu app; por ejemplo, si el host es una pantalla principal, asegúrate de que el atributo android:widgetCategory de los metadatos AppWidgetProviderInfo incluya la marca WIDGET_CATEGORY_HOME_SCREEN. Del mismo modo, para la pantalla de bloqueo, asegúrate de que el campo incluya la marca WIDGET_CATEGORY_KEYGUARD. Para obtener más información sobre este tema, consulta Cómo habilitar widgets de apps en la pantalla de bloqueo.