Firebase + Riverpod 3.0: A forma mais fácil de criar aplicações Flutter em 2026

E aí, amigo! 👋 Deixa eu adivinhar — você está construindo um app Flutter e precisa de um backend. Você ouviu falar que o Firebase é incrível, mas a configuração parece… complicada. Autenticação, Firestore, ouvintes em tempo real, gerenciamento de estado — é muita coisa para lidar.

Bem, eu tenho boas notícias. Com o Riverpod 3.0, a integração com o Firebase se torna ridiculamente simples. Estou falando de uma simplicidade do tipo ‘configure uma vez e esqueça’.

Hoje, vou te mostrar exatamente como construir uma estrutura de Firebase + Riverpod pronta para produção, que gerencia autenticação, operações no Firestore e dados em tempo real como um mestre. Sem dores de cabeça, sem confusão, apenas código limpo que funciona.

Vamos nessa! 🚀

Por que Firebase + Riverpod 3.0 é um “Casamento Perfeito”

Antes de começarmos a codar, deixe-me dizer por que esse combo é absolutamente matador:

O Firebase te oferece:

  • Autenticação pronta para uso (Google, e-mail, Apple, etc.)

  • Banco de dados em tempo real que sincroniza entre dispositivos instantaneamente

  • Armazenamento em nuvem (Cloud Storage) para arquivos e imagens

  • Notificações push prontas para enviar

  • Análises e relatórios de erros integrados

O Riverpod 3.0 te oferece:

  • Separação clara de responsabilidades

  • Cache automático e gerenciamento de estado

  • Facilidade para testes e simulações (mocking)

  • Providers tipados que capturam bugs em tempo de compilação

Juntos? Eles são imbatíveis. Vamos construir algo incrível.

Passo 1: Configuração do Firebase (A parte chata, mas seremos rápidos)

Primeiro, vamos configurar o Firebase. Prometo que esta é a única parte entediante.

Instale o FlutterFire CLI:

dart pub global activate flutterfire_cli

Configure o Firebase para o seu projeto:

flutterfire configure

Isso guiará você na criação de um projeto Firebase e na geração automática dos arquivos de configuração. Escolha suas plataformas (iOS, Android, Web) e deixe-o fazer a mágica.

Adicione as dependências ao seu pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  
  # State Management
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5
  
  # Firebase
  firebase_core: ^2.24.2
  firebase_auth: ^4.16.0
  cloud_firestore: ^4.14.0
  firebase_storage: ^11.6.0
  
  # Code Generation
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1

dev_dependencies:
  # Code Generation
  build_runner: ^2.4.8
  riverpod_generator: ^2.3.10
  freezed: ^2.4.6
  json_serializable: ^6.7.1

Execute flutter pub get e você estará pronto!

Passo 2: Inicialize o Firebase no seu App

Vamos configurar a inicialização do Firebase do jeito certo:

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase + Riverpod',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const AuthWrapper(),
    );
  }
}

Passo 3: Autenticação Firebase com Riverpod (A diversão começa!)

Agora, vamos criar um sistema de autenticação elegante. É aqui que o Riverpod realmente brilha.

Modelo de Usuário (User Model):

// lib/models/app_user.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'app_user.freezed.dart';
part 'app_user.g.dart';

@freezed
class AppUser with _$AppUser {
  const factory AppUser({
    required String uid,
    required String email,
    String? displayName,
    String? photoUrl,
  }) = _AppUser;

  factory AppUser.fromJson(Map<String, dynamic> json) =>
      _$AppUserFromJson(json);
}

Serviço de Autenticação Firebase (Firebase Auth Service):

// lib/services/firebase_auth_service.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/app_user.dart';

part 'firebase_auth_service.g.dart';

@riverpod
FirebaseAuth firebaseAuth(FirebaseAuthRef ref) {
  return FirebaseAuth.instance;
}

@riverpod
class FirebaseAuthService extends _$FirebaseAuthService {
  @override
  FutureOr<void> build() {
    // Initialize if needed
  }

