En aquest exemple ja no explicarem tots els passos que ja s'han comentat en els exemples inicials, per donar èmfasi en les particularitats d'aquesta aplicació. Tampoc ens preocuparem gaire per l'aspecte de l'aplicació.
La nostra aplicació està en la carpeta detecta_caiguda. El programa principal tindrà el contingut habitual i cridarà a la pantalla principal.
main.dart
import 'package:detecta_caiguda/screens/pant_principal.dart'; import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: PantPrincipal()
);
}
}
La pantalla principal estarà en el fitxer pant_principal.dart, dins de la carpeta screens; que haurem creat a la carpeta lib. Tindrà un text, on es mostrarà l'estat del sistema, un botó per reiniciar la detecció i un futur que mostrarà el resultat de la comunicació amb Telegram.
...
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Detector de caigudes'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_estat,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _reiniciDeteccio,
child: const Text("Reiniciar"),
),
const SizedBox(height: 30),
FutureBuilder(future: _rebut, builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return Text("Error: ${snapshot.error}");
} else if(snapshot.hasData){
return escriuResultats(snapshot.data);
} else {
return Text("");
}
})
],
),
),
);
}
}
...
Quan es detecti la caiguda, s'ha de fer una crida web per enviar el text. La informació que rebrem serà la resposta de Telegram. Aquesta resposta la decodificarem i analitzarem, per veure si la transmissió ha estat correcta. A la pantalla mostrarem el resultat de l'enviament.
Hem de fer la classe que s'encarrega de fer la crida web. Aquesta classe l'anomenarem CridaWeb i estarà en el fitxer crida_web.dart, que crearem a la carpeta data. Els codis per accedir a Telegram els posarem en un fitxer separat perquè quedin més amagats i, al mateix temps, siguin més fàcils de modificar. Per facilitar la comprensió, a continuació hi ha l'arbre de carpetes i fitxers:
lib
main.dart
data
claus_telegram.dart
crida_web.dart
screens
pant_principal.dart
El fitxer de les claus serà el següent:
claus_telegram.dart
class ClausTelegram{
static const token
= "^^34628844:AAFIpk-e7j3UZtYQYQaTduf4mPhnDqIcNXI";
static const usrId = "^^9199456";
}
A la classe CridaWeb, crearem el mètode enviaDades. Des que es faci la crida fins que arribi la resposta pot passar un temps; o, fins i tot, pot no arribar mai. Per tant, aquest mètode no el podrem definir com els que hem fet servir fins ara. Per un costat, el resultat del mètode serà un futur (Future), un tipus especial d'estructura de dades que s'omple de manera diferida. Per un altre costat, s'ha declarat com a asíncron (async) per tal que la resta de l'aplicació pugui continuar funcionant mentre s'espera l'arribada de les dades.
Ara és quan anem a fer la crida web; amb la funció http.get. El resultat de la lectura el guardarem a la variable resposta. Per tal que la funció http.get estigui disponible, hem de fer dues coses. Per un costat, cal anar al fitxer pubspec.yaml per dir-li que la volem fer servir. Cal posar-ho a dependencies, indicant quina és la versió que volem. És recomanable indicar la darrera versió; atès que quan comencem a escriure http ens ajuda a autocompletar, el normal és que ja ens doni la darrera versió. Quan vam escriure això, la darrera era la 1.5.0. També ens cal carregar sensors_plus, per llegir el sensor d'acceleració; en aquest cas, la darrera versió quan vam fer aquest programa era la 7.0.0.
...
dependencies:
flutter:
sdk: flutter
sensors_plus: ^7.0.0
http: ^1.5.0
...
Un cop afegit, cal picar al botó per baixar el paquet (
).
Per un altre costat, cal importar el paquet http.dart; ja que no ho fa de manera automàtica.
Si es preveu que l'aplicació es pugui instal·lar en dispositius que tinguin versions antiques d'Android, cal donar permís d'accés a internet. Això ho farem anar al fitxer AndroidManifest.xml que es troba a la carpeta main de la carpeta src de la carpeta app de la carpeta android i afegir la línia del permís després de la de manifest i abans de la d'application:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
...
Atenció: Cal tenir en compte que l'absència d'aquest permís pot donar problemes a l'executar l'aplicació en el dispositiu, malgrat aquesta funcioni correctament en el simulador. Per assegurar que l'aplicació funcionarà en qualsevol dispositiu Android, convé posar aquesta línia sempre que s'hagn de fer connexions a internet.
Obtindrem les dades del servidor amb la comanda http.get, a la qual se li ha de passar l'adreça en format URI. Per convertir l'adreça de l'enllaç a aquest format, fem servir la funció Uri.parse. El resultat no es pot assignar directament a la variable resposta, perquè trigarà en arribat; per això afegim l'ordre await, per indicar que ha d'esperar el resultat. La resposta tampoc serà directament text, per això fem servir la funció utf8.decode per fer la conversió. L'adreça és força complexa, per això l'hem distribuït en dues variables.
crida_web.dart
import 'dart:convert'; import 'package:detecta_caiguda/data/claus_telegram.dart'; import 'package:http/http.dart' as http;
class CridaWeb{
String urlTgm =
"https://api.telegram.org/bot${ClausTelegram.token}";
String urlBase =
"/sendMessage?chat_id=${ClausTelegram.usrId}&text=";
Future<String> enviaDades(missat) async{
String resp = "L'enviament ha fallat";
final resposta =
await http.get(Uri.parse("$urlTgm$urlBase$missat"));
String decoded = utf8.decode(resposta.bodyBytes);
if(decoded.contains('"ok":true')){
resp = "Enviament correcte";
}
return resp;
}
}
Ens falta completar la pantalla principal. Hem de crear un detector d'events (_streamSubscription) que estarà pendent dels events de l'acceleròmetre. Quan es detecti un canvi en l'acceleració, es calcularà el mòdul d'aquesta (fent l'arrel quadrada de la suma dels quadrats de les tres components) i es deixarà en múltiples de l'acceleració de la gravetat (per això dividim pel quadrat d'aquesta). Aquest mòdul es compararà amb un llindar i, si el supera, s'executarà la funció que haurà d'enviar el missatge. Per evitar crides successives a la funció, hi ha una variable que ho memoritza; així només enviem el missatge una vegada.
...
class _PantPrincipalState extends State<PantPrincipal> {
// Llindar per a la detecció de caiguda. Aquest valor l'hauràs d'ajustar
// segons la sensibilitat que vulguis. Un valor entre 15 i 25 sol funcionar bé.
Future<String>? _rebut;
CridaWeb cridaWeb = CridaWeb();
static const double llindarSacsejar = 18.0;
StreamSubscription<UserAccelerometerEvent>? _streamSubscription;
String _estat = "Esperant moviment...";
bool _caigudaDetectada = false;
@override
void initState() {
super.initState();
// Comencem a escoltar els events de l'acceleròmetre
_streamSubscription =
userAccelerometerEventStream(samplingPeriod: SensorInterval.normalInterval)
.listen((UserAccelerometerEvent event) {
// Calculem la magnitud total de l'acceleració
// g = sqrt(x^2 + y^2 + z^2)
final double g =
(event.x * event.x + event.y * event.y + event.z * event.z) / (9.81 * 9.81);
// Si la força g supera el nostre llindar, executem la funció
if (g > llindarSacsejar && !_caigudaDetectada) {
// Cridem a la funció que volem executar
_siCaigut();
}
});
}
...
La funció _siCaigut és molt senzilla, només envia el missatge i modifica la variable que evita múltiples enviaments.
...
void _siCaigut() {
setState(() {
_estat = "S'ha detectat una caiguda!";
_rebut = cridaWeb.enviaDades(_estat);
_caigudaDetectada = true; // Marquem que ja s'ha detectat per no repetir
});
}
...
La funció _reiniciDeteccio, que s'executa en prémer el polsador, torna a permetre la transmissió.
...
// Funció per reiniciar la detecció (per a la prova)
void _reiniciDeteccio() {
setState(() {
_estat = "Esperant moviment...";
_caigudaDetectada = false;
});
}
...
Els detectors d'events consumeixen força memòria, convé alliberar-la. Per aquest motiu, hem de sobrescriure el mètode dispose, afegint-hi la cancel·lació de la subscripció.
...
@override
void dispose() {
// És molt important cancel·lar la subscripció per evitar pèrdues de memòria
_streamSubscription?.cancel();
super.dispose();
}
...
El programa complet de la pantalla principal serà el següent:
pant_principal.dart
import 'package:detecta_caiguda/data/crida_web.dart'; import 'package:flutter/material.dart'; import 'package:sensors_plus/sensors_plus.dart'; import 'dart:async';
class PantPrincipal extends StatefulWidget {
const PantPrincipal({super.key});
@override
State<PantPrincipal> createState() => _PantPrincipalState();
}
class _PantPrincipalState extends State<PantPrincipal> {
// Llindar per a la detecció de caiguda. Aquest valor l'hauràs d'ajustar
// segons la sensibilitat que vulguis. Un valor entre 15 i 25 sol funcionar bé.
Future<String>? _rebut;
CridaWeb cridaWeb = CridaWeb();
static const double llindarSacsejar = 18.0;
StreamSubscription<UserAccelerometerEvent>? _streamSubscription;
String _estat = "Esperant moviment...";
bool _caigudaDetectada = false;
@override
void initState() {
super.initState();
// Comencem a escoltar els events de l'acceleròmetre
_streamSubscription =
userAccelerometerEventStream(samplingPeriod: SensorInterval.normalInterval)
.listen((UserAccelerometerEvent event) {
// Calculem la magnitud total de l'acceleració
// g = sqrt(x^2 + y^2 + z^2)
final double g =
(event.x * event.x + event.y * event.y + event.z * event.z) / (9.81 * 9.81);
// Si la força g supera el nostre llindar, executem la funció
if (g > llindarSacsejar && !_caigudaDetectada) {
// Cridem a la funció que volem executar
_siCaigut();
}
});
}
void _siCaigut() {
setState(() {
_estat = "S'ha detectat una caiguda!";
_rebut = cridaWeb.enviaDades(_estat);
_caigudaDetectada = true; // Marquem que ja s'ha detectat per no repetir
});
}
// Funció per reiniciar la detecció (per a la prova)
void _reiniciDeteccio() {
setState(() {
_estat = "Esperant moviment...";
_caigudaDetectada = false;
});
}
@override
void dispose() {
// És molt important cancel·lar la subscripció per evitar pèrdues de memòria
_streamSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Detector de caigudes'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_estat,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _reiniciDeteccio,
child: const Text("Reiniciar"),
),
const SizedBox(height: 30),
FutureBuilder(future: _rebut, builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return Text("Error: ${snapshot.error}");
} else if(snapshot.hasData){
return escriuResultats(snapshot.data);
} else {
return Text("");
}
})
],
),
),
);
}
}
Widget escriuResultats(String? dades){
if(dades != null){
return Text(dades.toString());
}
return Text("Alguna cosa ha fallat");
}
Nota: En aquest programa hi ha dues funcions que s'han desenvolupat a partir d'una consulta a Google Gemini.

Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.