Materia de Aplicaciones Móviles y Servicios Telemáticos
Demostrar el acceso a los recursos en red para la programación de aplicaciones móviles avanzadas.
Recursos: Android Studio. SDK Flutter.
Duración: 10 horas.
Introducción Flutter es un conjunto de herramientas de interfaz de usuario multiplataforma que está diseñado para permitir la reutilización de código en sistemas operativos como iOS y Android, al mismo tiempo que permite que las aplicaciones interactúen directamente con los servicios de la plataforma subyacente. El objetivo es permitir que los desarrolladores entreguen aplicaciones de alto rendimiento que se sientan naturales en diferentes plataformas, adoptando las diferencias donde existen mientras comparten la mayor cantidad de código posible.
Actividades
Crearemos un nuevo proyecto para familiarizarnos con los archivos de flutter.
En caso de tener más interés en el desarrollo de flutter, y tiene experiencia en Android nativo, se recomienda visitar el siguiente enlace: https://flutter.dev/docs/get-started/flutter-for/android-devs. Aquí se explica a un programador de Android, cuáles son sus equivalentes en Flutter. Así mismo encontrará para desarrolladores de IOS, Web, y más.
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
_AppState createState() => _AppState();
}
class MyHomePage extends StatefulWidget {
...
}
class _MyHomePageState extends State<MyHomePage> {
...
}
class _AppState extends State<MyApp> {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Vacheck',
navigatorKey: navigatorKey,
initialRoute:'nav',
routes:{
'nav': (context) => Nav(),
},
theme: ThemeData(
primaryColor: Colors.red[400],
),
);
}
}
import 'package:flutter/material.dart';
class Nav extends StatefulWidget {
@override
_NavState createState() => _NavState();
}
class _NavState extends State<Nav> {
int _currentIndex = 1;
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('Vacheck'),
),
body: _llamarPagina(_currentIndex),
bottomNavigationBar: _crearNavigationBar(),
);
}
Widget _crearNavigationBar() {
return BottomNavigationBar(
unselectedItemColor: Color.fromRGBO(168, 97, 93, 0.4),
iconSize: 30.0,
unselectedIconTheme: IconThemeData(size: 25.0),
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.data_usage), label: 'Data'),
BottomNavigationBarItem(
icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(Icons.bar_chart), label: 'Chart'),
]);
}
Widget _llamarPagina(int paginaActual) {
switch (paginaActual) {
default:
return Text("Hola Mundo");
}
}
}
La primera variable current index, nos ayudara luego para poder usar la barra de navegación entre páginas.
La función build es la que genera el widget que se muestra, el cual tiene título, barra de navegación y cuerpo. En el caso de la barra de navegación llamamos a la función _crearNavigationBar que crea una barra de navegación con 3 ítems, y que cambia el índice de la página en la que nos encontramos al presionar en el botón.
Por último, está la función _llamarPagina que se encarga de llamar a la página que el usuario seleccione, por ahora hemos dejado el switch en default para que en cualquier caso nos muestre siempre el mensaje Hola mundo.
import 'package:app_flutter_db/shared/nav.dart';
Una vez hecho esto, podemos ejecutar la app para visualizar los avances (En caso de que siga teniendo la app ejecutándose desde el inicio del proyecto, por favor detenerla y volverla a iniciar, debido a que se hizo el cambio de estado a la aplicación).
Ahora crearemos la página de bienvenida super básica. Iremos al directorio “lib” y crearemos una nueva carpeta llamada “pages” y dentro crearemos un archivo llamado “home_page.dart”.
Dentro del archivo home_page.dart empezaremos a definir la clase HomePage
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget{
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with WidgetsBindingObserver {
@override
Widget build(BuildContext context){
return Container(
color: Colors.red[300],
padding: EdgeInsets.symmetric(horizontal: 18),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.airport_shuttle,color:
Colors.white,size: 190,),
Text("Welcome",
style: TextStyle(
color: Colors.white,
fontSize: 60,
fontWeight:
FontWeight.bold
),
),
Text("Bienvenido a la app para visualizar la informacion de temperatura de los camiones que transportan las vacunas",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w300
)
,)
]),
);
}
}
En _HomePageState con la función build, crearemos el contenedor que va en el body del widget principal. En caso de error en el texto quitar los saltos de línea y dejarlo en una sola línea.
Widget _llamarPagina(int paginaActual) {
switch (paginaActual) {
default:
return HomePage();
}
}
Recordar importar el archivo home_page para no tener errores, guardamos y ahora se nos muestra un mensaje de bienvenida simple.
import 'package:app_flutter_db/pages/home_page.dart';
minSdkVersion 21
targetSdkVersion 31
sh-3.2# npm install -g firebase-tools
Nota: En caso de no tener instalado el gestor de paquetes npm puede ir al siguiente enlace para buscar otras alternativas de instalación: https://firebase.google.com/docs/cli#windows-npm
sh-3.2# firebase login
i Firebase optionally collects CLI and Emulator Suite usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.
? Allow Firebase to collect CLI and Emulator Suite usage and error reporting information? Yes
i To change your data collection preference at any time, run `firebase logout` and log in again.
Visit this URL on this device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&scope=email%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloudplatformprojects.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Ffirebase%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&response_type=code&state=301795975&redirect_uri=http%3A%2F%2Flocalhost%3A9005
Waiting for authentication...
✔ Success! Logged in as amst.investigacion@gmail.com
sh-3.2# dart pub global activate flutterfire_cli
adi@Adrianas-MacBook-Pro ~ % flutterfire configure
Nota: En caso de problemas con este comando agregar: C:\Users*username*\AppData\Local\Pub\Cache\bin a las variables del sistema y reiniciar el equipo.
adi@Adrianas-MacBook-Pro app_flutter_db % flutterfire configure
i Found 23 Firebase projects.
✔ Select a Firebase project to configure your Flutter application with · amst-flutter-db (AMST Flutter DB)
✔ Which platforms should your configuration support (use arrow keys & space to select)? · ios, macos, web, android
i Firebase android app com.example.app_flutter_db is not registered on Firebase project amst-flutter-db.
i Registered a new Firebase android app on Firebase project amst-flutter-db.
i Firebase ios app com.example.appFlutterDb is not registered on Firebase project amst-flutter-db.
i Registered a new Firebase ios app on Firebase project amst-flutter-db.
i Firebase macos app com.example.appFlutterDb.RunnerTests is not registered on Firebase project amst-flutter-db.
i Registered a new Firebase macos app on Firebase project amst-flutter-db.
i Firebase web app app_flutter_db (web) is not registered on Firebase project amst-flutter-db.
i Registered a new Firebase web app on Firebase project amst-flutter-db.
✔ The files android/build.gradle & android/app/build.gradle will be updated to apply Firebase configuration and gradle build plugins. Do you want to continue? · yes
Firebase configuration file lib/firebase_options.dart generated successfully with the following Firebase apps:
Platform Firebase App Id
web 1:1012422970186:web:80789bc9ffdb59b41b4fb3
android 1:1012422970186:android:55e09570ebc3aff71b4fb3
ios 1:1012422970186:ios:e09405689505518b1b4fb3
macos 1:1012422970186:ios:243d7c6a2c77968c1b4fb3
Learn more about using this file and next steps from the documentation:
> https://firebase.google.com/docs/flutter/setup
adi@Adrianas-MacBook-Pro app_flutter_db %
adi@Adrianas-MacBook-Pro app_flutter_db % flutter pub add firebase_core
Resolving dependencies...
collection 1.17.1 (1.18.0 available)
+ firebase_core 2.15.0
+ firebase_core_platform_interface 4.8.0
+ firebase_core_web 2.6.0
+ flutter_web_plugins 0.0.0 from sdk flutter
matcher 0.12.15 (0.12.16 available)
material_color_utilities 0.2.0 (0.8.0 available)
+ plugin_platform_interface 2.1.5
source_span 1.9.1 (1.10.0 available)
stack_trace 1.11.0 (1.11.1 available)
stream_channel 2.1.1 (2.1.2 available)
test_api 0.5.1 (0.6.1 available)
Changed 5 dependencies!
Podemos utilizar la terminal de Android studio.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
firebase_core: ^2.15.0
expandable: ^5.0.1
firebase_database: ^10.2.4
google_fonts: ^5.1.0
charts_flutter: ^0.12.0
Ejecutamos nuevamente el comando flutterfire configure para asegurar de que la configuración de Firebase de la app de Flutter esté actualizada. Colocamos la misma configuración que en los pasos anteriores.
Ahora vamos al archivo lib/main.dart, donde importamos el complemento principal de Firebase y el archivo de configuración que fue generado antes:
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
Reemplazar esta función:
void main() {
runApp(MyApp());
}
import 'dart:developer';
import 'package:firebase_database/ui/firebase_animated_list.dart';
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
class Data extends StatefulWidget {
@override
_DataState createState() => _DataState();
}
class _DataState extends State<Data> {
late final dataD;
late DatabaseReference _dataref;
@override
void initState() {
_dataref = FirebaseDatabase.instance.ref('data');
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Tab(
child: Text("Data from database"),
),
_crearListado(context),
]);
}
Widget _crearListado(BuildContext context) {
return Flexible(child: FirebaseAnimatedList(
query: _dataref,
itemBuilder: (BuildContext context, DataSnapshot snapshot,
Animation<double> animation, int index) {
var id = ((snapshot.value as dynamic)["end_device_ids"]["device_id"]).toString();
var humedad = ((snapshot.value as dynamic)["uplink_message"]["decoded_payload"]["humedad"]).toString();
var temperatura = ((snapshot.value as dynamic)["uplink_message"]["decoded_payload"]["temperatura"]).toString();
var fecha = ((snapshot.value as dynamic)["received_at"]).toString();
return Container(
margin: EdgeInsets.symmetric(vertical: 5,horizontal: 10),
decoration: BoxDecoration(
color: Colors.blueGrey[50],
borderRadius: BorderRadius.circular(5),
),
child: ListTile(
title: Text("ID: " + id,
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text("Humedad: "+humedad +"\n"
+ "Temperatura: " + temperatura
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Fecha: " + fecha.split("T")[0]),
Text("Hora: " + fecha.split(".")[0].split("T")[1]),
],
)
),
);
}
),
);
}
}
Dentro de esta clase inicializamos “_dataref” que es la referencia a la sección de datos que nos vamos a enfocar, en nuestro caso “data”, como podemos ver en la base de datos.
Luego tenemos una función build que construye la columna que se mostrara en pantalla, pero esta clase dentro de sus hijos llama a la función “_crearListado” que es la que realiza la magia. Dentro de esta función lo que hacemos es crear un widget de tipo Flexible, que tiene como hijo una FirebaseAdminList, este widget permite conectarse a tiempo real a la base de datos de firebase, y mostrar los cambios que se hagan a tiempo real. Dentro del itemBuilder de ese widget se ejecuta una función lambda que realiza la conexión hacia un query que es la referencia a la sección de la base, obteniendo los datos en una variable llamada snapshot desde donde saca la información como fecha, id, etc. Con esto generamos un container y alojamos la información de cómo queremos que se vea y automáticamente dart realizara el listado de todos los datos que reciba.
Widget _llamarPagina(int paginaActual) {
switch (paginaActual) {
case 0:
return Data();
default:
return HomePage();
}
}
Una vez realizado esto, guardar los archivos. Detener el proceso de debug y volver a ejecutar la app para que se compile la app con las nuevas dependencias. Al terminar si nos vamos a la pestaña de data veremos como ya obtiene los datos desde la base de datos.
Ahora crearemos la página del gráfico, crearemos un nuevo archivo “chart.dart” dentro de ./lib/pages/
Dentro añadiremos las siguientes líneas.
import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:firebase_database/firebase_database.dart';
class Chart extends StatefulWidget {
@override
_ChartState createState() => _ChartState();
}
class _ChartState extends State<Chart> {
late List<Registro> _data;
late List<charts.Series<Registro,String>> _chardata;
late DatabaseReference _dataref;
void _makeData(){
_chardata.add(
charts.Series(
domainFn: (Registro registro,_) =>
registro.fecha + " "+ registro.hora,
measureFn: (Registro registro,_) =>
registro.temperatura,
id: 'Registros',
data: _data,
)
);
}
@override
void initState(){
final FirebaseDatabase database = FirebaseDatabase.instance;
_dataref = database.reference().child("data");
_data = <Registro>[];
_chardata = <charts.Series<Registro, String>>[];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildBody(context),
);
}
Widget _buildBody(context){
return StreamBuilder(
stream: _dataref.onValue,
builder: (context,snapshot){
print(snapshot.data);
if(!snapshot.hasData){
return LinearProgressIndicator();
}else{
List<Registro> registros = <Registro>[];
Map data = (snapshot.data as dynamic).snapshot.value;
for(Map childata in data.values){
var fecha =
childata["received_at"].toString();
var temperatura =
childata["uplink_message"]["decoded_payload"]["temperatura"].toString();
var humedad =
childata["uplink_message"]["decoded_payload"]["humedad"].toString();
registros.add(Registro(fecha,temperatura,humedad
));
}
return _builChart(context,registros);
}
}
);
}
Widget _builChart(BuildContext context, List<Registro> registros){
_data = registros;
_makeData();
return Padding(
padding: EdgeInsets.all(8.0),
child: Container(
child: Center(
child: Column(
children: <Widget>[
Text("Diagrama de barras de temperaturas"),
SizedBox(height: 10.0),
Expanded(
child: charts.BarChart(_chardata,
animate: true,
animationDuration: Duration(seconds: 1),
vertical: false,
),
)
],
),
),
),
);
}
}
class Registro{
String fecha='';
String hora='';
double temperatura=0;
double humedad=0;
Registro(fecha,temperatura,humedad){
this.fecha = fecha.split("T")[0];
this.hora = fecha.split(".")[0].split("T")[1];
this.temperatura = double.parse(temperatura);
this.humedad = double.parse(humedad);
}
}
En esta clase definimos las listas donde se guardarán los datos, además de las referencias que son luego inicializadas. La función _makeData lo que hace es definir la configuración del gráfico y define como se usara la data que se entrega. Luego se define la función _buildBody que es llamada por la función _build para crear lo que se mostrara en el dispositivo. Se crea un widget tipo StreamBuilder, que desde información obtenida desde una base de datos genera lo que sea que le indiquemos. Recibimos los datos desde firebase y verificamos que lleguen correctamente, y con información para luego obtener cada dato y enviarlo a la siguiente función. _BuildChart lo que hace es ya indicar a dart como tiene que crear el chart en el dispositivo. Por último, agregamos una inner class que nos ayuda a manejar la información de los registros, y entregarlos ordenados al módulo de charts de flutter.
Widget _llamarPagina(int paginaActual) {
switch (paginaActual) {
case 0:
return Data();
case 2:
return Chart();
default:
return HomePage();
}
}
Nota: Les podría aparecer el siguiente error, pero no es motivo de preocupación por este laboratorio, puesto que viene directo de la librería que se está utilizando. Referirse a https://docs.flutter.dev/development/tools/sdk/release-notes/release-notes-3.0.0 para mas información.
Paso 5: Compilación de la aplicación
A continuación, realizaremos la compilación de la aplicación para los 2 principales sistemas móviles.
Android
Para compilar para Android, desde Android Studio en la parte superior encontraremos la opción Build, seleccionaremos esa opción, luego seleccionamos flutter y por último Build APK.
La apk la encontraremos en el directorio, ./build/app/outputs/flutter-apk/app-release.apk como se muestra a continuación:
IOS Para compilar nuestra aplicación para IOS necesitamos obligatoriamente tener una computadora macOS, debido a que necesitamos le programa xCode para poder compilarla. Esto es debido a que flutter usa los componentes nativos de IOS para compilar la aplicación.
Abrimos nuestro proyecto en Android Studio, desde ahí buscamos el siguiente archivo: ./ios/Runner/AppDelegate.swift
En la sección que dice “Team” deberemos agregar nuestra cuenta de desarrollador* (Probar con icloud).
*En caso de querer ver la aplicación en nuestro dispositivo iphone”: Una vez realizado esto, podemos conectar nuestro dispositivo iphone a nuestra macOS, en la parte superior debemos seleccionar nuestro dispositivo y luego darle en el botón de play.
Para compilar la aplicación: En la parte superior en la opción “product”, buscaremos “destinations”, y escogeremos en “any Ios Devices (arm64)”.
Iremos nuevamente a la opción “product” y escogemos la opción archive. Con eso se empezará a generar un archivo, esperaremos hasta que finalice.
Una vez finalizado nos mostrara el archivo en una ventana de archivos. A la derecha nos mostrara dos botones, escogeremos el que dice “Distribute App”.
Seleccionamos el método iOS App Store y presionamos en Next.
Seleccionamos el destino en Export y presionamos en Next.
En App Store distribution opios, dejamos igual y presionamos Next.
En Re-sign Runner, seleccionamos “Automatically manage signing” y presionamos next.
Por último, esperaremos y presionaremos el botón export.
Seleccionamos a donde quieren ser exportados nuestros archivos y damos en el botón Export.
Desafío
La práctica de laboratorio será desarrollado en el siguiente formato:
Nombre del archivo: AMST_Práctica de Laboratorio A_Grupo B_Apellido1_Apellido2_Apellido3