  Future<AppUser> signInWithEmail(String email, String password) async {
    try {
      final userCredential = await ref.read(firebaseAuthProvider).signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      
      return _userFromFirebase(userCredential.user!);
    } on FirebaseAuthException catch (e) {
      throw _handleAuthException(e);
    }
  }

  Future<AppUser> signUpWithEmail(String email, String password, String displayName) async {
    try {
      final userCredential = await ref.read(firebaseAuthProvider).createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      
      // Update display name
      await userCredential.user?.updateDisplayName(displayName);
      
      return _userFromFirebase(userCredential.user!);
    } on FirebaseAuthException catch (e) {
      throw _handleAuthException(e);
    }
  }

  Future<void> signOut() async {
    await ref.read(firebaseAuthProvider).signOut();
  }

  Future<void> resetPassword(String email) async {
    try {
      await ref.read(firebaseAuthProvider).sendPasswordResetEmail(email: email);
    } on FirebaseAuthException catch (e) {
      throw _handleAuthException(e);
    }
  }

  AppUser _userFromFirebase(User user) {
    return AppUser(
      uid: user.uid,
      email: user.email!,
      displayName: user.displayName,
      photoUrl: user.photoURL,
    );
  }

  String _handleAuthException(FirebaseAuthException e) {
    switch (e.code) {
      case 'user-not-found':
        return 'No user found with this email.';
      case 'wrong-password':
        return 'Wrong password provided.';
      case 'email-already-in-use':
        return 'An account already exists with this email.';
      case 'invalid-email':
        return 'The email address is invalid.';
      case 'weak-password':
        return 'The password is too weak.';
      default:
        return 'An error occurred. Please try again.';
    }
  }
}

Provider de Estado de Autenticação (A Mágica!):

Este provider rastreia automaticamente o estado da autenticação:

// lib/providers/auth_provider.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/app_user.dart';
import '../services/firebase_auth_service.dart';

part 'auth_provider.g.dart';

// Stream provider that listens to auth state changes
@riverpod
Stream<User?> authStateChanges(AuthStateChangesRef ref) {
  return ref.watch(firebaseAuthProvider).authStateChanges();
}

// Current user provider
@riverpod
Stream<AppUser?> currentUser(CurrentUserRef ref) {
  return ref.watch(authStateChangesProvider).when(
    data: (user) async* {
      if (user != null) {
        yield AppUser(
          uid: user.uid,
          email: user.email!,
          displayName: user.displayName,
          photoUrl: user.photoURL,
        );
      } else {
        yield null;
      }
    },
    loading: () => const Stream.empty(),
    error: (_, __) => const Stream.empty(),
  );
}

Login Screen:

// lib/screens/login_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/firebase_auth_service.dart';

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

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

class _LoginScreenState extends ConsumerState<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  bool _isLoading = false;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _handleLogin() async {
    if (!(_formKey.currentState?.validate() ?? false)) return;

    setState(() => _isLoading = true);

