tutoriales.com

Flutter: Desarrollando un Sistema de Autenticación Completo con Firebase Auth y Provider

Este tutorial te guiará paso a paso en la implementación de un sistema de autenticación completo en Flutter. Utilizaremos Firebase Authentication para manejar usuarios y sesiones, y Provider para una gestión de estado limpia y reactiva. Cubriremos desde la configuración inicial hasta el manejo de diferentes estados de autenticación.

Intermedio20 min de lectura11 views
Reportar error

🎯 Introducción al Sistema de Autenticación con Firebase y Provider en Flutter

La autenticación es una piedra angular en la mayoría de las aplicaciones móviles modernas. Permite personalizar la experiencia del usuario, proteger datos sensibles y habilitar funcionalidades específicas para cada cuenta. En Flutter, tenemos varias opciones para implementar la autenticación, y una de las más populares y robustas es Firebase Authentication.

Firebase Auth ofrece una solución backend completa y escalable para gestionar usuarios, soportando múltiples métodos de inicio de sesión (correo/contraseña, Google, Facebook, etc.). Para integrar esto de manera eficiente en nuestra aplicación Flutter, utilizaremos Provider, un paquete de gestión de estado simple pero potente, que nos ayudará a mantener la interfaz de usuario sincronizada con el estado de autenticación de nuestro usuario.

Este tutorial te proporcionará las bases para construir un sistema de autenticación sólido, desde la configuración inicial del proyecto hasta la gestión de sesiones de usuario y la navegación condicional.

¿Por qué Firebase Authentication?

  • Facilidad de integración: SDKs bien documentados y fáciles de usar.
  • Escalabilidad: Gestionado por Google, escala automáticamente con tu base de usuarios.
  • Múltiples métodos de autenticación: Soporte para correo/contraseña, Google, Facebook, Apple, etc.
  • Seguridad: Maneja de forma segura las credenciales y sesiones de usuario.
  • Backend sin servidor: No necesitas preocuparte por la infraestructura del servidor.

¿Por qué Provider para la gestión de estado?

  • Simplicidad: Fácil de aprender y usar, ideal para principiantes y proyectos de tamaño mediano.
  • Reactividad: Reconstruye automáticamente los widgets cuando el estado cambia.
  • Arquitectura limpia: Fomenta la separación de preocupaciones y la modularidad.
  • Eficiencia: Solo reconstruye los widgets que dependen del estado cambiado.

🛠️ Configuración Inicial del Proyecto Flutter y Firebase

Antes de sumergirnos en el código, necesitamos configurar nuestro entorno y conectar Flutter con Firebase.

1. Crear un Nuevo Proyecto Flutter

Si aún no tienes un proyecto, créalo con el siguiente comando:

flutter create auth_app
cd auth_app

2. Configurar Firebase para tu Proyecto Flutter

Esto implica varios pasos:

Paso 1: Crear un proyecto Firebase: Ve a [Firebase Console](https://console.firebase.google.com/) y crea un nuevo proyecto.
Paso 2: Registrar tu aplicación Flutter: Dentro de tu proyecto Firebase, añade una nueva aplicación (Android y/o iOS). Sigue las instrucciones para añadir el `google-services.json` (Android) y `GoogleService-Info.plist` (iOS) a tu proyecto Flutter.
Paso 3: Instalar Firebase CLI: Si no lo tienes, instálalo globalmente: `npm install -g firebase-tools`.
Paso 4: Inicializar FlutterFire CLI: Abre una terminal en la raíz de tu proyecto Flutter y ejecuta `flutterfire configure`. Sigue las instrucciones para seleccionar tu proyecto Firebase y las plataformas que deseas configurar.
Paso 5: Habilitar Autenticación por Correo/Contraseña: En la consola de Firebase, ve a `Authentication` > `Sign-in method` y habilita `Email/Password`.
📌 Nota: Es crucial seguir los pasos de `flutterfire configure` con precisión, ya que esto generará automáticamente los archivos de configuración de Firebase necesarios (`firebase_options.dart`) y añadirá las dependencias correctas en tu `pubspec.yaml`.

3. Añadir Dependencias Necesarias

Edita tu archivo pubspec.yaml y añade las siguientes dependencias:

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.24.2
  firebase_auth: ^4.15.3
  provider: ^6.1.1

Después de añadir las dependencias, ejecuta flutter pub get para descargarlas.

4. Inicializar Firebase en tu Aplicación

Modifica tu archivo main.dart para inicializar Firebase antes de ejecutar la aplicación. Esto es esencial para que Firebase Auth funcione correctamente.

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:provider/provider.dart';
import 'firebase_options.dart'; // Generado por flutterfire configure

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Auth App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const AuthWrapper(), // Aquí manejaremos la lógica de redirección
    );
  }
}
Inicio WidgetsFlutterBinding.ensureInitialized() await Firebase.initializeApp() runApp(MyApp()) MyApp() AuthWrapper() Inicialización completada

