Partilhar: WhatsApp
aulify
UC UC02835 · T. Desenv. Software

Ficha 02 · Navegação, APIs e armazenamento

Vários ecrãs, consumir API, persistir, publicar
Versão · Aluno
Tempo · 60 minutos
Cotação · 100 pontos
Aluno(a)
Turma
Data
Objectivos da ficha

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.

  1. Ícone e splash screen próprios (não o default do Flutter).
  2. Política de privacidade publicada (obrigatória nas duas lojas).
  3. Screenshots e descrição para a página da loja.
  4. Build release assinado (flutter build appbundle / ios).
  5. Testar em dispositivos reais (vários tamanhos, Android e iOS).
  6. Bundle id / package name único e definitivo.
  7. 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.