Flutter: Criando seu primeiro App Mobile completo

Publicado em 2026-02-01 • leitura estimada • ~5 min

Guia completo Flutter: instalação, widgets, navegação, state management com Provider, integração com APIs e publicação nas lojas.

O que é Flutter e por que usar

Flutter é um framework open-source do Google para criar apps nativos para iOS, Android, Web e Desktop com uma única base de código. Usa a linguagem Dart e renderiza com o próprio motor gráfico (Skia), garantindo performance e UI consistente.

Vantagens do Flutter

  • Hot Reload: veja mudanças em tempo real sem perder estado
  • UI declarativa: widgets compostos como árvore
  • Performance nativa: compila para ARM/x86 nativo
  • Uma codebase: iOS, Android, Web, Desktop
  • Ecosistema maduro: pacotes no pub.dev

Passo 1: Instalação e setup

Instalar Flutter SDK

macOS/Linux

# Baixe o Flutter SDK
git clone https://github.com/flutter/flutter.git -b stable

# Adicione ao PATH (no ~/.bashrc ou ~/.zshrc)
export PATH="$PATH:$HOME/flutter/bin"

# Verifique instalação
flutter doctor

Windows

Baixe o ZIP do site oficial (flutter.dev) e adicione ao PATH.

Instalar dependências

# Android Studio (para emulador Android)
# Xcode (macOS, para emulador iOS)
# VS Code com extensão Flutter

# Verificar dependências
flutter doctor

# Aceitar licenças Android
flutter doctor --android-licenses

Criar primeiro projeto

flutter create meu_app
cd meu_app
flutter run

Passo 2: Estrutura de um projeto Flutter

meu_app/
├── android/          # Código nativo Android
├── ios/              # Código nativo iOS
├── lib/              # Código Dart (seu app)
│   └── main.dart     # Ponto de entrada
├── test/             # Testes
├── pubspec.yaml      # Dependências (como package.json)

Passo 3: Widgets básicos

Tudo no Flutter é widget. Widgets são elementos da UI que se compõem em árvore.

Stateless vs Stateful Widgets

StatelessWidget (imutável)

import 'package:flutter/material.dart';

class MeuWidget extends StatelessWidget {
  final String texto;

  const MeuWidget({Key? key, required this.texto}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(texto);
  }
}

StatefulWidget (mutável, tem estado)

class Contador extends StatefulWidget {
  @override
  _ContadorState createState() => _ContadorState();
}

class _ContadorState extends State<Contador> {
  int _contador = 0;

  void _incrementar() {
    setState(() {
      _contador++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Contador: $_contador'),
        ElevatedButton(
          onPressed: _incrementar,
          child: Text('Incrementar'),
        ),
      ],
    );
  }
}

Widgets comuns

Layout

// Container (box model)
Container(
  width: 200,
  height: 100,
  padding: EdgeInsets.all(16),
  margin: EdgeInsets.symmetric(vertical: 8),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
  ),
  child: Text('Hello'),
)

// Row (horizontal)
Row(
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: [
    Icon(Icons.star),
    Text('5.0'),
    Text('(120)'),
  ],
)

// Column (vertical)
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('Título'),
    Text('Subtítulo'),
  ],
)

// ListView (scroll vertical)
ListView(
  children: [
    ListTile(title: Text('Item 1')),
    ListTile(title: Text('Item 2')),
    ListTile(title: Text('Item 3')),
  ],
)

// GridView (grid responsivo)
GridView.count(
  crossAxisCount: 2,
  children: List.generate(10, (index) {
    return Card(
      child: Center(child: Text('Item $index')),
    );
  }),
)

Input

// TextField
TextField(
  decoration: InputDecoration(
    labelText: 'Email',
    hintText: 'seu@email.com',
    prefixIcon: Icon(Icons.email),
  ),
  onChanged: (value) => print(value),
)

// ElevatedButton
ElevatedButton(
  onPressed: () => print('Clicou'),
  child: Text('Enviar'),
)

// Checkbox
Checkbox(
  value: true,
  onChanged: (value) => print(value),
)

