Aula 23 · Semana 10 · Terça, 10 de novembro de 2026 · 08:30 às 10:30
Teoria (30 minutos): execução em segundo plano: porque o sistema suspende ou termina a app, o que WorkManager e BGTaskScheduler garantem, e o que pode correr num isolate de background em Flutter. Prática: ponto de situação da sprint 2, demonstração do workmanager a atualizar a cache, e trabalho de grupo.
BGAppRefreshTask, BGProcessingTask e silent push no iOS.workmanager e escrever o callback de topo com @pragma('vm:entry-point').beginBackgroundTask); o Android mantém o processo até precisar de memória, e depois mata-o sem aviso.Timer, streams, animações, um await http.get a meio, isolates. A app não é avisada; da próxima vez começa do zero, em main().Para os testes: uma app em segundo plano não está a correr; está suspensa ou morta, e o código que estava a meio não termina.
| Mecanismo | O que é | O que a tua app tem de saber |
|---|---|---|
| WorkManager | Trabalho diferido ou periódico, com restrições (rede, carregador, bateria). Sobrevive a reinícios. | Mínimo 15 min entre execuções. O sistema escolhe a hora. A tarefa deve ser curta e idempotente. |
| Foreground service | Processo a correr com notificação permanente visível. | Só para o que o utilizador vê a acontecer: música, navegação, upload. Android 14 exige tipo declarado e permissão própria. |
| AlarmManager | Acordar a app a uma hora. | Exato exige SCHEDULE_EXACT_ALARM (12+); no 14 só despertadores e calendários a têm por defeito. Inexato pode atrasar minutos. |
| Doze | Ecrã desligado e telemóvel parado: rede, jobs e alarmes adiados para janelas de manutenção cada vez mais espaçadas. | A tua tarefa "de 6 em 6 horas" pode correr às 8 ou às 9. Não há volta a dar, e é assim que deve ser. |
| App Standby buckets | Active, working set, frequent, rare, restricted, consoante quanto o utilizador usa a app. | App raramente aberta: poucos jobs por dia. O utilizador pode ainda pô-la em "restrito" nas definições de bateria. |
Para os testes: o WorkManager garante que a tarefa corre, não quando; o Doze e os buckets decidem o quando.
BGAppRefreshTask: até 30 s, para "atualizar antes de o utilizador abrir". O sistema aprende a que horas abres a app e acorda-a um pouco antes. Pedes earliestBeginDate, não uma hora.BGProcessingTask: minutos, para manutenção pesada. Corre tipicamente de noite, a carregar. Podes exigir rede e carregador.content-available: 1, aula 19): o servidor acorda a app quando há novidade. Também limitado: poucas por hora, sem garantia de entrega.UIBackgroundModes no Info.plist: fetch, processing, remote-notification, audio, location, voip. Cada um é revisto pela Apple; áudio sem tocar som é rejeição.adb. Com a app parada no debugger do Xcode, e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"..."].Para os testes: no iOS não há tarefa periódica com hora; o sistema aprende os hábitos do utilizador e decide quando acordar a app.
workmanager: WorkManager no Android e BGTaskScheduler no iOS, com uma API só. Periódicas com frequência só no Android; no iOS registas identificadores e o sistema chama quando quer.background_fetch: o mesmo espaço, com background fetch no iOS e modo headless no Android. flutter_background_service: foreground service com um isolate a correr.main() não correu, nenhum widget existe.http, shared_preferences, drift, flutter_secure_storage, flutter_local_notifications. Não pode: BuildContext, Provider, setState, plugins que precisam de uma Activity (diálogos de permissão, image_picker), o estado em memória da app.@pragma('vm:entry-point') na função de topo: em release o tree-shaking apaga o que nenhum código Dart chama, e esta é chamada pelo nativo.Map em memória não.Para os testes: o callback de background corre num isolate novo, sem árvore de widgets e sem o estado da app; só o disco é partilhado.
return false deixa o WorkManager repetir com backoff.adb shell dumpsys batterystats; Energy gauge no Xcode. O utilizador vê "gastou 12% em segundo plano" nas definições de bateria, e desinstala.Para os testes: trabalho em background é curto, idempotente e tolera ser adiado ou repetido; se precisa de hora exata, não é background, é um alarme ou uma notificação agendada.
Cada grupo, um minuto, em pé, sem ligar o portátil:
main e corre no telemóvel neste momento. A notificação de aviso e o deep link de ontem já estão em PR?main. Candidatos: favoritos sincronizados, cache offline com "atualizado há X", mapa com localidade mais próxima, notificação de aviso com deep link. Nem tudo é obrigatório; o que entrar tem os quatro estados de UI tratados.import 'package:workmanager/workmanager.dart';
import 'background/tarefas_background.dart'; // callbackDispatcher e tarefaCache
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Workmanager().initialize(callbackDispatcher); // função de topo: slide seguinte
// Android: de 6 em 6 horas, só com rede e bateria não fraca. O sistema escolhe a hora.
// O nome único evita duplicar: registar outra vez com o mesmo nome mantém a existente.
await Workmanager().registerPeriodicTask(
tarefaCache, // uniqueName: para cancelar e não duplicar
tarefaCache, // taskName: o que chega ao callback
frequency: const Duration(hours: 6),
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
),
backoffPolicy: BackoffPolicy.exponential, // falhou? repete com espera crescente
);
// Testar já, só em desenvolvimento: registerOneOffTask('teste', tarefaCache, initialDelay: 10 s)
runApp(/* ...como na aula 22 */);
}
import 'package:workmanager/workmanager.dart';
const tarefaCache = 'pt.uac.tempoacores.atualizarCache';
/// Chamada pelo nativo num isolate novo, sem a app: de topo, e com o pragma
/// para o tree-shaking não a apagar em release.
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((tarefa, dados) async {
if (tarefa != tarefaCache) return true; // tarefa desconhecida: não repetir
try { // sem Provider nem BuildContext: montamos o que precisamos à mão
final token = await const FlutterSecureStorage().read(key: 'token');
if (token == null) return true; // sem sessão, nada a fazer
final api = ApiClient(token: token);
final cache = PrevisoesCache(AppDatabase()); // drift, aula 16
final favoritos = await FavoritosLocais.ids(); // shared_preferences
for (final id in favoritos) {
await cache.guardar(id, await api.previsao(id)); // GET /v1/forecast/{id}
}
await AvisosBackground(api, cache).verificarENotificar(favoritos); // aula 22
return true;
} catch (e) {
return false; // o WorkManager repete com backoff exponencial
}
});
}
cancelByUniqueName, não duplicar); o segundo chega ao callback para ele saber o que fazer.zonedSchedule (ontem) ou um alarme.watch().return true é feito; return false é repetir com backoff; exceção não apanhada é falha silenciosa em release. Apanha tudo e regista.adb shell dumpsys jobscheduler | grep tempoacores lista o que está agendado; registerOneOffTask com initialDelay de 10 s corre já. Os print do isolate aparecem no flutter run normal.BGTaskSchedulerPermittedIdentifiers e UIBackgroundModes no Info.plist, registo no AppDelegate, e o sistema chama executeTask quando quiser. Não entra no projeto.cancelByUniqueName. Testa com initialDelay de 10 s, a app fechada e o telemóvel bloqueado.main.BGAppRefreshTask, BGProcessingTask e como o sistema decide quando as corre.