Flutter + Firebase é uma das combinações mais produtivas para construir apps mobile completos. O Flutter cuida da UI e o Firebase cuida do backend: autenticação, banco de dados, storage, e muito mais. Vamos construir um app completo.

Arquitetura

┌─────────────────────────────┐
│          Flutter App         │
│  ┌───────────────────────┐  │
│  │  UI (Widgets)         │  │
│  │  State Management     │  │
│  └───────────┬───────────┘  │
└──────────────┼──────────────┘
               │
┌──────────────┴──────────────┐
│          Firebase            │
│  ┌─────────┐ ┌───────────┐  │
│  │   Auth  │ │ Firestore │  │
│  └─────────┘ └───────────┘  │
│  ┌─────────┐ ┌───────────┐  │
│  │ Storage │ │ Functions │  │
│  └─────────┘ └───────────┘  │
└─────────────────────────────┘

Configuração

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.24.0
  firebase_auth: ^4.16.0
  cloud_firestore: ^4.14.0
  firebase_storage: ^11.6.0
  provider: ^6.1.1

Inicialização

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

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

Autenticação

// services/auth_service.dart
import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  User? get usuarioAtual => _auth.currentUser;

  Stream<User?> get authStateChanges => _auth.authStateChanges();

  Future<User?> entrarEmailSenha(String email, String senha) async {
    try {
      final resultado = await _auth.signInWithEmailAndPassword(
        email: email,
        password: senha,
      );
      return resultado.user;
    } on FirebaseAuthException catch (e) {
      throw Exception(_traduzirErro(e.code));
    }
  }

  Future<User?> registrarEmailSenha(String email, String senha) async {
    try {
      final resultado = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: senha,
      );
      return resultado.user;
    } on FirebaseAuthException catch (e) {
      throw Exception(_traduzirErro(e.code));
    }
  }

  Future<void> sair() async {
    await _auth.signOut();
  }

  String _traduzirErro(String code) {
    switch (code) {
      case 'user-not-found':
        return 'Usuário não encontrado';
      case 'wrong-password':
        return 'Senha incorreta';
      case 'email-already-in-use':
        return 'E-mail já cadastrado';
      case 'weak-password':
        return 'Senha muito fraca';
      default:
        return 'Erro desconhecido';
    }
  }
}

Firestore: banco de dados

// services/tarefa_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class TarefaService {
  final CollectionReference _tarefas =
      FirebaseFirestore.instance.collection('tarefas');

  // Criar
  Future<void> criar(String titulo, String usuarioId) async {
    await _tarefas.add({
      'titulo': titulo,
      'concluida': false,
      'usuarioId': usuarioId,
      'criadaEm': FieldValue.serverTimestamp(),
    });
  }

  // Ler (stream em tempo real)
  Stream<QuerySnapshot> listar(String usuarioId) {
    return _tarefas
        .where('usuarioId', isEqualTo: usuarioId)
        .orderBy('criadaEm', descending: true)
        .snapshots();
  }

  // Atualizar
  Future<void> toggleConcluida(String id, bool atual) async {
    await _tarefas.doc(id).update({'concluida': !atual});
  }

  // Deletar
  Future<void> deletar(String id) async {
    await _tarefas.doc(id).delete();
  }
}

State Management com Provider

// providers/tarefa_provider.dart
import 'package:flutter/foundation.dart';
import '../services/tarefa_service.dart';

class TarefaProvider extends ChangeNotifier {
  final TarefaService _service = TarefaService();
  
  Stream<QuerySnapshot> listarTarefas(String usuarioId) {
    return _service.listar(usuarioId);
  }

  Future<void> adicionar(String titulo, String usuarioId) async {
    await _service.criar(titulo, usuarioId);
  }

  Future<void> toggle(String id, bool atual) async {
    await _service.toggleConcluida(id, atual);
  }

  Future<void> remover(String id) async {
    await _service.deletar(id);
  }
}

