Notificación y alarmas en Flutter

Nov 10 2020
Trabajando con el complemento Flutter, Android_Alarm_Manager. ¡Sepa esto! ¡Este complemento solo funciona para la plataforma Android! Personalmente, no conozco ningún equivalente de iOS.

Trabajando con el complemento Flutter, Android_Alarm_Manager.

¡Sepa esto! ¡Este complemento solo funciona para la plataforma Android ! Personalmente, no conozco ningún equivalente de iOS. Alternativamente, un mes después de publicar este artículo, encontré un complemento que proporciona notificaciones en las plataformas Android e iOS. Yo, por supuesto, escribí un artículo y eso también. Vea abajo.

Notificaciones en Flutter

Sepa que si desea utilizar el complemento que se describe aquí, debe seguir su archivo Léame explícitamente para configurar las cosas correctamente. Eres AndroidManfest.xml, al menos debería verse así a continuación:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="NAME OF YOUR APPLICATION STARTING WITH COM.">
<!-- The INTERNET permission access.-->
    <uses-permission android:name="android.permission.INTERNET"/>

    <!-- android_alarm_manager -->
    <!-- Start an Alarm When the Device Boots if past due -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- application needs to have the device stay on -->
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
<application
        android:name="io.flutter.app.FlutterApplication"
        android:label="code_samples"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <meta-data android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <meta-data
              android:name="io.flutter.embedding.android.SplashScreenDrawable"
              android:resource="@drawable/launch_background"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!-- android_alarm_manager -->
        <service
                android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
                android:permission="android.permission.BIND_JOB_SERVICE"
                android:exported="false"/>
        <receiver
                android:name="io.flutter.plugins.androidalarmmanager.AlarmBroadcastReceiver"
                android:exported="false"/>
        <receiver
                android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
                android:enabled="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

# https://pub.dev/packages/android_alarm_manager
android_alarm_manager: ^0.4.0

En este caso, ¡es para activar alarmas en tu aplicación! En mi caso, encontré el complemento de Flutter, android_alarm_manager , que satisfacía las necesidades de una aplicación reciente en la que estaba trabajando y así… hice una rutina para trabajar fácilmente con ella. Si lo desea, tome una copia , hágala suya y comparta las mejoras que realice. ¿Frio?

Ahora, si tiene tiempo, continúe leyendo lo que descubrí que tenía que hacer para que este complemento funcionara casi a prueba de tontos, de modo que pueda configurar fácil y rápidamente una alarma o cualquier otra operación que se realice 'en segundo plano' en algún momento en el futuro mientras se ejecuta la aplicación. Hace la vida un poco más fácil, y eso es algo bueno. ¿Derecho?

Solo capturas de pantalla. Haga clic en Para Gists.

Como siempre, prefiero usar capturas de pantalla sobre lo esencial para mostrar el código en mis artículos. Me resulta más fácil trabajar con ellos y más fáciles de leer. Sin embargo, puede hacer clic / tocar en ellos para ver el código en una esencia o en Github. Irónicamente, es mejor leer este artículo sobre desarrollo móvil en su computadora que en su teléfono. Además, programamos principalmente en nuestras computadoras; no en nuestros teléfonos. Por ahora.

Vamos a empezar.

Otras historias de Greg Perry

Mi enfoque aquí es presentar primero esta clase de utilidad en un ejemplo y demostrar cómo usarla, aprovechando así el complemento Flutter, android_alarm_manager . De hecho, usaré el mismo ejemplo que aparece en la página de ejemplo del propio complemento . Sin embargo, este ejemplo se ha modificado para utilizar el archivo de biblioteca presentado aquí. Una copia de este ejemplo está disponible como esencia, android_alarm_manager . Después del ejemplo, recorreré partes de la clase de utilidad en sí misma describiendo a veces lo que se debe hacer para permitir que dicha clase sea utilizada por las masas inquebrantables que son el público en general.

Mantenlo estático

En este ejemplo muy simple, debe presionar el botón que se muestra, y 5 segundos después, esos dos ceros como se muestra en la captura de pantalla a continuación se convertirán en unos. ¡Wheeee! Lo que es realmente interesante, por supuesto, es lo que hay debajo del capó en el código.

Tenga en cuenta que cambié el código del original para acomodar las operaciones asincrónicas que deben realizarse antes de que la aplicación pueda comenzar a ejecutarse. He utilizado un widget de FutureBuilder para lograr esto. Al hacerlo, definí una nueva función llamada, initSettings () , para inicializar la rutina 'Preferencias compartidas' que se usa para 'recordar' el número total de alarmas disparadas como se ve en la captura de pantalla anterior.