    try {
      await ref.read(firebaseAuthServiceProvider.notifier).signInWithEmail(
        _emailController.text.trim(),
        _passwordController.text,
      );
      
      if (mounted) {
        // Navigation will be handled by AuthWrapper
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(e.toString()),
            backgroundColor: Colors.red,
          ),
        );
      }
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24.0),
            child: Form(
              key: _formKey,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Icon(
                    Icons.firebase,
                    size: 80,
                    color: Theme.of(context).primaryColor,
                  ),
                  const SizedBox(height: 24),
                  Text(
                    'Welcome Back!',
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Sign in to continue',
                    style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                      color: Colors.grey,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 48),
                  TextFormField(
                    controller: _emailController,
                    decoration: const InputDecoration(
                      labelText: 'Email',
                      border: OutlineInputBorder(),
                      prefixIcon: Icon(Icons.email_outlined),
                    ),
                    keyboardType: TextInputType.emailAddress,
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your email';
                      }
                      if (!value.contains('@')) {
                        return 'Please enter a valid email';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  TextFormField(
                    controller: _passwordController,
                    decoration: const InputDecoration(
                      labelText: 'Password',
                      border: OutlineInputBorder(),
                      prefixIcon: Icon(Icons.lock_outline),
                    ),
                    obscureText: true,
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your password';
                      }
                      if (value.length < 6) {
                        return 'Password must be at least 6 characters';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 24),
                  ElevatedButton(
                    onPressed: _isLoading ? null : _handleLogin,
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(vertical: 16),
                    ),
                    child: _isLoading
                        ? const SizedBox(
                            height: 20,
                            width: 20,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          )
                        : const Text('Sign In'),
                  ),
                  const SizedBox(height: 16),
                  TextButton(
                    onPressed: () {
                      // Navigate to signup screen
                    },
                    child: const Text("Don't have an account? Sign up"),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Wrapper de Autenticação (Navegação Automática!):

Este widget exibe automaticamente a tela correta com base no estado de autenticação:

// lib/screens/auth_wrapper.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/auth_provider.dart';
import 'login_screen.dart';
import 'home_screen.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authStateChangesProvider);

    return authState.when(
      data: (user) {
        if (user != null) {
          return const HomeScreen();
        } else {
          return const LoginScreen();
        }
      },
      loading: () => const Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      ),
      error: (error, stack) => Scaffold(
        body: Center(
          child: Text('Error: $error'),
        ),
      ),
    );
  }
}

Passo 4: Integração com Firestore (Onde a coisa fica realmente legal)

Agora, vamos adicionar o Firestore para dados em tempo real. Criaremos um sistema simples de gerenciamento de tarefas.

Modelo de Tarefa (Task Model):

// lib/models/task.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

part 'task.freezed.dart';
part 'task.g.dart';

@freezed
class Task with _$Task {
  const factory Task({
    required String id,
    required String title,
    required String description,
    required bool isCompleted,
    required String userId,
    @JsonKey(fromJson: _timestampFromJson, toJson: _timestampToJson)
    required DateTime createdAt,
  }) = _Task;

  factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
}

DateTime _timestampFromJson(Timestamp timestamp) => timestamp.toDate();
Timestamp _timestampToJson(DateTime date) => Timestamp.fromDate(date);

Firestore Service:

// lib/services/firestore_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/task.dart';

part 'firestore_service.g.dart';

@riverpod
FirebaseFirestore firestore(FirestoreRef ref) {
  return FirebaseFirestore.instance;
}

@riverpod
class FirestoreService extends _$FirestoreService {
  @override
  FutureOr<void> build() {
    // Initialize if needed
  }

  // Create a new task
  Future<String> createTask({
    required String userId,
    required String title,
    required String description,
  }) async {
    final docRef = ref.read(firestoreProvider).collection('tasks').doc();
    
    final task = Task(
      id: docRef.id,
      title: title,
      description: description,
      isCompleted: false,
      userId: userId,
      createdAt: DateTime.now(),
    );

    await docRef.set(task.toJson());
    return docRef.id;
  }

  // Update task
  Future<void> updateTask(String taskId, Map<String, dynamic> data) async {
    await ref.read(firestoreProvider)
        .collection('tasks')
        .doc(taskId)
        .update(data);
  }

  // Delete task
  Future<void> deleteTask(String taskId) async {
    await ref.read(firestoreProvider)
        .collection('tasks')
        .doc(taskId)
        .delete();
  }

  // Toggle task completion
  Future<void> toggleTaskCompletion(String taskId, bool isCompleted) async {
    await updateTask(taskId, {'isCompleted': !isCompleted});
  }
}

Provider de Tarefas em Tempo Real (Isso é Mágica!):

Este provider atualiza automaticamente quando os dados do Firestore sofrem alterações:

// lib/providers/tasks_provider.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/task.dart';
import '../services/firestore_service.dart';
import 'auth_provider.dart';

part 'tasks_provider.g.dart';