🔑 Implementación del Servicio de Autenticación (AuthProvider)

Crearemos un ChangeNotifier que encapsulará toda la lógica de autenticación. Este AuthProvider será el corazón de nuestro sistema de autenticación.

Crea un nuevo archivo lib/services/auth_service.dart:

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';

class AuthService with ChangeNotifier {
  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
  User? _user;

  AuthService() {
    _firebaseAuth.authStateChanges().listen((User? user) {
      _user = user;
      notifyListeners(); // Notifica a los listeners cuando el estado del usuario cambia
    });
  }

  User? get user => _user;
  bool get isAuthenticated => _user != null;

  // Registro de usuario con correo y contraseña
  Future<String?> signUp(String email, String password) async {
    try {
      await _firebaseAuth.createUserWithEmailAndPassword(
          email: email, password: password);
      return null; // Éxito, no hay error
    } on FirebaseAuthException catch (e) {
      return e.message; // Retorna el mensaje de error de Firebase
    } catch (e) {
      return 'Ocurrió un error inesperado.';
    }
  }

  // Inicio de sesión de usuario con correo y contraseña
  Future<String?> signIn(String email, String password) async {
    try {
      await _firebaseAuth.signInWithEmailAndPassword(
          email: email, password: password);
      return null; // Éxito, no hay error
    } on FirebaseAuthException catch (e) {
      return e.message;
    } catch (e) {
      return 'Ocurrió un error inesperado.';
    }
  }

  // Cerrar sesión
  Future<void> signOut() async {
    await _firebaseAuth.signOut();
  }
}
💡 Consejo: Usar `with ChangeNotifier` es la forma estándar de hacer que una clase sea un proveedor de estado para el paquete `provider`. El método `notifyListeners()` es crucial para que los widgets que escuchan a este proveedor se reconstruyan cuando cambia el estado interno.

🖼️ Creando las Pantallas de Autenticación y Home

Necesitaremos una pantalla para el inicio de sesión/registro y otra para el contenido principal de la aplicación (Home), a la que solo se podrá acceder si el usuario está autenticado.

1. Pantalla de Inicio de Sesión/Registro (login_screen.dart)

Crea lib/screens/login_screen.dart.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  bool _isLogin = true; // true para login, false para registro

  void _authenticate(BuildContext context) async {
    final authService = Provider.of<AuthService>(context, listen: false);
    String? errorMessage;

    if (_isLogin) {
      errorMessage = await authService.signIn(
          _emailController.text, _passwordController.text);
    } else {
      errorMessage = await authService.signUp(
          _emailController.text, _passwordController.text);
    }

    if (errorMessage != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(errorMessage)),
      );
    } else {
      // Opcional: Mostrar un mensaje de éxito si el registro fue exitoso
      if (!_isLogin) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Registro exitoso. ¡Bienvenido!')), 
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_isLogin ? 'Iniciar Sesión' : 'Registrarse'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: 'Correo Electrónico',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.emailAddress,
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(
                labelText: 'Contraseña',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => _authenticate(context),
              child: Text(_isLogin ? 'Iniciar Sesión' : 'Registrarse'),
            ),
            const SizedBox(height: 12),
            TextButton(
              onPressed: () {
                setState(() {
                  _isLogin = !_isLogin;
                });
              },
              child: Text(
                _isLogin
                    ? '¿No tienes una cuenta? Regístrate'
                    : '¿Ya tienes una cuenta? Inicia Sesión',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

2. Pantalla Principal (home_screen.dart)

Crea lib/screens/home_screen.dart.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final authService = Provider.of<AuthService>(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Bienvenido'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await authService.signOut();
            },
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '¡Hola, ${authService.user?.email ?? 'Usuario'}!',
              style: const TextStyle(fontSize: 24),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 20),
            const Text(
              'Has iniciado sesión correctamente.',
              style: TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: () {
                // Ejemplo de acción en la pantalla principal
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('¡Acción realizada!'))
                );
              },
              child: const Text('Realizar Acción Importante'),
            ),
          ],
        ),
      ),
    );
  }
}

