Desenvolvimento de aplicações móveis
Apresentação
UC02835 (50h · 4,5 pts) ensina a construir aplicações móveis. Aborda as abordagens (nativo, cross-platform, PWA), foca Flutter (Dart), e cobre UI, navegação, estado, consumo de APIs, armazenamento local, UX móvel e publicação nas lojas.
Mobile não é "web mais pequena": toque, gestos, offline, bateria, ecrãs muito variados, permissões e lojas de apps mudam tudo.
Abordagens
Nativo
- Android — Kotlin (ou Java) + Android Studio.
- iOS — Swift (ou Objective-C) + Xcode.
Prós: performance máxima, acesso total ao hardware e APIs do SO, melhor integração. Contras: duas bases de código, duas equipas/competências, mais custo.
Cross-platform
Uma base de código → várias plataformas.
- Flutter (Dart, Google) — render próprio, UI consistente, rápido. Foco desta UC.
- React Native (JS, Meta) — usa componentes nativos, ecossistema React.
- .NET MAUI, Kotlin Multiplatform — alternativas.
Prós: 1 código para Android + iOS (+ web/desktop), time-to-market, equipa pequena. Contras: algum overhead, casos extremos podem precisar de código nativo.
PWA (Progressive Web App)
App web que instala a partir do browser, funciona offline (service worker), recebe notificações.
Prós: sem loja, 1 código (web), actualização instantânea. Contras: acesso limitado a hardware, menos integração, iOS restringe.
Como escolher
| Caso | Recomendação |
|---|---|
| Conteúdo simples, orçamento mínimo | PWA |
| App de negócio, equipa pequena | Flutter / React Native |
| Performance crítica, jogos, hardware intensivo | Nativo |
Flutter · fundamentos
Setup
# Instalar Flutter SDK (flutter.dev)
flutter doctor # verifica ambiente
flutter create minha_app
cd minha_app
flutter run # corre em emulador/dispositivo
Editor: VSCode ou Android Studio com plugin Flutter/Dart.
Dart em 1 minuto
void main() {
String nome = 'Ana';
int idade = 17;
var lista = [1, 2, 3]; // tipo inferido
final pi = 3.14; // imutável
if (idade >= 18) {
print('$nome é maior');
}
for (var n in lista) print(n);
int soma(int a, int b) => a + b;
print(soma(2, 3));
}
Dart: tipado, OO, null safety (String? aceita null, String não).
Tudo é widget
A UI Flutter é uma árvore de widgets (componentes). Layout = compor widgets.
import 'package:flutter/material.dart';
void main() => runApp(const MinhaApp());
class MinhaApp extends StatelessWidget {
const MinhaApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Aulify',
theme: ThemeData(primarySwatch: Colors.indigo),
home: Scaffold(
appBar: AppBar(title: const Text('Aulify')),
body: const Center(child: Text('Olá, mundo!')),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
),
);
}
}
Scaffold fornece a estrutura padrão de um ecrã Material.
Widgets essenciais
Conteúdo:
- Text('...'), Icon(Icons.x), Image.network/asset(...).
Layout:
- Column (vertical), Row (horizontal).
- Stack (sobrepostos), Expanded, Flexible.
- Container (caixa com padding, margin, decoração).
- Padding, Center, SizedBox.
- ListView (lista com scroll), GridView.
Interacção:
- ElevatedButton, TextButton, IconButton.
- TextField, Checkbox, Switch, Slider.
- GestureDetector, InkWell.
Column(
children: [
Text('Lista de produtos'),
Expanded(
child: ListView(
children: const [
ListTile(leading: Icon(Icons.shopping_cart), title: Text('Caneta')),
ListTile(leading: Icon(Icons.book), title: Text('Caderno')),
],
),
),
],
)
Stateless vs Stateful
StatelessWidget — não muda depois de construído.
class Saudacao extends StatelessWidget {
final String nome;
const Saudacao(this.nome, {super.key});
@override
Widget build(BuildContext context) => Text('Olá, $nome');
}
StatefulWidget — tem estado que muda ao longo do tempo.
class Contador extends StatefulWidget {
const Contador({super.key});
@override
State<Contador> createState() => _ContadorState();
}
class _ContadorState extends State<Contador> {
int _n = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Cliques: $_n', style: const TextStyle(fontSize: 32)),
ElevatedButton(
onPressed: () => setState(() => _n++),
child: const Text('Incrementar'),
),
],
);
}
}
setState() notifica o Flutter para reconstruir o widget com o novo estado.
Navegação
Push / pop
// Avançar para novo ecrã
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetalheScreen()),
);
// Voltar
Navigator.pop(context);
// Passar dados
Navigator.push(context, MaterialPageRoute(
builder: (c) => DetalheScreen(produtoId: 42),
));
Rotas nomeadas
MaterialApp(
initialRoute: '/',
routes: {
'/': (c) => const HomeScreen(),
'/perfil': (c) => const PerfilScreen(),
'/sobre': (c) => const SobreScreen(),
},
);
Navigator.pushNamed(context, '/perfil');
Para apps maiores: go_router (rotas declarativas, deep links).
Gestão de estado
À medida que a app cresce, setState não chega — estado precisa de ser partilhado entre ecrãs.
| Solução | Quando |
|---|---|
setState |
Estado local de 1 widget |
Provider |
Estado partilhado, simples |
Riverpod |
Provider melhorado, testável |
Bloc |
Apps grandes, fluxos complexos |
GetX |
Tudo-em-um (controverso) |
Provider (exemplo)
class CarrinhoModel extends ChangeNotifier {
final List<String> _itens = [];
List<String> get itens => List.unmodifiable(_itens);
void adicionar(String item) {
_itens.add(item);
notifyListeners(); // avisa os widgets que escutam
}
}
// No topo da app
ChangeNotifierProvider(
create: (_) => CarrinhoModel(),
child: const MinhaApp(),
)
// Num widget
final carrinho = context.watch<CarrinhoModel>();
Text('${carrinho.itens.length} itens');
context.read<CarrinhoModel>().adicionar('Caneta');
Princípio: começar simples (setState) e subir de complexidade só quando necessário.
Consumir APIs
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<dynamic>> buscarProdutos() async {
final resposta = await http.get(
Uri.parse('https://api.aulify.pt/produtos'),
);
if (resposta.statusCode == 200) {
return jsonDecode(resposta.body) as List;
} else {
throw Exception('Erro ${resposta.statusCode}');
}
}
async/await/Future — operações que demoram (rede, disco) não bloqueiam a UI.
FutureBuilder
Mostra loading, depois dados ou erro:
FutureBuilder<List<dynamic>>(
future: buscarProdutos(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Erro: ${snapshot.error}'));
}
final produtos = snapshot.data!;
return ListView.builder(
itemCount: produtos.length,
itemBuilder: (c, i) => ListTile(
title: Text(produtos[i]['nome']),
),
);
},
)
Modelos de dados
class Produto {
final int id;
final String nome;
final double preco;
Produto({required this.id, required this.nome, required this.preco});
factory Produto.fromJson(Map<String, dynamic> json) => Produto(
id: json['id'],
nome: json['nome'],
preco: (json['preco'] as num).toDouble(),
);
}
Converter JSON em objectos torna o código mais seguro e legível.
Armazenamento local
| Pacote | Uso |
|---|---|
shared_preferences |
Chave-valor simples (settings, flags) |
flutter_secure_storage |
Dados sensíveis (token), encriptado |
sqflite |
SQLite no dispositivo |
hive / isar |
NoSQL local rápido |
path_provider |
Aceder a pastas do dispositivo |
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', 'abc123');
await prefs.setBool('dark_mode', true);
final token = prefs.getString('token');
final dark = prefs.getBool('dark_mode') ?? false;
Token de autenticação → flutter_secure_storage (encriptado), não shared_preferences.
UX móvel
Princípios
- Alvos de toque ≥ 48×48 dp (dedos não são precisos como rato).
- Feedback imediato: ripple, loading, animações curtas.
- Offline-first quando possível; nunca crashar sem rede.
- Permissões — pedir só quando necessário, explicar o porquê antes.
- Estados de ecrã: loading, vazio, erro, sucesso — desenhar todos.
- Navegação consistente (bottom nav, drawer).
- Adaptar a vários tamanhos e orientações.
Acessibilidade
Semanticswidgets, labels em botões só com ícone.- Respeitar tamanho de fonte do sistema (
MediaQuery.textScaler). - Contraste suficiente.
- Suporte a leitor de ecrã (TalkBack/VoiceOver).
Performance
constem widgets imutáveis (evita reconstruções).ListView.builder(lazy) em listas grandes, nãoListView(children:[...]).- Imagens dimensionadas, cache (
cached_network_image). - Evitar trabalho pesado na thread de UI (usar
compute/isolates).
Publicação
Preparação
- Ícone e splash screen (
flutter_launcher_icons). - Nome e bundle id únicos.
- Política de privacidade (obrigatória nas lojas).
- Screenshots e descrição para a loja.
- Testar em dispositivos reais (não só emulador).
- Build release assinado.
Builds
flutter build apk --release # Android APK
flutter build appbundle --release # Android (Play Store, .aab)
flutter build ios --release # iOS (precisa de macOS + Xcode)
Lojas
| Loja | Conta | Custo | Revisão |
|---|---|---|---|
| Google Play | Google Play Console | 25 $ (uma vez) | horas a dias |
| App Store | Apple Developer | 99 $/ano | 1-3 dias |
Apps são revistas: cumprir guidelines (privacidade, conteúdo, permissões justificadas) ou são rejeitadas.
Apêndices
A · Cheatsheet Flutter
StatelessWidget / StatefulWidget
setState(() => x = y);
Navigator.push / pop / pushNamed
Column / Row / Stack / Expanded
ListView.builder(itemCount:, itemBuilder:)
FutureBuilder(future:, builder:)
context.watch<T>() / context.read<T>()
flutter run / build apk / build appbundle
B · Glossário
Dart. Linguagem do Flutter. Widget. Componente de UI Flutter. State. Dados que mudam e redesenham a UI. Hot reload. Ver mudanças sem reiniciar a app. PWA. Progressive Web App. Scaffold. Estrutura base de ecrã Material.
C · Recursos
- flutter.dev · documentação oficial + codelabs.
- dart.dev · linguagem.
- pub.dev · pacotes (como pip/npm).
- Flutter Apprentice (livro) · raywenderlich.
- Flutter Widget of the Week · YouTube oficial.