Flutter Tutorial: Cómo crear tu primera aplicación Flutter

Publicado: 2022-03-11

¿Qué es Flutter?

Flutter es el SDK de desarrollo de aplicaciones móviles de Google que permite que su producto se dirija a las plataformas Android e iOS simultáneamente, sin la necesidad de mantener dos bases de código separadas. Además, las aplicaciones que usan Flutter también se pueden compilar para apuntar al próximo sistema operativo Fuchsia de Google.

Flutter recientemente alcanzó un hito importante: la versión estable 1.0. El lanzamiento tuvo lugar en Londres, el 5 de diciembre de 2018, en el evento Flutter Live. Si bien aún se puede considerar como una empresa de software temprana y en evolución, este artículo se centrará en un concepto ya probado y demostrará cómo desarrollar una aplicación de mensajería completamente funcional que se dirija a las dos principales plataformas móviles que usan Flutter 1.2 y Firebase.

Como se puede ver en el gráfico a continuación, Flutter ha ganado muchos usuarios en los últimos meses. En 2018, la participación de mercado de Flutter se duplicó y está en camino de superar a React Native en términos de consultas de búsqueda, de ahí nuestra decisión de crear un nuevo tutorial de Flutter.

Gráfico que compara los usuarios de Flutter y React de julio a septiembre de 2018.

Nota: este artículo se enfoca solo en ciertas partes de la implementación. La referencia completa del código fuente del proyecto se puede encontrar en este repositorio de GitHub.

requisitos previos

Aunque se ha hecho un esfuerzo para permitir que los lectores sigan y realicen este proyecto, incluso si es su primer intento de desarrollo móvil, se mencionan y utilizan muchos conceptos básicos de desarrollo móvil que no son específicos de Flutter sin una explicación detallada.

Esto se ha realizado por brevedad del artículo, ya que uno de sus objetivos es que el lector complete el proyecto en una sola sesión. Finalmente, el artículo asume que ya tiene configurado su entorno de desarrollo, incluidos los complementos necesarios de Android Studio y el SDK de Flutter.

Configuración de base de fuego

Configurar Firebase es lo único que tenemos que hacer de forma independiente para cada plataforma. En primer lugar, asegúrese de crear un nuevo proyecto en Firebase Dashboard y agregue aplicaciones de Android e iOS en el espacio de trabajo recién generado. La plataforma producirá dos archivos de configuración que debe descargar: google-services.json para Android y GoogleService-Info.plist para iOS. Antes de cerrar el panel, asegúrese de habilitar los proveedores de autenticación de Firebase y Google, ya que los usaremos para la identificación del usuario. Para hacer esto, elija el elemento Autenticación del menú y luego seleccione la pestaña Método de inicio de sesión.

Ahora puede cerrar el tablero mientras el resto de la configuración se lleva a cabo en nuestra base de código. En primer lugar, debemos colocar los archivos que descargamos en nuestro proyecto. El archivo google-services.json debe colocarse en la $(FLUTTER_PROJECT_ROOT)/android/app y GoogleService-Info.plist debe colocarse en el $(FLUTTER_PROJECT_ROOT)/ios/Runner . A continuación, debemos configurar las bibliotecas de Firebase que usaremos en el proyecto y conectarlas con los archivos de configuración. Esto se hace especificando los paquetes Dart (bibliotecas) que usaremos en el archivo pubspec.yaml de nuestro proyecto. En la sección de dependencias del archivo, pegue el siguiente fragmento:

 flutter_bloc: shared_preferences: firebase_auth: cloud_firestore: google_sign_in: flutter_facebook_login:

Los dos primeros no están relacionados con Firebase, pero se usarán con frecuencia en el proyecto. Los dos últimos son, con suerte, autoexplicativos.

Finalmente, debemos configurar los ajustes del proyecto específicos de la plataforma que permitirán que nuestro flujo de autenticación se complete con éxito. En el lado de Android, debemos agregar el complemento Gradle de servicios de Google a nuestra configuración de Gradle a nivel de proyecto. En otras palabras, debemos agregar el siguiente elemento a la lista de dependencias en el archivo $(FLUTTER_PROJECT_ROOT)/android/build.gradle :

 classpath 'com.google.gms:google-services:4.2.0' // change 4.2.0 to the latest version