// Stream provider for real-time tasks
@riverpod
Stream<List<Task>> userTasks(UserTasksRef ref) {
  final user = ref.watch(currentUserProvider).value;
  
  if (user == null) {
    return Stream.value([]);
  }

  return ref
      .watch(firestoreProvider)
      .collection('tasks')
      .where('userId', isEqualTo: user.uid)
      .orderBy('createdAt', descending: true)
      .snapshots()
      .map((snapshot) {
        return snapshot.docs.map((doc) {
          return Task.fromJson({...doc.data(), 'id': doc.id});
        }).toList();
      });
}

// Filtered tasks providers
@riverpod
Stream<List<Task>> activeTasks(ActiveTasksRef ref) {
  return ref.watch(userTasksProvider).when(
    data: (tasks) => Stream.value(
      tasks.where((task) => !task.isCompleted).toList(),
    ),
    loading: () => Stream.value([]),
    error: (_, __) => Stream.value([]),
  );
}

@riverpod
Stream<List<Task>> completedTasks(CompletedTasksRef ref) {
  return ref.watch(userTasksProvider).when(
    data: (tasks) => Stream.value(
      tasks.where((task) => task.isCompleted).toList(),
    ),
    loading: () => Stream.value([]),
    error: (_, __) => Stream.value([]),
  );
}

Login Screen:

// lib/screens/login_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/firebase_auth_service.dart';

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

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

class _LoginScreenState extends ConsumerState<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  bool _isLoading = false;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _handleLogin() async {
    if (!(_formKey.currentState?.validate() ?? false)) return;

    setState(() => _isLoading = true);

    try {
      await ref.read(firebaseAuthServiceProvider.notifier).signInWithEmail(
        _emailController.text.trim(),
        _passwordController.text,
      );
      
      if (mounted) {
        // Navigation will be handled by AuthWrapper
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(e.toString()),
            backgroundColor: Colors.red,
          ),
        );
      }
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24.0),
            child: Form(
              key: _formKey,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Icon(
                    Icons.firebase,
                    size: 80,
                    color: Theme.of(context).primaryColor,
                  ),
                  const SizedBox(height: 24),
                  Text(
                    'Welcome Back!',
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Sign in to continue',
                    style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                      color: Colors.grey,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(height: 48),
                  TextFormField(
                    controller: _emailController,
                    decoration: const InputDecoration(
                      labelText: 'Email',
                      border: OutlineInputBorder(),
                      prefixIcon: Icon(Icons.email_outlined),
                    ),
                    keyboardType: TextInputType.emailAddress,
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your email';
                      }
                      if (!value.contains('@')) {
                        return 'Please enter a valid email';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  TextFormField(
                    controller: _passwordController,
                    decoration: const InputDecoration(
                      labelText: 'Password',
                      border: OutlineInputBorder(),
                      prefixIcon: Icon(Icons.lock_outline),
                    ),
                    obscureText: true,
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Please enter your password';
                      }
                      if (value.length < 6) {
                        return 'Password must be at least 6 characters';
                      }
                      return null;
                    },
                  ),
                  const SizedBox(height: 24),
                  ElevatedButton(
                    onPressed: _isLoading ? null : _handleLogin,
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(vertical: 16),
                    ),
                    child: _isLoading
                        ? const SizedBox(
                            height: 20,
                            width: 20,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          )
                        : const Text('Sign In'),
                  ),
                  const SizedBox(height: 16),
                  TextButton(
                    onPressed: () {
                      // Navigate to signup screen
                    },
                    child: const Text("Don't have an account? Sign up"),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Wrapper de Autenticação (Navegação Automática!):

Este widget exibe automaticamente a tela correta com base no estado de autenticação:

// lib/screens/auth_wrapper.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/auth_provider.dart';
import 'login_screen.dart';
import 'home_screen.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(authStateChangesProvider);

    return authState.when(
      data: (user) {
        if (user != null) {
          return const HomeScreen();
        } else {
          return const LoginScreen();
        }
      },
      loading: () => const Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      ),
      error: (error, stack) => Scaffold(
        body: Center(
          child: Text('Error: $error'),
        ),
      ),
    );
  }
}