Passo 4: Exemplo prático - Lista de Tarefas

import 'package:flutter/material.dart';

void main() {
  runApp(MeuApp());
}

class MeuApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Lista de Tarefas',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: ListaTarefas(),
    );
  }
}

class ListaTarefas extends StatefulWidget {
  @override
  _ListaTarefasState createState() => _ListaTarefasState();
}

class _ListaTarefasState extends State<ListaTarefas> {
  final List<String> _tarefas = [];
  final TextEditingController _controller = TextEditingController();

  void _adicionarTarefa() {
    if (_controller.text.isNotEmpty) {
      setState(() {
        _tarefas.add(_controller.text);
        _controller.clear();
      });
    }
  }

  void _removerTarefa(int index) {
    setState(() {
      _tarefas.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Minhas Tarefas'),
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: InputDecoration(
                      hintText: 'Nova tarefa',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                SizedBox(width: 8),
                ElevatedButton(
                  onPressed: _adicionarTarefa,
                  child: Text('Adicionar'),
                ),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _tarefas.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_tarefas[index]),
                  trailing: IconButton(
                    icon: Icon(Icons.delete),
                    onPressed: () => _removerTarefa(index),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Passo 5: Navegação entre telas

Navigator.push (ir para outra tela)

// Tela 1
ElevatedButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => SegundaTela()),
    );
  },
  child: Text('Ir para Tela 2'),
)

// Tela 2
class SegundaTela extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Tela 2')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Voltar'),
        ),
      ),
    );
  }
}

Named routes (rotas nomeadas)

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => HomeScreen(),
    '/detalhes': (context) => DetalhesScreen(),
    '/perfil': (context) => PerfilScreen(),
  },
)

// Navegar
Navigator.pushNamed(context, '/detalhes');

// Passar argumentos
Navigator.pushNamed(
  context,
  '/detalhes',
  arguments: {'id': 123},
);

// Receber argumentos
final args = ModalRoute.of(context)!.settings.arguments as Map;

Passo 6: State Management com Provider

Instalar Provider

Adicione no pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.1

Execute:

flutter pub get

Criar modelo de dados

import 'package:flutter/foundation.dart';

class ContadorModel extends ChangeNotifier {
  int _contador = 0;

  int get contador => _contador;

  void incrementar() {
    _contador++;
    notifyListeners();
  }

  void decrementar() {
    _contador--;
    notifyListeners();
  }
}

Configurar Provider

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => ContadorModel(),
      child: MeuApp(),
    ),
  );
}

Consumir estado

class TelaPrincipal extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Provider Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Contador:'),
            Consumer<ContadorModel>(
              builder: (context, contador, child) {
                return Text(
                  '${contador.contador}',
                  style: TextStyle(fontSize: 48),
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              context.read<ContadorModel>().incrementar();
            },
            child: Icon(Icons.add),
          ),
          SizedBox(height: 8),
          FloatingActionButton(
            onPressed: () {
              context.read<ContadorModel>().decrementar();
            },
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Passo 7: Integração com API REST

Instalar pacote HTTP

dependencies:
  http: ^1.1.0

Buscar dados de uma API

import 'dart:convert';
import 'package:http/http.dart' as http;

class Usuario {
  final int id;
  final String nome;
  final String email;

  Usuario({required this.id, required this.nome, required this.email});

  factory Usuario.fromJson(Map<String, dynamic> json) {
    return Usuario(
      id: json['id'],
      nome: json['name'],
      email: json['email'],
    );
  }
}

Future<List<Usuario>> buscarUsuarios() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/users'),
  );

  if (response.statusCode == 200) {
    List<dynamic> body = jsonDecode(response.body);
    return body.map((json) => Usuario.fromJson(json)).toList();
  } else {
    throw Exception('Erro ao carregar usuários');
  }
}

Exibir dados com FutureBuilder