🔄 Envoltorio de Autenticación (AuthWrapper)

Ahora, necesitamos una forma de decidir qué pantalla mostrar al usuario, basándonos en su estado de autenticación. Aquí es donde AuthWrapper y Provider brillan.

Modifica main.dart para añadir AuthWrapper.

// ... (imports existentes)
import 'package:auth_app/screens/home_screen.dart';
import 'package:auth_app/screens/login_screen.dart';
import 'package:auth_app/services/auth_service.dart'; // Asegúrate de importar tu servicio

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => AuthService()),
      ],
      child: MaterialApp(
        title: 'Auth App',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const AuthWrapper(), // Nuestra lógica de redirección
      ),
    );
  }
}

class AuthWrapper extends StatelessWidget {
  const AuthWrapper({super.key});

  @override
  Widget build(BuildContext context) {
    final authService = Provider.of<AuthService>(context);

    if (authService.isAuthenticated) {
      return const HomeScreen();
    } else {
      return const LoginScreen();
    }
  }
}
🔥 Importante: `MultiProvider` es utilizado para registrar `AuthService` en el árbol de widgets. Cualquier widget debajo de `MultiProvider` puede acceder a una instancia de `AuthService` usando `Provider.of(context)`. `AuthWrapper` escucha los cambios en `AuthService` y reconstruye su UI para mostrar `HomeScreen` o `LoginScreen` según el estado de autenticación.
Inicio AuthWrapper ¿Usuario Autenticado? No HomeScreen LoginScreen Fin

✨ Gestión de Errores y Experiencia de Usuario

Hemos implementado la gestión básica de errores mostrando un SnackBar. Aquí hay algunas mejoras adicionales que puedes considerar para una mejor UX:

1. Indicadores de Carga

Durante el proceso de inicio de sesión o registro, puede haber un pequeño retraso mientras se comunica con Firebase. Es buena práctica mostrar un indicador de carga.

Modifica _LoginScreenState en login_screen.dart:

// ... dentro de _LoginScreenState
bool _isLoading = false; // Nuevo estado

void _authenticate(BuildContext context) async {
  setState(() {
    _isLoading = true; // Iniciar carga
  });

  final authService = Provider.of<AuthService>(context, listen: false);
  String? errorMessage;

  if (_isLogin) {
    errorMessage = await authService.signIn(
        _emailController.text, _passwordController.text);
  } else {
    errorMessage = await authService.signUp(
        _emailController.text, _passwordController.text);
  }

  setState(() {
    _isLoading = false; // Finalizar carga
  });

  if (errorMessage != null) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(errorMessage)),
    );
  } else {
    if (!_isLogin) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Registro exitoso. ¡Bienvenido!')), 
      );
    }
  }
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(_isLogin ? 'Iniciar Sesión' : 'Registrarse'),
    ),
    body: Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextField(
            controller: _emailController,
            decoration: const InputDecoration(
              labelText: 'Correo Electrónico',
              border: OutlineInputBorder(),
            ),
            keyboardType: TextInputType.emailAddress,
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _passwordController,
            decoration: const InputDecoration(
              labelText: 'Contraseña',
              border: OutlineInputBorder(),
            ),
            obscureText: true,
          ),
          const SizedBox(height: 24),
          _isLoading // Mostrar CircularProgressIndicator si está cargando
              ? const CircularProgressIndicator()
              : ElevatedButton(
                  onPressed: () => _authenticate(context),
                  child: Text(_isLogin ? 'Iniciar Sesión' : 'Registrarse'),
                ),
          const SizedBox(height: 12),
          TextButton(
            onPressed: _isLoading ? null : () {
              setState(() {
                _isLogin = !_isLogin;
              });
            },
            child: Text(
              _isLogin
                  ? '¿No tienes una cuenta? Regístrate'
                  : '¿Ya tienes una cuenta? Inicia Sesión',
            ),
          ),
        ],
      ),
    ),
  );
}

2. Validaciones de Formulario

Es fundamental validar la entrada del usuario antes de enviarla a Firebase (ej. formato de correo, longitud de contraseña).