android_alarm_manager.dart

También puede ver en la función initSettings () que se muestra a continuación, establece el 'recuento total' en cero si es la primera vez que se ejecuta la aplicación. Sin embargo, es al principio que la rutina de la biblioteca, AlarmManager , se inicializa para configurar el 'Servicio de alarma' particular necesario para realizar notificaciones en un teléfono Android.

android_alarm_manager.dart

Vayamos más abajo en el código de ejemplo y veamos qué compone ese pequeño botón. Tenga en cuenta que la rutina de la biblioteca usa los mismos nombres para los parámetros y funciones que componen el complemento Flutter subrayado, Android_Alarm_Manager . Mejor ser coherente con esas cosas. Además, al igual que la función oneShot () del propio complemento , la versión de esta biblioteca, una vez llamada, 'esperará' durante el tiempo especificado antes de activar la rutina de devolución de llamada especificada. En el caso de este ejemplo modificado, la rutina de devolución de llamada es una función anónima que se ejecutará después de una duración de 5 segundos. En la captura de pantalla a continuación, la aplicación, de hecho, se reinició con el botón presionado nuevamente, lo que indica, gracias a Preferencias compartidas, que el botón se presionó dos veces desde que se ejecutó por primera vez. Wheeee.

android_alarm_manager.dart

Una mirada más cercana a esa función anónima a continuación, y vemos que se le pasa un único parámetro de tipo, entero. Puede adivinar que ese es el mismo valor de identificación pasado al segundo parámetro como un número aleatorio usando las funciones de Dart, Random (). NextInt (pow (2,31)) . Ahora, ¿por qué molestarse en pasar ese valor cuando ya se ha pasado al parámetro que está justo al lado? Llegaremos a eso.

Por ahora, puede ver más abajo que la función de devolución de llamada, a su vez, llama a la función _incrementCounter (). Allí, el 'recuento total' actual de pulsaciones de botones se recupera de la rutina 'Preferencias compartidas'. Luego, se actualiza por uno para tener en cuenta la pulsación de este botón actual y se guarda de nuevo en Preferencias compartidas. Luego, la pantalla de la aplicación se actualiza con la variable incrementada, _counter , usando la función, setState (). ¿Seguiste todo eso hasta ahora?

android_alarm_manager.dart

Manteniéndolo estático

A diferencia del ejemplo original (ver más abajo), este ejemplo puede usar una función anónima y no es necesario que use específicamente una función estática. Por supuesto, también puede usar una función estática en este ejemplo, siempre y cuando todavía acepte ese único valor entero. Sin embargo, en el ejemplo original, debe ser una función estática o una función de alto nivel definida fuera de cualquier clase en ese archivo Dart de la biblioteca; ya ve, cualquiera de las dos es un requisito cuando se trabaja directamente con el complemento Flutter, Android_Alarm_Manager .

Sin embargo, si ha estado siguiendo mis artículos, sabrá que me gustan las opciones. Se trata de tener opciones conmigo. Escribí esta clase de utilidad para que sea más complaciente, para permitir funciones anónimas, por ejemplo. Por lo tanto, con tal disposición, pasar ese valor entero de id como parámetro permite que la función sea una función estática, una función de alto nivel o una función anónima. Opciones! Nuevamente, a continuación se muestran capturas de pantalla del ejemplo original:

Pasemos a la clase de utilidad, AlarmManager , en sí. Nuevamente, está diseñado para funcionar con el complemento. Y de nuevo, muchos de sus parámetros son el mismo parámetro utilizado por el complemento y, por lo tanto, se pasan a ese complemento, pero no antes de realizar pruebas exhaustivas de parámetros en busca de valores válidos. Otro rasgo necesario de tales clases de utilidad. Hace todo el trabajo para que usted no tenga que hacerlo. ¿Derecho?

En la captura de pantalla a continuación, es la primera parte de esta clase de utilidad. En su función estática, init (), vemos que el complemento está realmente inicializado. Tenga en cuenta que cualquier error desafortunado que pueda ocurrir en el intento de inicialización se detectará en la declaración try-catch . Las clases de servicios públicos también necesitan hacer eso. A continuación, en la función init (), está la clase 'helper', _Callback , que se llama para inicializar los medios necesarios para que la aplicación se comunique con el Aislamiento separado utilizado por el Servicio de alarma en segundo plano.