Luego, debemos aplicar ese complemento agregando esta línea al final de $(FLUTTER_PROJECT_ROOT)/android/app/build.gradle :

 apply plugin: 'com.google.gms.google-services'

Lo último para esta plataforma es dar de alta los parámetros de su aplicación de Facebook. Lo que estamos buscando aquí es editar estos dos archivos: $(FLUTTER_PROJECT_ROOT)/android/app/src/main/AndroidManifest.xml y $(FLUTTER_PROJECT_ROOT)/android/app/src/main/res/values/strings.xml :

 <!-- AndroidManifest.xml --> <manifest xmlns:androcom.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/> <activity android:name="com.facebook.FacebookActivity" android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:label="@string/app_name" /> <activity android:name="com.facebook.CustomTabActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="@string/fb_login_protocol_scheme" /> </intent-filter> </activity> <!-- … --> </application> </manifest> <!-- strings.xml --> <resources> <string name="app_name">Toptal Chat</string> <string name="facebook_app_id">${YOUR_FACEBOOK_APP_ID}</string> <string name="fb_login_protocol_scheme">${YOUR_FACEBOOK_URL}</string> </resources>

Ahora es el momento de iOS. Afortunadamente, solo necesitamos cambiar un archivo en este caso. Agregue los siguientes valores (tenga en cuenta que es posible que el elemento CFBundleURLTypes ya exista en la lista; en ese caso, debe agregar estos elementos a la matriz existente en lugar de declararlo nuevamente) a $(FLUTTER_PROJECT)ROOT/ios/Runner/Info.plist expediente:

 <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_FACEBOOK_URL}</string> </array> </dict> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>${YOUR_REVERSED_GOOGLE_WEB_CLIENT_ID}</string> </array> </dict> </array> <key>FacebookAppID</key> <string>${YOUR_FACEBOOK_APP_ID}</string> <key>FacebookDisplayName</key> <string>${YOUR_FACEBOOK_APP_NAME}</string> <key>LSApplicationQueriesSchemes</key> <array> <string>fbapi</string> <string>fb-messenger-share-api</string> <string>fbauth2</string> <string>fbshareextension</string> </array>

Una palabra sobre la arquitectura BLoC

Este estándar de arquitectura se describió en uno de nuestros artículos anteriores, demostrando el uso de BLoC para compartir código en Flutter y AngularDart, por lo que no lo explicaremos en detalle aquí.

La idea básica detrás de la idea principal es que cada pantalla tiene las siguientes clases: - vista - que es responsable de mostrar el estado actual y delegar la entrada del usuario como eventos para bloquear. - estado - que representa datos "en vivo" con los que el usuario interactúa usando la vista actual. - bloque : que responde a eventos y actualiza el estado en consecuencia, solicitando opcionalmente datos de uno o varios repositorios locales o remotos. - evento - que es el resultado de una acción definitiva que puede o no cambiar el estado actual.

Como representación gráfica, se puede pensar así:

Flutter Tutorial: Representación gráfica de la arquitectura BLoC.

Además, tenemos un directorio modelo que contiene clases de datos y repositorios que producen instancias de estas clases.

Desarrollo de interfaz de usuario

La creación de la interfaz de usuario con Flutter se realiza completamente en Dart, a diferencia del desarrollo de aplicaciones nativas en Android e iOS, donde la interfaz de usuario se crea con el esquema XML y está completamente separada del código base de la lógica empresarial. Vamos a utilizar composiciones de elementos de interfaz de usuario relativamente simples con diferentes componentes basados ​​en el estado actual (por ejemplo, isLoading, isEmpty parámetros). La interfaz de usuario en Flutter gira en torno a los widgets, o más bien al árbol de widgets. Los widgets pueden ser sin estado o con estado. Cuando se trata de los con estado, es importante enfatizar que, cuando se llama a setState() en un widget en particular que se muestra actualmente (llamarlo en el constructor o después de que se elimine da como resultado un error de tiempo de ejecución), un pase de compilación y dibujo es programado para ser realizado en el próximo ciclo de dibujo.