Tela de Login

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

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _senhaController = TextEditingController();
  final _auth = AuthService();
  bool _carregando = false;

  Future<void> _entrar() async {
    if (!_formKey.currentState!.validate()) return;
    
    setState(() => _carregando = true);
    try {
      await _auth.entrarEmailSenha(
        _emailController.text,
        _senhaController.text,
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(e.toString())),
      );
    } finally {
      setState(() => _carregando = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                controller: _emailController,
                decoration: InputDecoration(labelText: 'E-mail'),
                validator: (v) => v!.isEmpty ? 'Obrigatório' : null,
              ),
              SizedBox(height: 16),
              TextFormField(
                controller: _senhaController,
                decoration: InputDecoration(labelText: 'Senha'),
                obscureText: true,
                validator: (v) => v!.length < 6 ? 'Mínimo 6 caracteres' : null,
              ),
              SizedBox(height: 24),
              ElevatedButton(
                onPressed: _carregando ? null : _entrar,
                child: _carregando
                    ? CircularProgressIndicator()
                    : Text('Entrar'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Tela principal com lista

// screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:provider/provider.dart';
import '../providers/tarefa_provider.dart';
import '../services/auth_service.dart';

class HomeScreen extends StatelessWidget {
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final provider = Provider.of<TarefaProvider>(context);
    final usuarioId = AuthService().usuarioAtual!.uid;

    return Scaffold(
      appBar: AppBar(
        title: Text('Minhas Tarefas'),
        actions: [
          IconButton(
            icon: Icon(Icons.logout),
            onPressed: () => AuthService().sair(),
          ),
        ],
      ),
      body: StreamBuilder<QuerySnapshot>(
        stream: provider.listarTarefas(usuarioId),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(child: Text('Erro ao carregar'));
          }
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          final tarefas = snapshot.data!.docs;

          if (tarefas.isEmpty) {
            return Center(child: Text('Nenhuma tarefa ainda'));
          }

          return ListView.builder(
            itemCount: tarefas.length,
            itemBuilder: (context, index) {
              final tarefa = tarefas[index];
              final dados = tarefa.data() as Map<String, dynamic>;

              return ListTile(
                leading: Checkbox(
                  value: dados['concluida'],
                  onChanged: (_) => provider.toggle(
                    tarefa.id,
                    dados['concluida'],
                  ),
                ),
                title: Text(
                  dados['titulo'],
                  style: TextStyle(
                    decoration: dados['concluida']
                        ? TextDecoration.lineThrough
                        : null,
                  ),
                ),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () => provider.remover(tarefa.id),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _mostrarDialogo(context, provider, usuarioId),
        child: Icon(Icons.add),
      ),
    );
  }

  void _mostrarDialogo(BuildContext context, TarefaProvider provider, String usuarioId) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text('Nova Tarefa'),
        content: TextField(
          controller: _controller,
          decoration: InputDecoration(hintText: 'Título'),
          autofocus: true,
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: Text('Cancelar'),
          ),
          ElevatedButton(
            onPressed: () {
              if (_controller.text.isNotEmpty) {
                provider.adicionar(_controller.text, usuarioId);
                _controller.clear();
                Navigator.pop(ctx);
              }
            },
            child: Text('Adicionar'),
          ),
        ],
      ),
    );
  }
}

Regras de segurança Firestore

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /tarefas/{tarefaId} {
      allow read, write: if request.auth != null 
        && resource.data.usuarioId == request.auth.uid;
      allow create: if request.auth != null 
        && request.resource.data.usuarioId == request.auth.uid;
    }
  }
}

Conclusão

Flutter + Firebase permite construir apps completos em dias, não semanas. O Firebase cuida de toda a infraestrutura (auth, banco, storage, functions) enquanto você foca na experiência do usuário.

Para apps pequenos e médios, essa combinação é imbatível em produtividade. Para apps maiores, considere adicionar uma camada de abstração sobre o Firebase para facilitar migrações futuras.

Caso eu tenha falado alguma besteira, por favor, agradecerei correções e sugestões.