Por último, puede ver a continuación que todos y cada uno de los valores de los parámetros no nulos se asignan a las propiedades específicas y también estáticas que componen la clase de utilidad, AlarmManager . No hay mucha estática por aquí.

alarm_manager.dart

Encontrará que muchas de las propiedades y funciones que componen esta clase son estáticas. Sin embargo, la elección de usar miembros estáticos en una clase debe ser moderada. Por ejemplo, dado que la función init () es estática, eso significa que se puede llamar en cualquier lugar, en cualquier momento y cualquier número de veces. Ese hecho requiere una consideración adicional. En este caso, la primera declaración de una línea en la captura de pantalla anterior es una declaración if: 'i f (_init) return _init;'. Era necesario al escribir esta función init (). Con eso, ahora puede llamar a esa función tantas veces como desee. Independientemente, los servicios y complementos necesarios solo se inicializan con la primera llamada. Y así, en un equipo de desarrolladores, por ejemplo, si la llamada a la función init () se realiza por error más de una vez, no hay ningún daño. Otra característica deseable de una clase de servicios públicos. ¿Ves lo que estoy haciendo aquí? Haciéndolo "infalible". ¿Derecho?

Inicie su configuración

Por cierto, con esos parámetros pasados ​​a esas variables estáticas, eso significa que tiene más opciones en nuestro ejemplo. Cuando se llama a la función init (), la configuración podría haberse especificado allí mismo. Hacerlo permitiría que todas y cada una de las llamadas posteriores a las funciones, oneShot (), oneShotAt () y periodic (), usen esas configuraciones si no proporcionan explícitamente las suyas propias. Lo he demostrado a continuación. Puede ver las diferencias hechas en el código de ejemplo si en su lugar se usaron los parámetros adicionales en la función init (). Eso solo deja la llamada a la función 'oneShot' con su duración, su identificación y la función de devolución de llamada necesaria. Hace que el código sea un poco más limpio. Opciones!

D ¡No coloque init () en una función build () ! Parecería que la función init () del propio complemento de Flutter se llama AndroidAlarmManager. initialize () ;, es propenso a causar efectos secundarios o problemas. En algunos casos, iniciará una reconstrucción (muy parecido a llamar a la función setState ()). Es por eso que mi clase de utilidad tiene una función init () separada . Es preferible que se llame cerca del inicio de su aplicación, por ejemplo, en un widget FutureBuilder con MaterialApp. Compruébelo usted mismo y, en su ejemplo modificado, intente comentar la función AlarmManger.init () desde initSettings () y colóquela en su lugar justo antes de su función oneShot () (ver más abajo). Su ejemplo comenzará a encontrar errores.

android_alarm_manager.dart

Toma tu oneShot

ok, de vuelta a la clase de utilidad. En la función oneShot (), los primeros tres valores de parámetros 'obligatorios' están probando su validez y se pasan a la función oneShot () del propio complemento . Todos excepto el parámetro 'Función', devolución de llamada . En su lugar, eso se agrega a un objeto Map estático identificado de manera única por el ID de entero proporcionado usando el siguiente comando, _Callback.oneShots [id] = callback . Volveremos a eso muy pronto. Por último, observa que hay una llamada a la función estática, oneShot (), que también se encuentra en esa clase auxiliar, _Callback . Se erige como la 'función estática' necesaria para ser utilizada por el complemento. Esto deja el resto de los valores de los parámetros para tomar esas muchas variables estáticas usando el operador de fusión nula, ?? . El operador se utiliza de modo que cuando no se pasa un parámetro explícito, se utilizan en su lugar los valores de esas variables estáticas. ¿Consíguelo? Por cierto, todas esas variables estáticas se inicializan con valores predeterminados para que no se pasen valores nulos al complemento. Agradable.

alarm_manager.dart

No te arriesgues

Tenga en cuenta que la llamada al complemento en sí también se incluye en una declaración try-catch . Eso es porque es un programa de terceros. No sabemos qué podría suceder, y dado que esta es una clase de utilidad, no queremos bloquear su aplicación, sino detectar cualquier excepción que pueda ocurrir.

Además, como cualquier buena clase de utilidad, esta clase tiene los medios para que el desarrollador 'pruebe' si la operación fue exitosa o no. De lo contrario, se registra cualquier excepción que pueda haber ocurrido para que el desarrollador pueda manejarla. Esto se demuestra a continuación en el ejemplo modificado con la declaración if recién insertada .