Passo 4: Integração com Firestore (Onde a coisa fica realmente legal)

Agora, vamos adicionar o Firestore para dados em tempo real. Criaremos um sistema simples de gerenciamento de tarefas.

Modelo de Tarefa (Task Model):

// lib/models/task.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

part 'task.freezed.dart';
part 'task.g.dart';

@freezed
class Task with _$Task {
  const factory Task({
    required String id,
    required String title,
    required String description,
    required bool isCompleted,
    required String userId,
    @JsonKey(fromJson: _timestampFromJson, toJson: _timestampToJson)
    required DateTime createdAt,
  }) = _Task;

  factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
}

DateTime _timestampFromJson(Timestamp timestamp) => timestamp.toDate();
Timestamp _timestampToJson(DateTime date) => Timestamp.fromDate(date);

Firestore Service:

// lib/services/firestore_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/task.dart';

part 'firestore_service.g.dart';

@riverpod
FirebaseFirestore firestore(FirestoreRef ref) {
  return FirebaseFirestore.instance;
}

@riverpod
class FirestoreService extends _$FirestoreService {
  @override
  FutureOr<void> build() {
    // Initialize if needed
  }

  // Create a new task
  Future<String> createTask({
    required String userId,
    required String title,
    required String description,
  }) async {
    final docRef = ref.read(firestoreProvider).collection('tasks').doc();
    
    final task = Task(
      id: docRef.id,
      title: title,
      description: description,
      isCompleted: false,
      userId: userId,
      createdAt: DateTime.now(),
    );

    await docRef.set(task.toJson());
    return docRef.id;
  }

  // Update task
  Future<void> updateTask(String taskId, Map<String, dynamic> data) async {
    await ref.read(firestoreProvider)
        .collection('tasks')
        .doc(taskId)
        .update(data);
  }

  // Delete task
  Future<void> deleteTask(String taskId) async {
    await ref.read(firestoreProvider)
        .collection('tasks')
        .doc(taskId)
        .delete();
  }

  // Toggle task completion
  Future<void> toggleTaskCompletion(String taskId, bool isCompleted) async {
    await updateTask(taskId, {'isCompleted': !isCompleted});
  }
}

Provider de Tarefas em Tempo Real (Isso é Mágica!):

Este provider atualiza automaticamente quando os dados do Firestore sofrem alterações:

// lib/providers/tasks_provider.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/task.dart';
import '../services/firestore_service.dart';
import 'auth_provider.dart';

part 'tasks_provider.g.dart';

// Stream provider for real-time tasks
@riverpod
Stream<List<Task>> userTasks(UserTasksRef ref) {
  final user = ref.watch(currentUserProvider).value;
  
  if (user == null) {
    return Stream.value([]);
  }

  return ref
      .watch(firestoreProvider)
      .collection('tasks')
      .where('userId', isEqualTo: user.uid)
      .orderBy('createdAt', descending: true)
      .snapshots()
      .map((snapshot) {
        return snapshot.docs.map((doc) {
          return Task.fromJson({...doc.data(), 'id': doc.id});
        }).toList();
      });
}

// Filtered tasks providers
@riverpod
Stream<List<Task>> activeTasks(ActiveTasksRef ref) {
  return ref.watch(userTasksProvider).when(
    data: (tasks) => Stream.value(
      tasks.where((task) => !task.isCompleted).toList(),
    ),
    loading: () => Stream.value([]),
    error: (_, __) => Stream.value([]),
  );
}

@riverpod
Stream<List<Task>> completedTasks(CompletedTasksRef ref) {
  return ref.watch(userTasksProvider).when(
    data: (tasks) => Stream.value(
      tasks.where((task) => task.isCompleted).toList(),
    ),
    loading: () => Stream.value([]),
    error: (_, __) => Stream.value([]),
  );
}