Ejemplo de Validación de Formulario (Expandir)

Para implementar validación, puedes envolver tus TextFields en un TextFormField y el Column en un Form widget. Luego, usar un GlobalKey<FormState>.

// ... dentro de _LoginScreenState
final _formKey = GlobalKey<FormState>(); // Añadir esta línea

// ... modificar el método _authenticate
void _authenticate(BuildContext context) async {
  if (!_formKey.currentState!.validate()) { // Validar el formulario
    return; // Si la validación falla, no continuar
  }
  // ... (el resto del código de autenticación permanece igual)
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(_isLogin ? 'Iniciar Sesión' : 'Registrarse'),
    ),
    body: Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form( // Envolver el Column en un Form
        key: _formKey, // Asignar la clave al Form
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextFormField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: 'Correo Electrónico',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.emailAddress,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Por favor, introduce tu correo';
                } else if (!value.contains('@')) {
                  return 'Introduce un correo válido';
                }
                return null;
              },
            ),
            const SizedBox(height: 16),
            TextFormField(
              controller: _passwordController,
              decoration: const InputDecoration(
                labelText: 'Contraseña',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Por favor, introduce tu contraseña';
                } else if (value.length < 6) {
                  return 'La contraseña debe tener al menos 6 caracteres';
                }
                return null;
              },
            ),
            const SizedBox(height: 24),
            // ... (resto del código igual)
          ],
        ),
      ),
    ),
  );
}

3. Recordar Sesión (Persistencia)

Firebase Auth maneja la persistencia de la sesión por defecto. Una vez que un usuario inicia sesión, Firebase lo mantiene autenticado incluso si la aplicación se cierra y se vuelve a abrir. Esto se gestiona automáticamente por _firebaseAuth.authStateChanges(), que notificará a tu AuthService el estado actual del usuario al inicio de la aplicación.


🚀 Probando la Aplicación

  1. Ejecuta la aplicación: flutter run
  2. Registro: En la pantalla de inicio de sesión, cambia a la vista de registro (botón "¿No tienes una cuenta? Regístrate"). Introduce un correo electrónico y una contraseña válidos y regístrate. Deberías ser redirigido a la HomeScreen.
  3. Verifica en Firebase: Visita la consola de Firebase (Authentication > Users) para ver el nuevo usuario registrado.
  4. Cerrar Sesión: Haz clic en el icono de cerrar sesión en la AppBar de la HomeScreen. Deberías volver a la LoginScreen.
  5. Iniciar Sesión: Vuelve a la LoginScreen, introduce las credenciales del usuario que registraste e inicia sesión. Deberías ser redirigido a la HomeScreen.
  6. Reiniciar la aplicación: Cierra la aplicación (o haz un hot restart) y ábrela de nuevo. Si el usuario estaba logueado, deberías ver la HomeScreen directamente, demostrando la persistencia de la sesión.
Tutorial Completo

📝 Resumen y Próximos Pasos

Has aprendido a construir un sistema de autenticación completo en Flutter utilizando Firebase Authentication para el backend y Provider para una gestión de estado reactiva. Cubrimos:

  • Configuración de un proyecto Flutter con Firebase.
  • Implementación de un AuthService para manejar el registro, inicio de sesión y cierre de sesión.
  • Creación de pantallas de Login y Home.
  • Uso de AuthWrapper para la navegación condicional basada en el estado de autenticación.
  • Mejoras en la experiencia de usuario con indicadores de carga.

Este es un punto de partida sólido. Puedes expandir este sistema añadiendo:

  • Otros métodos de inicio de sesión: Google Sign-In, Facebook Login, Apple Sign-In.
  • Restablecimiento de contraseña: Funcionalidad para que los usuarios puedan recuperar sus contraseñas.
  • Verificación de correo electrónico: Asegurarte de que los usuarios usen un correo electrónico válido.
  • Perfil de usuario: Almacenar datos adicionales del usuario en Firestore o Realtime Database.
  • Roles de usuario: Implementar diferentes niveles de acceso.

La autenticación es un componente crítico, y dominarla te abre un mundo de posibilidades para crear aplicaciones Flutter dinámicas y seguras. ¡Sigue experimentando y construyendo!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!

Flutter: Desarrollando un Sistema de Autenticación Completo con Firebase Auth y Provider | tutoriales.com