android_alarm_manager.dart

Más estático

Más adelante en la clase de servicios públicos, AlarmManager . Vemos la función oneShotAt (). Nuevamente, debido a que todas estas funciones son funciones estáticas, es necesario incorporar algunas salvaguardas en el código. En circunstancias desafortunadas, por ejemplo, es posible que el complemento no se inicialice primero cuando se llama a esta función onShotAt (). En otras palabras, su función init () no se llamó primero. Podría suceder cuando lo usa el público en general. Puede ver en la captura de pantalla a continuación, tal situación se prueba con la función assert (). Esto es con la esperanza de que un desarrollador detecte tal error mientras está en desarrollo. En producción, está atrapado por esa declaración if que sigue a la función assert ().

Tenga en cuenta que esta función oneShotAt () tiene su propio objeto Map para almacenar la función 'callback' pasada, y tiene su propia función estática, _Callback.onShatAt (), para pasar a la función oneShotAt () propia del complemento . Todo esto implica, por cierto, que puede llamar a estas funciones cualquier cantidad de veces en su aplicación programando cualquier cantidad de operaciones para que ocurran en el futuro. Por supuesto, cada uno debe tener asignado su propio valor de identificación único, recuerde. De lo contrario, cualquier operación ya programada se sobrescribirá con una nueva si utiliza el mismo valor de identificación. Ese es el punto cuando se usan identificaciones únicas. ¿Derecho?

Sin embargo, todo esto también implica que puede usar la misma identificación, pero entre las tres funciones diferentes, oneShot (), oneShotAt () y periodic () por separado. Recuerde, tienen sus propios objetos Map y funciones estáticas independientes. Este hecho me sirvió bien en mi proyecto reciente en el que las identificaciones utilizadas eran los mismos valores que se encuentran en los campos primarios de la base de datos residente. ¡Opciones, cariño! ¡Quiéralo!

alarm_manager.dart

Un vistazo a los complementos

Un vistazo rápido ahora a las funciones oneShot () y oneShotAt () del propio complemento de Flutter , y puede ver que su función oneShot (), de hecho, simplemente pasa su parámetro a su contraparte oneShotAt (). Tenga en cuenta que el objeto 'CallbackHandle' que ve en la captura de pantalla siguiente proviene de la función, _getCallbackHandle (), que, a su vez, llamó a la función de marco de Flutter, PluginUtilities.getCallbackHandle (devolución de llamada) . Esta operación 'arranca' una copia de la función de devolución de llamada para tener acceso a ella y llamar a dicha función en el Aislamiento que se ejecuta en segundo plano. Volveré a eso también.

La operación de devolución de llamada

Continuemos por ahora y echemos un vistazo a la 'clase auxiliar' , _Callback , en el archivo de la biblioteca . Puede ver a continuación que los objetos Map que se agregan en la función Callback se definen en esta clase como propiedades estáticas. Esta clase también tiene una función init () y se llama en la función init () del propio AlarmManger . Es en esta función init () donde se registra un 'puerto de comunicación' con tres identificadores de nombre específicos. El puerto es utilizado por el Isolate en segundo plano para comunicarse con el Isolate en primer plano pasándole mensajes. ¿Adivina cuáles son los valores de esos identificadores de nombres? Aparecen en la pantalla de abajo se almacena en las variables, _oneShot , _oneShotAt , y _periodic .

alarm_manager.dart

Como su nombre lo indica, los aislamientos son segmentos separados de memoria
que están por diseño, bueno… aislados. No se comparte la memoria. Solo existe el paso de mensajes entre Aislados. El contenido de dicho mensaje puede ser un valor primitivo (nulo, num, bool, double, String), una instancia de un objeto SendPort , un objeto List o un objeto Map con cualquiera de esos valores primitivos mencionados en primer lugar.

Escuchar en segundo plano

Al puerto se le asigna además un 'oyente' para reaccionar si y cuando se recibe un mensaje, en este caso, por el fondo Isolate que ejecuta el servicio de alarma. El oyente tiene la forma de una función anónima que toma un objeto Map como parámetro. Puede ver a continuación que el objeto Map contiene un valor entero (que resulta ser el id) y una cadena que almacena uno de esos 'identificadores de nombre'. La declaración del caso determina entonces qué "tipo" de función se activará. ¿Ves cómo funciona eso? Por supuesto, al ser una clase de utilidad, todo está incluido en una declaración try-catch . Por ejemplo, no tenemos idea de lo que sucederá cuando se ejecute la función elegida. Queremos detectar cualquier excepción que pueda ocurrir. ¿Derecho?