class ListaUsuarios extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Usuarios')),
      body: FutureBuilder<List<Usuario>>(
        future: buscarUsuarios(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Erro: ${snapshot.error}'));
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return Center(child: Text('Nenhum usuário encontrado'));
          }

          final usuarios = snapshot.data!;
          return ListView.builder(
            itemCount: usuarios.length,
            itemBuilder: (context, index) {
              final usuario = usuarios[index];
              return ListTile(
                leading: CircleAvatar(child: Text(usuario.nome[0])),
                title: Text(usuario.nome),
                subtitle: Text(usuario.email),
              );
            },
          );
        },
      ),
    );
  }
}

Passo 8: Persistência local (SQLite)

Instalar sqflite

dependencies:
  sqflite: ^2.3.0
  path: ^1.8.3

Criar database helper

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final DatabaseHelper instance = DatabaseHelper._init();
  static Database? _database;

  DatabaseHelper._init();

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDB('tarefas.db');
    return _database!;
  }

  Future<Database> _initDB(String filePath) async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, filePath);

    return await openDatabase(
      path,
      version: 1,
      onCreate: _createDB,
    );
  }

  Future _createDB(Database db, int version) async {
    await db.execute('''
      CREATE TABLE tarefas(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        titulo TEXT NOT NULL,
        concluida INTEGER NOT NULL
      )
    ''');
  }

  Future<int> inserir(Map<String, dynamic> row) async {
    final db = await database;
    return await db.insert('tarefas', row);
  }

  Future<List<Map<String, dynamic>>> buscarTodas() async {
    final db = await database;
    return await db.query('tarefas');
  }

  Future<int> atualizar(Map<String, dynamic> row) async {
    final db = await database;
    return await db.update('tarefas', row, where: 'id = ?', whereArgs: [row['id']]);
  }

  Future<int> deletar(int id) async {
    final db = await database;
    return await db.delete('tarefas', where: 'id = ?', whereArgs: [id]);
  }
}

Passo 9: Publicar na Google Play e App Store

Android (Google Play)

  1. Crie uma conta de desenvolvedor ($25 uma vez)
  2. Configure signing key:
    keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
  3. Configure android/key.properties
  4. Build release:
    flutter build appbundle
  5. Upload build/app/outputs/bundle/release/app-release.aab no Play Console

iOS (App Store)

  1. Conta Apple Developer ($99/ano)
  2. Configure signing no Xcode
  3. Build release:
    flutter build ipa
  4. Upload via Xcode ou Transporter
  5. Submeta para review no App Store Connect

Boas práticas

  • Use const widgets: melhora performance
  • Evite setState() excessivo: use Provider ou Riverpod
  • Separe widgets grandes: mantenha widgets pequenos e reutilizáveis
  • Use keys quando necessário: para preservar estado em listas
  • Teste no dispositivo real: emuladores não refletem performance real

Recursos úteis

  • pub.dev: pacotes e plugins
  • flutter.dev/docs: documentação oficial
  • FlutterFlow: low-code para prototipar
  • Widget of the Week: canal no YouTube do Flutter

Conclusão

Flutter permite criar apps nativos de alta qualidade com uma única codebase. Comece com widgets básicos, aprenda navegação e state management, integre APIs e publique nas lojas. Com hot reload e ecossistema rico, você pode iterar rapidamente e entregar apps profissionais para iOS e Android.

FAQ

Flutter é melhor que React Native?

Depende. Flutter tem melhor performance e UI consistente. React Native tem comunidade maior e usa JavaScript. Escolha baseado na sua stack.

Preciso saber Dart para usar Flutter?

Sim, mas Dart é simples se você conhece JavaScript, Java ou C#. A sintaxe é familiar e a curva de aprendizado é suave.

Flutter funciona para Web?

Sim, mas não é ideal para sites tradicionais. Use para web apps complexos ou para reusar código mobile.

Como debugar no Flutter?

Use DevTools, print(), debugger no VS Code, e Flutter Inspector para visualizar a árvore de widgets.

Flutter é gratuito?

Sim, totalmente open-source. Custos são apenas das contas de desenvolvedor (Google Play, App Store).