Ficha 02 · Navegação, APIs e armazenamento
- Navegar entre ecrãs
- Consumir APIs
- Armazenar localmente
- Conhecer publicação
Parte I · Navegação
Exercício 1 · Push/pop (15 pts)
a) Escreve o código para ir de HomeScreen para DetalheScreen.
b) E para voltar.
c) Como passar um produtoId para o DetalheScreen?
// a) Avançar
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetalheScreen()),
);
// b) Voltar
Navigator.pop(context);
// c) Passar dados
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetalheScreen(produtoId: 42),
),
);
// DetalheScreen recebe no construtor:
class DetalheScreen extends StatelessWidget {
final int produtoId;
const DetalheScreen({super.key, required this.produtoId});
...
}
Exercício 2 · Rotas nomeadas (10 pts)
Configura uma MaterialApp com 3 rotas nomeadas e mostra como navegar para uma delas.
MaterialApp(
initialRoute: '/',
routes: {
'/': (c) => const HomeScreen(),
'/perfil': (c) => const PerfilScreen(),
'/definicoes': (c) => const DefinicoesScreen(),
},
);
// Navegar
Navigator.pushNamed(context, '/perfil');
Para apps maiores com deep links: pacote go_router.
Parte II · APIs
Exercício 3 · Consumir API (20 pts)
Escreve uma função async que faz GET a https://api.aulify.pt/produtos, devolve a lista (JSON) ou lança excepção em erro.
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<dynamic>> buscarProdutos() async {
final url = Uri.parse('https://api.aulify.pt/produtos');
final resposta = await http.get(url);
if (resposta.statusCode == 200) {
return jsonDecode(resposta.body) as List;
} else {
throw Exception('Erro ${resposta.statusCode} ao buscar produtos');
}
}
await espera a resposta sem bloquear a UI. jsonDecode converte string JSON em lista/mapa Dart.
Exercício 4 · FutureBuilder (20 pts)
Usa um FutureBuilder para mostrar: - Um spinner enquanto carrega. - A lista de produtos quando chega. - Mensagem de erro se falhar.
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 ?? [];
if (produtos.isEmpty) {
return const Center(child: Text('Sem produtos'));
}
return ListView.builder(
itemCount: produtos.length,
itemBuilder: (context, i) => ListTile(
title: Text(produtos[i]['nome']),
subtitle: Text('${produtos[i]['preco']} €'),
),
);
},
)
Cobre os 4 estados: loading, erro, vazio, dados. Boa prática de UX móvel.
Exercício 5 · Modelo de dados (10 pts)
Cria uma classe Produto com fromJson para converter o JSON da API em objecto.
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) {
return Produto(
id: json['id'] as int,
nome: json['nome'] as String,
preco: (json['preco'] as num).toDouble(),
);
}
}
// Uso
final lista = (jsonDecode(body) as List)
.map((j) => Produto.fromJson(j))
.toList();
Trabalhar com objectos tipados é mais seguro que aceder a json['campo'] por todo o lado.
Parte III · Armazenamento
Exercício 6 · Escolher solução (10 pts)
Para cada dado, indica onde guardar:
a) Preferência "dark mode on/off". ___
b) Token de autenticação. ___
c) Cache de 500 produtos para uso offline. ___
d) Última pesquisa do utilizador. ___
a) shared_preferences — chave-valor simples.
b) flutter_secure_storage — sensível, tem de ser encriptado.
c) sqflite ou hive/isar — volume de dados estruturados, consultável offline.
d) shared_preferences — string simples.
Regra: dados sensíveis → secure storage; settings → shared_preferences; muitos dados estruturados → BD local.
Exercício 7 · shared_preferences (15 pts)
Escreve funções para guardar e ler a preferência "dark mode" (booleano), com valor por defeito false.
import 'package:shared_preferences/shared_preferences.dart';
Future<void> guardarDarkMode(bool ativo) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('dark_mode', ativo);
}
Future<bool> lerDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('dark_mode') ?? false; // default false
}
// Uso
await guardarDarkMode(true);
final escuro = await lerDarkMode();
O ?? false garante um valor por defeito quando a chave ainda não existe (primeira execução).
Parte IV · Publicação
Exercício 8 · Antes de publicar (10 pts)
Lista 5 coisas a preparar antes de submeter uma app às lojas.
- Ícone e splash screen próprios (não o default do Flutter).
- Política de privacidade publicada (obrigatória nas duas lojas).
- Screenshots e descrição para a página da loja.
- Build release assinado (
flutter build appbundle/ios). - Testar em dispositivos reais (vários tamanhos, Android e iOS).
- Bundle id / package name único e definitivo.
- Permissões justificadas (a Apple/Google rejeitam pedidos sem razão clara).
Custos: Google Play 25 $ uma vez; Apple Developer 99 $/ano. Ambas revêem a app antes de aprovar.