alarm_manager.dart

Enviar un mensaje

Entonces, ¿cómo se envía ese mensaje a la aplicación que se ejecuta en primer plano? Bueno, una vez que esos identificadores de nombre estén registrados arriba, entonces (ver abajo) cualquiera de las tres funciones de clase de utilidad, AlarmManager . oneShot (), AlarmManager . oneShotAt () , y AlarmManager . periodic (), pasará las tres funciones estáticas correspondientes, _Callback . onShot (), _Callback . onShotAt () y _Callback . periodic (), directamente al complemento Flutter. Si lo hace, permitirá que Isolate en segundo plano pase un mensaje a la aplicación que se ejecuta en Isolate en primer plano. Los tres tipos de llamadas se enumeran a continuación.

Verá, son estas tres funciones estáticas correspondientes, _Callback . onShot (), _Callback . onShotAt () y _Callback . periodic (), que son 'el puente' desde el fondo Isolate hasta el primer plano Isolate. Cuando llega el momento de activar una alarma, por ejemplo, el servicio de alarma llamará a una de estas tres funciones estáticas. Tenga en cuenta que, como sucede, no es la función real definida en el primer plano Aislar, sino una 'copia arrancada' de la misma. Verá un comportamiento contrario a la intuición debido a este hecho. Por ejemplo, cualquier variable estática en esa función normalmente definida en primer plano Aislar será nula en segundo plano Aislar. Puede probar este fenómeno usted mismo.

Por ejemplo, en nuestro ejemplo modificado, si presioné el botón por tercera vez, sabemos que el objeto Map, oneShots , tiene ese objeto Function para ejecutar y actualizar la pantalla después de cinco segundos, y eso es lo que hace en la captura de pantalla. de la rutina 'Listener' a continuación. Sin embargo, en el proceso, ese objeto Mapa está vacío si se accede en segundo plano ¿Aislar? ¿Cómo es eso posible? Es posible porque es una copia del objeto Mapa y no el que está en primer plano Aislar. Una vez más, los aislamientos solo pueden pasar "mensajes" entre sí. No comparten memoria.

Nuevamente, estas tres funciones estáticas están en la clase auxiliar, _Callback , listadas una tras otra. Se muestran en la captura de pantalla a continuación. En cada uno, puede ver que se hace referencia al Aislamiento de primer plano utilizando los 'identificadores de nombre' y se le pasa un objeto Mapa del Aislamiento de fondo. Tenga en cuenta que el operador de acceso de miembros condicional, ?. , se utiliza en caso de que la operación de búsqueda devuelva un valor nulo. Lo hará si el nombre no existe, por ejemplo. No es probable que suceda nunca ya que todo esto es código internalizado, pero al ser una clase de utilidad, no corremos ningún riesgo. ¿Derecho?

Todo esto se encuentra al final del archivo de biblioteca, y es aquí donde finalmente vemos el valor de esos '' identificadores de nombre en las variables, _oneShot , _oneShotAt , y _periodic . Cada uno de ellos recibe el nombre de su tipo de función correspondiente. No es muy imaginativo, pero tiene sentido. También vemos que son variables de alto nivel definidas fuera de una clase o función de alto nivel. De hecho, son variables constantes con la palabra clave const . Como las variables finales, las variables const se inicializan solo una vez y luego no se pueden cambiar. A diferencia de las variables finales, se definen cuando se compila la aplicación. Por lo tanto, para nuestras necesidades aquí, están disponibles incluso para aislar en segundo plano. Agradable.

alarm_manager.dart

Si no tienes un control sobre todas estas cosas de Aislar. No se preocupe por eso ahora mismo. Para eso es la clase de servicios públicos. A diferencia del código de ejemplo del complemento original, no tiene que preocuparse por la configuración de 'puertos de comunicación' o cuándo usar una función o variable estática o de alto nivel. Por eso escribí esta clase en primer lugar, para que tampoco tenga que preocuparme por todas esas cosas. Es por eso que estas clases de utilidad están escritas, para que puedan ser utilizadas una y otra vez por nuestras muchas aplicaciones que todos escribiremos en el futuro. ¿Derecho?

Salud.

→ Otras historias de Greg Perry

DECODE Flutter en YouTube