Tela Inicial com Tarefas em Tempo Real (Home Screen with Real-time Tasks):

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/auth_provider.dart';
import '../providers/tasks_provider.dart';
import '../services/firebase_auth_service.dart';
import '../services/firestore_service.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(currentUserProvider);
    final tasksAsync = ref.watch(userTasksProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Tasks'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await ref.read(firebaseAuthServiceProvider.notifier).signOut();
            },
          ),
        ],
      ),
      body: userAsync.when(
        data: (user) {
          if (user == null) return const SizedBox.shrink();

          return Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Text(
                  'Welcome, ${user.displayName ?? user.email}!',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
              ),
              Expanded(
                child: tasksAsync.when(
                  data: (tasks) {
                    if (tasks.isEmpty) {
                      return Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(
                              Icons.task_outlined,
                              size: 64,
                              color: Colors.grey[400],
                            ),
                            const SizedBox(height: 16),
                            Text(
                              'No tasks yet',
                              style: TextStyle(
                                fontSize: 18,
                                color: Colors.grey[600],
                              ),
                            ),
                            const SizedBox(height: 8),
                            Text(
                              'Tap the + button to create one',
                              style: TextStyle(
                                color: Colors.grey[500],
                              ),
                            ),
                          ],
                        ),
                      );
                    }

                    return ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: tasks.length,
                      itemBuilder: (context, index) {
                        final task = tasks[index];
                        return Card(
                          margin: const EdgeInsets.only(bottom: 12),
                          child: ListTile(
                            leading: Checkbox(
                              value: task.isCompleted,
                              onChanged: (_) {
                                ref
                                    .read(firestoreServiceProvider.notifier)
                                    .toggleTaskCompletion(
                                      task.id,
                                      task.isCompleted,
                                    );
                              },
                            ),
                            title: Text(
                              task.title,
                              style: TextStyle(
                                decoration: task.isCompleted
                                    ? TextDecoration.lineThrough
                                    : null,
                              ),
                            ),
                            subtitle: Text(task.description),
                            trailing: IconButton(
                              icon: const Icon(Icons.delete_outline),
                              onPressed: () {
                                ref
                                    .read(firestoreServiceProvider.notifier)
                                    .deleteTask(task.id);
                              },
                            ),
                          ),
                        );
                      },
                    );
                  },
                  loading: () => const Center(
                    child: CircularProgressIndicator(),
                  ),
                  error: (error, stack) => Center(
                    child: Text('Error: $error'),
                  ),
                ),
              ),
            ],
          );
        },
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(child: Text('Error: $error')),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddTaskDialog(context, ref),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddTaskDialog(BuildContext context, WidgetRef ref) {
    final titleController = TextEditingController();
    final descriptionController = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('New Task'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: titleController,
              decoration: const InputDecoration(
                labelText: 'Title',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: descriptionController,
              decoration: const InputDecoration(
                labelText: 'Description',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () async {
              final user = ref.read(currentUserProvider).value;
              if (user != null && titleController.text.isNotEmpty) {
                await ref.read(firestoreServiceProvider.notifier).createTask(
                  userId: user.uid,
                  title: titleController.text,
                  description: descriptionController.text,
                );
                if (context.mounted) {
                  Navigator.pop(context);
                }
              }
            },
            child: const Text('Add'),
          ),
        ],
      ),
    );
  }
}

Passo 5: Geração de Código

Não se esqueça de gerar todo o código dos providers:

flutter pub run build_runner build --delete-conflicting-outputs
```

## Pro Tips for Firebase + Riverpod

Here are some golden nuggets I've learned the hard way:

**1. Security Rules Are Critical**

Set up proper Firestore security rules:
```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /tasks/{taskId} {
      allow read, write: if request.auth != null 
        && request.auth.uid == resource.data.userId;
      allow create: if request.auth != null 
        && request.auth.uid == request.resource.data.userId;
    }
  }
}

2. Use StreamProviders para dados em tempo real

Eles gerenciam automaticamente a inscrição (subscription) e o cancelamento da inscrição (unsubscription):