Para abreviar, solo mostraremos una de las clases de UI (vista) aquí:

 class LoginScreen extends StatefulWidget { LoginScreen({Key key}) : super(key: key); @override State<StatefulWidget> createState() => _LoginState(); } class _LoginState extends State<LoginScreen> { final _bloc = LoginBloc(); @override Widget build(BuildContext context) { return BlocProvider<LoginBloc>( bloc: _bloc, child: LoginWidget(widget: widget, widgetState: this) ); } @override void dispose() { _bloc.dispose(); super.dispose(); } } class LoginWidget extends StatelessWidget { const LoginWidget({Key key, @required this.widget, @required this.widgetState}) : super(key: key); final LoginScreen widget; final _LoginState widgetState; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Login"), ), body: BlocBuilder( bloc: BlocProvider.of<LoginBloc>(context), builder: (context, LoginState state) { if (state.loading) { return Center( child: CircularProgressIndicator(strokeWidth: 4.0) ); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginGoogle(this), child: Text( "Login with Google", style: TextStyle(color: Colors.white), ), color: Colors.redAccent, ), ), ButtonTheme( minWidth: 256.0, height: 32.0, child: RaisedButton( onPressed: () => BlocProvider.of<LoginBloc>(context).onLoginFacebook(this), child: Text( "Login with Facebook", style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ), ), ], ), ); } }), ); } void navigateToMain() { NavigationHelper.navigateToMain(widgetState.context); } }

El resto de las clases de la interfaz de usuario siguen los mismos patrones, pero tal vez tengan acciones diferentes y pueden presentar un árbol de widgets de estado vacío además del estado de carga.

Autenticación

Como habrás adivinado, usaremos las bibliotecas google_sign_in y flutter_facebook_login para autenticar al usuario confiando en su perfil de red social. En primer lugar, asegúrese de importar estos paquetes al archivo que manejará la lógica de solicitud de inicio de sesión:

 import 'package:flutter_facebook_login/flutter_facebook_login.dart'; import 'package:google_sign_in/google_sign_in.dart';

Ahora, vamos a tener dos partes independientes que se encargarán de nuestro flujo de autenticación. El primero va a iniciar una solicitud de inicio de sesión de Facebook o Google:

 void onLoginGoogle(LoginWidget view) async { dispatch(LoginEventInProgress()); final googleSignInRepo = GoogleSignIn(signInOption: SignInOption.standard, scopes: ["profile", "email"]); final account = await googleSignInRepo.signIn(); if (account != null) { LoginRepo.getInstance().signInWithGoogle(account); } else { dispatch(LogoutEvent()); } } void onLoginFacebook(LoginWidget view) async { dispatch(LoginEventInProgress()); final facebookSignInRepo = FacebookLogin(); final signInResult = await facebookSignInRepo.logInWithReadPermissions(["email"]); if (signInResult.status == FacebookLoginStatus.loggedIn) { LoginRepo.getInstance().signInWithFacebook(signInResult); } else if (signInResult.status == FacebookLoginStatus.cancelledByUser) { dispatch(LogoutEvent()); } else { dispatch(LoginErrorEvent(signInResult.errorMessage)); } }

El segundo se llamará cuando obtengamos los datos del perfil de cualquiera de los proveedores. Vamos a lograr esto instruyendo a nuestro controlador de inicio de sesión para escuchar firebase_auth onAuthStateChange stream:

 void _setupAuthStateListener(LoginWidget view) { if (_authStateListener == null) { _authStateListener = FirebaseAuth.instance.onAuthStateChanged.listen((user) { if (user != null) { final loginProvider = user.providerId; UserRepo.getInstance().setCurrentUser(User.fromFirebaseUser(user)); if (loginProvider == "google") { // TODO analytics call for google login provider } else { // TODO analytics call for facebook login provider } view.navigateToMain(); } else { dispatch(LogoutEvent()); } }, onError: (error) { dispatch(LoginErrorEvent(error)); }); } }

La implementación de UserRepo y LoginRepo no se publicará aquí, pero no dude en consultar el repositorio de GitHub para obtener una referencia completa.

Tutorial de Flutter: Cómo crear una aplicación de mensajería instantánea

Finalmente, llegamos a la parte interesante. Como su nombre lo indica, los mensajes deben intercambiarse lo más rápido posible, idealmente, esto debe ser instantáneo . Afortunadamente, cloud_firestore nos permite interactuar con la instancia de Firestore y podemos usar su función de snapshots() para abrir un flujo de datos que nos brindará actualizaciones en tiempo real. En mi opinión, todo el código chat_repo es bastante sencillo con la excepción del método startChatroomForUsers . Es responsable de crear una nueva sala de chat para dos usuarios, a menos que haya una existente que contenga a ambos usuarios (ya que no queremos tener varias instancias del mismo par de usuarios), en cuyo caso devuelve la sala de chat existente.

Sin embargo, debido al diseño de Firestore, actualmente no admite consultas anidadas array-contains . Por lo tanto, no podemos recuperar el flujo de datos apropiado, pero necesitamos realizar un filtrado adicional de nuestro lado. Esa solución consiste en recuperar todas las salas de chat del usuario que ha iniciado sesión y luego buscar la que también contiene al usuario seleccionado:

 Future<SelectedChatroom> startChatroomForUsers(List<User> users) async { DocumentReference userRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[1].uid); QuerySnapshot queryResults = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .where("participants", arrayContains: userRef) .getDocuments(); DocumentReference otherUserRef = _firestore .collection(FirestorePaths.USERS_COLLECTION) .document(users[0].uid); DocumentSnapshot roomSnapshot = queryResults.documents.firstWhere((room) { return room.data["participants"].contains(otherUserRef); }, orElse: () => null); if (roomSnapshot != null) { return SelectedChatroom(roomSnapshot.documentID, users[0].displayName); } else { Map<String, dynamic> chatroomMap = Map<String, dynamic>(); chatroomMap["messages"] = List<String>(0); List<DocumentReference> participants = List<DocumentReference>(2); participants[0] = otherUserRef; participants[1] = userRef; chatroomMap["participants"] = participants; DocumentReference reference = await _firestore .collection(FirestorePaths.CHATROOMS_COLLECTION) .add(chatroomMap); DocumentSnapshot chatroomSnapshot = await reference.get(); return SelectedChatroom(chatroomSnapshot.documentID, users[0].displayName); } }

Además, debido a restricciones de diseño similares, Firebase actualmente no admite actualizaciones de matrices (inserción de un elemento nuevo en un valor de campo de matriz existente) con un valor FieldValue.serverTimestamp() especial.

Este valor le indica a la plataforma que el campo que contiene esto en lugar de un valor real debe completarse con la marca de tiempo real en el servidor en el momento en que se realiza la transacción. En su lugar, usamos DateTime.now() en el momento en que creamos nuestro nuevo objeto serializado de mensajes e insertamos ese objeto en la colección de mensajes de la sala de chat.

 Future<bool> sendMessageToChatroom(String chatroomId, User user, String message) async { try { DocumentReference authorRef = _firestore.collection(FirestorePaths.USERS_COLLECTION).document(user.uid); DocumentReference chatroomRef = _firestore.collection(FirestorePaths.CHATROOMS_COLLECTION).document(chatroomId); Map<String, dynamic> serializedMessage = { "author" : authorRef, "timestamp" : DateTime.now(), "value" : message }; chatroomRef.updateData({ "messages" : FieldValue.arrayUnion([serializedMessage]) }); return true; } catch (e) { print(e.toString()); return false; } }

Terminando

Obviamente, la aplicación de mensajería Flutter que desarrollamos es más una prueba de concepto que una aplicación de mensajería instantánea lista para el mercado. Como ideas para un mayor desarrollo, se podría considerar la introducción de cifrado de extremo a extremo o contenido enriquecido (chats grupales, archivos adjuntos de medios, análisis de URL). Pero antes de todo eso, uno debe implementar las notificaciones automáticas, ya que son prácticamente una característica imprescindible para una aplicación de mensajería instantánea, y la hemos movido fuera del alcance de este artículo por razones de brevedad. Además, a Firestore todavía le faltan un par de funciones para tener consultas array-contains anidadas más simples y precisas.

Como se mencionó al comienzo del artículo, Flutter ha madurado recientemente a la versión estable 1.0 y seguirá creciendo, no solo en lo que respecta a las características y capacidades del marco, sino también en lo que respecta a la comunidad de desarrollo y las bibliotecas de terceros y recursos. Tiene sentido invertir su tiempo en familiarizarse con el desarrollo de la aplicación Flutter ahora, ya que claramente está aquí para quedarse y acelerar su proceso de desarrollo móvil.

Sin costo adicional, los desarrolladores de Flutter también estarán listos para apuntar al sistema operativo emergente de Google: Fuchsia.

Relacionado: The Dart Language: cuando Java y C# no son lo suficientemente nítidos