@riverpod
Stream<List<Task>> userTasks(UserTasksRef ref) {
  // Stream automatically manages listeners
  return firestore.collection('tasks').snapshots().map(...);
}

3. Gerencie o Modo Offline

Ative a persistência offline:

await FirebaseFirestore.instance.settings = const Settings(
  persistenceEnabled: true,
  cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);

4. Operações em Lote (Batch Operations) para múltiplas escritas

Future<void> batchCreateTasks(List<Task> tasks) async {
  final batch = ref.read(firestoreProvider).batch();
  
  for (final task in tasks) {
    final docRef = ref.read(firestoreProvider).collection('tasks').doc();
    batch.set(docRef, task.toJson());
  }
  
  await batch.commit();
}

5. Tratamento de Erros com AsyncValue

final tasksAsync = ref.watch(userTasksProvider);

tasksAsync.when(
  data: (tasks) => YourWidget(tasks),
  loading: () => LoadingWidget(),
  error: (error, stack) => ErrorWidget(error),
);

Erros Comuns a Evitar
Não faça isto:

// Reading providers in build without watching
final user = ref.read(currentUserProvider); // Won't rebuild!

Faça isto:

// Watch providers to rebuild on changes
final user = ref.watch(currentUserProvider);

Não faça isto:

// Forgetting to handle null users
final tasks = firestore.collection('tasks')
  .where('userId', isEqualTo: user.uid) // Might be null!

Faça isto:

// Always check for null
if (user == null) return Stream.value([]);
final tasks = firestore.collection('tasks')
  .where('userId', isEqualTo: user.uid)

Dicas de Otimização de Performance

Use select() para campos específicos:

// Only rebuild when email changes
final email = ref.watch(currentUserProvider.select((user) => user.value?.email));

Limitar as consultas ao Firestore:

// Add .limit() to prevent loading too much data
.collection('tasks')
.where('userId', isEqualTo: userId)
.orderBy('createdAt', descending: true)
.limit(50)

Paginação com o Firestore:

@riverpod
Stream<List<Task>> paginatedTasks(
  PaginatedTasksRef ref,
  DocumentSnapshot? lastDoc,
) {
  var query = firestore
      .collection('tasks')
      .orderBy('createdAt', descending: true)
      .limit(20);
  
  if (lastDoc != null) {
    query = query.startAfterDocument(lastDoc);
  }
  
  return query.snapshots().map((snapshot) => ...);
}

Testando seu App com Firebase + Riverpod

Testar é super fácil com o Riverpod:

void main() {
  test('User tasks stream emits correctly', () async {
    final container = ProviderContainer(
      overrides: [
        firestoreProvider.overrideWithValue(mockFirestore),
        currentUserProvider.overrideWith((_) => Stream.value(testUser)),
      ],
    );

    final tasks = await container.read(userTasksProvider.future);
    expect(tasks, isNotEmpty);
  });
}

O que vem a seguir?

Você agora tem uma base sólida de Firebase + Riverpod! Aqui estão algumas funcionalidades que você pode adicionar:

  • Cloud Storage para upload de arquivos.

  • Firebase Cloud Messaging para notificações push.

  • Cloud Functions para lógica de backend.

  • Firebase Analytics para rastreamento de métricas.

  • Remote Config para sinalizadores de recursos (feature flags).

  • Firebase Performance Monitoring para monitoramento de desempenho.


Conclusão

Veja bem, Firebase + Riverpod 3.0 é o “time dos sonhos” para o desenvolvimento Flutter. Você obtém um backend poderoso sem precisar gerenciar servidores e um gerenciamento de estado limpo que torna o trabalho com seu código um verdadeiro prazer.

A configuração que mostrei hoje está pronta para produção. Usei exatamente esse padrão em aplicativos com milhares de usuários, e ele escala maravilhosamente bem.

Chega de lutar contra o gerenciamento de estado. Chega de configurações de backend complicadas. Apenas um código limpo e de fácil manutenção que permite que você se concentre em construir os recursos que seus usuários vão amar.

Agora, vá lá e construa algo incrível! 🔥

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20