Módulo 13 — Acesso a Recursos Nativos e Permissões

Você chegou a um módulo que abre uma nova dimensão no desenvolvimento de aplicativos móveis: o acesso ao hardware do dispositivo. Até aqui, o aplicativo de delivery que você construiu comunica-se com o backend na nuvem, exibe dados em telas elaboradas, autentica usuários com segurança e os notifica em tempo real. Mas o smartphone que o usuário carrega no bolso é muito mais do que um terminal de acesso à internet — ele tem uma câmera, um receptor GPS, um acelerômetro, um giroscópio e a capacidade de compartilhar informações com outros aplicativos. Usar esses recursos transforma a experiência do usuário de algo que poderia ser feito em um navegador web em algo verdadeiramente móvel.

No contexto do delivery, os recursos nativos resolvem problemas práticos muito reais. O usuário pode atualizar sua foto de perfil tirando uma selfie ou selecionando uma imagem da galeria. A geolocalização permite que o aplicativo sugira automaticamente o endereço de entrega mais provável com base na posição atual do dispositivo, poupando ao usuário o trabalho de digitá-lo a cada pedido. O compartilhamento de conteúdo permite que o usuário envie o código de referral para amigos ou compartilhe os detalhes de um pedido pelo WhatsApp. E os sensores do dispositivo oferecem possibilidades criativas que não existem em nenhuma outra plataforma.

O elo que conecta todos esses recursos é o sistema de permissões. Aceder à câmera, à localização ou ao acelerômetro sem a aprovação explícita do usuário é uma violação de privacidade — e os sistemas operacionais móveis modernos impedem isso de forma ativa. Neste módulo, você aprenderá como o sistema de permissões funciona em profundidade e como usar o pacote permission_handler para gerenciá-lo de forma elegante e respeitosa.


Seção 1 — O Modelo de Permissões dos Sistemas Operacionais Móveis

Para implementar permissões corretamente, você precisa entender o modelo de permissões do Android em profundidade. Desenvolvedores que tratam as permissões como uma formalidade a ser cumprida — apenas colocando as linhas no AndroidManifest.xml e esperando que tudo funcione — frequentemente se deparam com falhas inexplicáveis em dispositivos de usuários reais. Entender o modelo transforma o processo de permissões de um obstáculo em uma ferramenta que você usa a seu favor.

A Evolução do Modelo de Permissões no Android

O Android nasceu com um modelo de permissões que hoje chamamos de permissões em tempo de instalação: o usuário via a lista de permissões que o aplicativo solicitava durante a instalação e precisava aceitar todas de uma vez, sem possibilidade de escolha granular. Uma pergunta clássica daquela época era “por que esse jogo de puzzle precisa de acesso aos meus contatos?”. Sem a capacidade de negar permissões individualmente, o usuário só tinha duas opções: aceitar tudo ou não instalar o aplicativo.

O Android 6.0 (API level 23, lançado em 2015) mudou isso fundamentalmente ao introduzir as permissões em tempo de execução, também chamadas de permissões perigosas (dangerous permissions). A partir dessa versão, as permissões que acessam dados sensíveis do usuário — câmera, localização, contatos, microfone, armazenamento — precisam ser solicitadas explicitamente ao usuário no momento em que o aplicativo precisa delas, não na instalação. O usuário vê um diálogo do sistema operacional perguntando se deseja conceder aquela permissão específica, com a opção de negar.

O Android 11 (API level 30) adicionou mais uma camada: a opção “Permitir apenas desta vez” para permissões de localização, câmera e microfone. Com essa opção, o usuário concede a permissão apenas para a sessão atual do aplicativo — na próxima vez que o aplicativo for aberto e precisar daquela permissão, precisará solicitá-la novamente. O Android 12 aprofundou ainda mais esse controle, introduzindo a localização aproximada como alternativa à localização precisa.

O Android 13 (API level 33) exigiu que a permissão POST_NOTIFICATIONS (para notificações push, que você usou no Módulo 12) também fosse solicitada em tempo de execução. O Android 14 adicionou controle granular de acesso à galeria de fotos, permitindo que o usuário conceda acesso apenas a fotos e vídeos selecionados, em vez de toda a galeria.

Essa evolução constante do modelo de permissões tem uma implicação prática importante para você: o comportamento do aplicativo ao solicitar permissões pode variar dependendo da versão do Android no dispositivo do usuário. O pacote permission_handler abstrai essas diferenças, mas é importante que você entenda que essa abstração existe por uma razão.

Categorias de Permissões no Android

O Android divide as permissões em três categorias segundo sua sensibilidade. As permissões normais (normal permissions) são concedidas automaticamente pelo sistema na instalação, sem necessidade de aprovação explícita do usuário. Elas representam operações de baixo risco, como INTERNET (acesso à internet) e VIBRATE (vibração do dispositivo). O fato de seu aplicativo estar disponível na Play Store e o usuário tê-lo instalado é considerado consentimento suficiente.

As permissões perigosas (dangerous permissions) são as que exigem aprovação explícita em tempo de execução. Elas acessam dados privados do usuário ou recursos que podem afetar significativamente a privacidade ou a segurança. Câmera, localização, contatos, microfone e armazenamento externo se enquadram nessa categoria. Cada permissão perigosa pertence a um grupo de permissões (permission group), e quando o usuário aprova uma permissão de um grupo, o sistema geralmente aprova automaticamente as outras permissões do mesmo grupo — mas esse comportamento é uma implementação específica de cada fabricante e não deve ser assumido.

As permissões especiais (special permissions) são aquelas que controlam recursos particularmente sensíveis e que requerem que o usuário navegue até as configurações do sistema para concedê-las, pois não há diálogo automático de concessão. Exemplos incluem SYSTEM_ALERT_WINDOW (exibir sobreposições em outros aplicativos) e WRITE_SETTINGS (modificar configurações do sistema). Para o delivery, você não precisará de permissões especiais.

Os Quatro Estados de uma Permissão

O pacote permission_handler representa o status de uma permissão com quatro estados distintos que cobrem todos os cenários possíveis:

O estado granted indica que o usuário concedeu a permissão e o recurso está disponível para uso imediato.

O estado denied indica que o usuário negou a permissão explicitamente, mas ainda é possível solicitá-la novamente. Após duas negativas consecutivas no Android, o sistema pode modificar esse comportamento para permanentlyDenied.

O estado permanentlyDenied indica que o usuário negou a permissão e marcou a opção “Não perguntar novamente” (ou que o sistema atingiu o limite de solicitações e passou automaticamente para esse estado). Nesse caso, o único caminho para obter a permissão é redirecionar o usuário às configurações do aplicativo no sistema operacional.

O estado restricted indica que a permissão está restrita por uma política do dispositivo ou do sistema — por exemplo, em dispositivos corporativos gerenciados pelo MDM da empresa onde o administrador proibiu o uso da câmera. O aplicativo não pode superar essa restrição e deve degradar graciosamente a funcionalidade correspondente.

stateDiagram-v2
    [*] --> NaoSolicitada: instalação do app
    NaoSolicitada --> granted: usuário aprova
    NaoSolicitada --> denied: usuário nega
    denied --> granted: usuário aprova na nova solicitação
    denied --> permanentlyDenied: usuário nega com "não perguntar novamente"
    permanentlyDenied --> granted: usuário abre configurações e aprova
    granted --> denied: usuário revoga nas configurações (Android 13+)
    NaoSolicitada --> restricted: política do dispositivo


Seção 2 — O Pacote permission_handler

O permission_handler é o pacote Flutter que unifica o gerenciamento de permissões para Android e iOS em uma única API Dart. Sem ele, você precisaria escrever código nativo para cada plataforma e invocar esse código via platform channels — um processo trabalhoso e propenso a erros. Com o permission_handler, solicitar uma permissão é uma chamada de método assíncrona simples.

Para adicionar o pacote ao projeto:

dependencies:
  permission_handler: ^12.0.1

Configurando o AndroidManifest

O permission_handler requer que todas as permissões que o aplicativo pode solicitar estejam declaradas no AndroidManifest.xml. Essa declaração é um requisito do Android: sem ela, o sistema não permite que a permissão seja solicitada, independentemente do que o código faça. Para o aplicativo de delivery, adicione as permissões relevantes:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Câmera e galeria -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
    <!-- Para Android < 13 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                     android:maxSdkVersion="32"/>

    <!-- Localização -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

    <!-- Notificações (Android 13+) -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <application ...>
        ...
    </application>
</manifest>

A declaração de uma permissão no manifesto não concede automaticamente o acesso ao recurso — ela apenas informa ao sistema que o aplicativo pode precisar daquela permissão em algum momento. A concessão efetiva ainda depende da aprovação explícita do usuário (para permissões perigosas) ou acontece automaticamente na instalação (para permissões normais).

A API do permission_handler

O permission_handler expõe uma API baseada em enums. Cada recurso do dispositivo é representado por um valor do enum Permission:

import 'package:permission_handler/permission_handler.dart';

/// Serviço que centraliza todas as operações de permissão do aplicativo.
class PermissaoServico {
  /// Verifica o status atual de uma permissão sem solicitá-la.
  Future<PermissionStatus> verificarStatus(Permission permissao) async {
    return permissao.status;
  }

  /// Solicita uma permissão ao usuário. Exibe o diálogo do sistema.
  /// Se a permissão já foi concedida, retorna imediatamente com [PermissionStatus.granted].
  /// Se a permissão está permanentemente negada, retorna [PermissionStatus.permanentlyDenied].
  Future<PermissionStatus> solicitar(Permission permissao) async {
    return permissao.request();
  }

  /// Verifica se a permissão foi concedida.
  Future<bool> estaGerenciada(Permission permissao) async {
    return (await permissao.status).isGranted;
  }

  /// Abre as configurações do aplicativo no sistema operacional.
  /// Usado quando a permissão está permanentemente negada.
  Future<bool> abrirConfiguracoesSistema() async {
    return openAppSettings();
  }

  /// Solicita a permissão e trata todos os estados possíveis,
  /// retornando true somente se o acesso foi concedido.
  Future<bool> solicitarComTratamento(
    Permission permissao, {
    Future<void> Function()? aoNegarPermanentemente,
  }) async {
    final PermissionStatus status = await permissao.request();

    switch (status) {
      case PermissionStatus.granted:
      case PermissionStatus.limited:
        return true;

      case PermissionStatus.permanentlyDenied:
        await aoNegarPermanentemente?.call();
        return false;

      case PermissionStatus.denied:
      case PermissionStatus.restricted:
      case PermissionStatus.provisional:
        return false;
    }
  }
}
import 'package:permission_handler/permission_handler.dart';

class PermissaoServico {
  Future<bool> estaGerenciada(Permission p) async => (await p.status).isGranted;
  Future<bool> abrirConfiguracoesSistema() => openAppSettings();

  Future<bool> solicitarComTratamento(
    Permission p, {
    Future<void> Function()? aoNegarPermanentemente,
  }) async {
    final status = await p.request();
    if (status.isGranted || status == PermissionStatus.limited) return true;
    if (status.isPermanentlyDenied) await aoNegarPermanentemente?.call();
    return false;
  }
}

O Padrão de Uso Correto: Verificar Antes de Solicitar

Uma boa prática é sempre verificar o status da permissão antes de solicitá-la, para evitar exibir o diálogo desnecessariamente quando a permissão já foi concedida. O padrão que você adotará em todo o projeto de delivery é:

graph TD
    A[Usuário aciona funcionalidade<br/>que requer permissão] --> B{Status atual?}
    B -->|granted| C[Acessa o recurso diretamente]
    B -->|denied / notDetermined| D[Exibe rationale ao usuário]
    D --> E[Solicita permissão]
    E --> F{Resultado?}
    F -->|granted| C
    F -->|denied| G[Desabilita a funcionalidade<br/>mostra explicação breve]
    F -->|permanentlyDenied| H[Exibe diálogo explicativo<br/>com botão para configurações]
    H --> I[openAppSettings]
    B -->|permanentlyDenied| H
    B -->|restricted| G


Seção 3 — Captura de Imagens: Câmera e Galeria

A captura de imagens é uma das funcionalidades mais solicitadas em aplicativos móveis modernos. No delivery, há pelo menos dois casos de uso claros: o usuário pode querer atualizar sua foto de perfil, e o entregador pode precisar registrar uma foto do local de entrega quando o cliente não está disponível para receber o pedido pessoalmente.

O pacote image_picker é a solução recomendada pelo Flutter para selecionar imagens da galeria ou capturar novas fotos com a câmera. Ele encapsula as APIs nativas de seleção de imagens de forma que você não precisa se preocupar com as diferenças entre as versões do Android.

dependencies:
  image_picker: ^1.1.2

Permissões Necessárias

Para capturar uma foto com a câmera, você precisa da permissão Permission.camera. Para selecionar uma imagem da galeria, você precisa da permissão Permission.photos (no iOS) ou Permission.storage (Android anterior a 13) / Permission.photos (Android 13+). O image_picker gerencia internamente a solicitação de permissão para acesso à galeria, mas para câmera você precisa solicitar explicitamente antes de chamar o picker.

Implementando o Serviço de Seleção de Imagem

import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import '../permissoes/permissao_servico.dart';

/// Resultado da operação de seleção de imagem.
sealed class ResultadoImagemSelecionada {
  const ResultadoImagemSelecionada();
}

/// O usuário selecionou uma imagem com sucesso.
final class ImagemSelecionada extends ResultadoImagemSelecionada {
  final XFile arquivo;
  const ImagemSelecionada(this.arquivo);
}

/// O usuário cancelou a seleção sem escolher uma imagem.
final class SelecaoCancelada extends ResultadoImagemSelecionada {
  const SelecaoCancelada();
}

/// Ocorreu um erro durante a seleção.
final class ErroNaSelecao extends ResultadoImagemSelecionada {
  final String mensagem;
  const ErroNaSelecao(this.mensagem);
}

/// Permissão necessária foi negada.
final class PermissaoNegada extends ResultadoImagemSelecionada {
  const PermissaoNegada();
}

class ImagemServico {
  final ImagePicker _picker;
  final PermissaoServico _permissaoServico;

  ImagemServico({
    ImagePicker? picker,
    required PermissaoServico permissaoServico,
  })  : _picker = picker ?? ImagePicker(),
        _permissaoServico = permissaoServico;

  /// Solicita ao usuário que tire uma nova foto com a câmera.
  /// Verifica e solicita a permissão de câmera antes de abrir o picker.
  Future<ResultadoImagemSelecionada> capturarFotoComCamera({
    double? qualidade,   // 0.0 a 100.0; null = qualidade máxima
    int? larguraMaxima,  // em pixels; null = resolução original
  }) async {
    // Verifica permissão de câmera antes de prosseguir
    final bool permissaoConcedida = await _permissaoServico.solicitarComTratamento(
      Permission.camera,
    );

    if (!permissaoConcedida) {
      return const PermissaoNegada();
    }

    try {
      final XFile? arquivo = await _picker.pickImage(
        source: ImageSource.camera,
        imageQuality: qualidade?.round() ?? 85,
        maxWidth: larguraMaxima?.toDouble(),
      );

      if (arquivo == null) return const SelecaoCancelada();
      return ImagemSelecionada(arquivo);
    } catch (e) {
      return ErroNaSelecao('Não foi possível acessar a câmera: $e');
    }
  }

  /// Solicita ao usuário que selecione uma imagem da galeria.
  Future<ResultadoImagemSelecionada> selecionarDaGaleria({
    double? qualidade,
  }) async {
    try {
      final XFile? arquivo = await _picker.pickImage(
        source: ImageSource.gallery,
        imageQuality: qualidade?.round() ?? 85,
      );

      if (arquivo == null) return const SelecaoCancelada();
      return ImagemSelecionada(arquivo);
    } catch (e) {
      return ErroNaSelecao('Não foi possível acessar a galeria: $e');
    }
  }
}
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import '../permissoes/permissao_servico.dart';

sealed class ResultadoImagem { const ResultadoImagem(); }
final class ImagemSelecionada extends ResultadoImagem {
  const ImagemSelecionada(this.arquivo);
  final XFile arquivo;
}
final class SelecaoCancelada  extends ResultadoImagem { const SelecaoCancelada();  }
final class ErroNaSelecao     extends ResultadoImagem {
  const ErroNaSelecao(this.mensagem);
  final String mensagem;
}
final class PermissaoNegada   extends ResultadoImagem { const PermissaoNegada();   }

class ImagemServico {
  ImagemServico({ImagePicker? picker, required PermissaoServico permissoes})
      : _picker = picker ?? ImagePicker(), _permissoes = permissoes;

  final ImagePicker _picker;
  final PermissaoServico _permissoes;

  Future<ResultadoImagem> capturarFotoComCamera({int qualidade = 85}) async {
    if (!await _permissoes.solicitarComTratamento(Permission.camera)) {
      return const PermissaoNegada();
    }
    return _pick(ImageSource.camera, qualidade);
  }

  Future<ResultadoImagem> selecionarDaGaleria({int qualidade = 85}) =>
      _pick(ImageSource.gallery, qualidade);

  Future<ResultadoImagem> _pick(ImageSource src, int q) async {
    try {
      final f = await _picker.pickImage(source: src, imageQuality: q);
      return f == null ? const SelecaoCancelada() : ImagemSelecionada(f);
    } catch (e) {
      return ErroNaSelecao('Erro: $e');
    }
  }
}

Usando o Serviço no Widget de Perfil do Usuário

O widget que permite ao usuário atualizar sua foto de perfil demonstra como integrar o ImagemServico com a interface e o provider de estado:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:io';
import '../../infraestrutura/imagem/imagem_servico.dart';
import '../../apresentacao/perfil/perfil_notifier.dart';

class WidgetFotoPerfil extends StatelessWidget {
  const WidgetFotoPerfil({super.key});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => _exibirOpcoesFoto(context),
      child: Stack(
        children: [
          Consumer<PerfilNotifier>(
            builder: (context, notifier, _) {
              return CircleAvatar(
                radius: 48,
                backgroundImage: notifier.urlFotoPerfil != null
                    ? NetworkImage(notifier.urlFotoPerfil!)
                    : null,
                child: notifier.urlFotoPerfil == null
                    ? const Icon(Icons.person, size: 48)
                    : null,
              );
            },
          ),
          const Positioned(
            bottom: 0,
            right: 0,
            child: CircleAvatar(
              radius: 14,
              backgroundColor: Colors.blue,
              child: Icon(Icons.camera_alt, size: 16, color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }

  void _exibirOpcoesFoto(BuildContext context) {
    showModalBottomSheet<void>(
      context: context,
      builder: (context) => SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.camera_alt),
              title: const Text('Tirar uma foto'),
              onTap: () {
                Navigator.pop(context);
                _capturarImagem(context, usarCamera: true);
              },
            ),
            ListTile(
              leading: const Icon(Icons.photo_library),
              title: const Text('Escolher da galeria'),
              onTap: () {
                Navigator.pop(context);
                _capturarImagem(context, usarCamera: false);
              },
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _capturarImagem(
    BuildContext context, {
    required bool usarCamera,
  }) async {
    final ImagemServico servico = context.read<ImagemServico>();
    final PerfilNotifier notifier = context.read<PerfilNotifier>();

    final ResultadoImagem resultado = usarCamera
        ? await servico.capturarFotoComCamera()
        : await servico.selecionarDaGaleria();

    if (!context.mounted) return;

    switch (resultado) {
      case ImagemSelecionada(:final arquivo):
        await notifier.atualizarFotoPerfil(File(arquivo.path));
      case PermissaoNegada():
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Permissão de câmera necessária. '
                'Habilite nas configurações do dispositivo.'),
          ),
        );
      case ErroNaSelecao(:final mensagem):
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(mensagem)),
        );
      case SelecaoCancelada():
        break; // usuário cancelou — nenhuma ação necessária
    }
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:io';
import '../../infraestrutura/imagem/imagem_servico.dart';
import '../../apresentacao/perfil/perfil_notifier.dart';

class WidgetFotoPerfil extends StatelessWidget {
  const WidgetFotoPerfil({super.key});

  @override
  Widget build(BuildContext context) => GestureDetector(
        onTap: () => _opcoes(context),
        child: Stack(children: [
          Consumer<PerfilNotifier>(
            builder: (_, n, __) => CircleAvatar(
              radius: 48,
              backgroundImage:
                  n.urlFotoPerfil != null ? NetworkImage(n.urlFotoPerfil!) : null,
              child: n.urlFotoPerfil == null ? const Icon(Icons.person, size: 48) : null,
            ),
          ),
          const Positioned(
            bottom: 0, right: 0,
            child: CircleAvatar(radius: 14, backgroundColor: Colors.blue,
              child: Icon(Icons.camera_alt, size: 16, color: Colors.white)),
          ),
        ]),
      );

  void _opcoes(BuildContext ctx) => showModalBottomSheet<void>(
        context: ctx,
        builder: (_) => SafeArea(child: Column(mainAxisSize: MainAxisSize.min, children: [
          ListTile(leading: const Icon(Icons.camera_alt), title: const Text('Tirar foto'),
              onTap: () { Navigator.pop(ctx); _capturar(ctx, camera: true);  }),
          ListTile(leading: const Icon(Icons.photo_library), title: const Text('Da galeria'),
              onTap: () { Navigator.pop(ctx); _capturar(ctx, camera: false); }),
        ])),
      );

  Future<void> _capturar(BuildContext ctx, {required bool camera}) async {
    final svc = ctx.read<ImagemServico>();
    final n   = ctx.read<PerfilNotifier>();
    final r   = camera ? await svc.capturarFotoComCamera() : await svc.selecionarDaGaleria();
    if (!ctx.mounted) return;
    switch (r) {
      case ImagemSelecionada(:final arquivo): await n.atualizarFotoPerfil(File(arquivo.path));
      case PermissaoNegada(): ScaffoldMessenger.of(ctx).showSnackBar(
          const SnackBar(content: Text('Permissão negada. Habilite nas configurações.')));
      case ErroNaSelecao(:final mensagem): ScaffoldMessenger.of(ctx).showSnackBar(
          SnackBar(content: Text(mensagem)));
      case SelecaoCancelada(): break;
    }
  }
}

Seção 4 — Geolocalização: Obtendo a Posição do Dispositivo

A geolocalização é o recurso nativo com o maior potencial de transformação da experiência do usuário no delivery. Em vez de digitar o endereço completo a cada pedido, o usuário pode permitir que o aplicativo detecte sua posição atual e preencha automaticamente o endereço de entrega. Essa melhoria aparentemente pequena reduz significativamente o atrito no processo de pedido e aumenta a satisfação do usuário.

O pacote geolocator fornece uma API Dart completa para acesso à geolocalização no Android e iOS, abstraindo as diferenças entre os provedores de localização de cada plataforma.

dependencies:
  geolocator: ^13.0.2

No AndroidManifest.xml, as permissões de localização já foram declaradas na seção anterior. O geolocator também requer a configuração do provedor de localização no arquivo do módulo app no Gradle — consulte a documentação do pacote para a versão exata que você está usando, pois essa configuração varia entre versões.

Provedores de Localização no Android

O Android determina a posição do dispositivo combinando informações de múltiplas fontes de forma transparente para o desenvolvedor. O GPS (Global Positioning System) oferece alta precisão — tipicamente de 1 a 10 metros — mas consome mais bateria e demora mais para fixar um sinal, especialmente em ambientes fechados onde o sinal de satélite é obstruído. O Wi-Fi e o Bluetooth fornecem posicionamento por triangulação das redes e beacons ao redor, com precisão de dezenas a centenas de metros, mas com baixo consumo de bateria e resposta rápida. O sinal celular (torres de telefonia) oferece a menor precisão — centenas de metros a quilômetros — mas está disponível praticamente em qualquer lugar com cobertura de rede.

O Fused Location Provider, gerenciado pelo Google Play Services, combina automaticamente essas fontes baseando-se no nível de precisão solicitado e no estado da bateria do dispositivo. O geolocator usa o Fused Location Provider no Android, o que significa que você especifica apenas o nível de precisão desejado (LocationAccuracy) e o sistema escolhe a combinação mais eficiente de provedores.

Níveis de Precisão

O enum LocationAccuracy oferece seis níveis de precisão, do mais econômico ao mais preciso:

Nível Precisão típica Consumo de bateria Caso de uso no delivery
lowest 3000 m Mínimo Não aplicável
low 1000 m Baixo Não aplicável
medium 100 m Moderado Sugestão de bairro
high 10 m Alto Sugestão de endereço
best 1-5 m Muito alto Navegação turn-by-turn
bestForNavigation < 1 m Máximo Não aplicável no delivery

Para o caso de uso de sugestão de endereço no delivery — onde o usuário quer confirmar o endereço e pode corrigi-lo —, a precisão high é a escolha correta: oferece precisão suficiente para identificar o endereço correto sem o alto consumo de bateria do modo best.

Implementando o Serviço de Localização

import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';

/// Representa uma posição geográfica com coordenadas e metadados.
class PosicaoGeografica {
  final double latitude;
  final double longitude;
  final double precisaoMetros;
  final DateTime timestamp;

  const PosicaoGeografica({
    required this.latitude,
    required this.longitude,
    required this.precisaoMetros,
    required this.timestamp,
  });
}

sealed class ResultadoLocalizacao { const ResultadoLocalizacao(); }
final class LocalizacaoObtida extends ResultadoLocalizacao {
  const LocalizacaoObtida(this.posicao);
  final PosicaoGeografica posicao;
}
final class PermissaoLocalizacaoNegada extends ResultadoLocalizacao {
  const PermissaoLocalizacaoNegada({this.permanente = false});
  final bool permanente;
}
final class ServicoLocalizacaoDesabilitado extends ResultadoLocalizacao {
  const ServicoLocalizacaoDesabilitado();
}

class LocalizacaoServico {
  /// Obtém a posição atual do dispositivo, tratando todos os cenários de erro.
  Future<ResultadoLocalizacao> obterPosicaoAtual() async {
    // 1. Verifica se o serviço de localização está habilitado no sistema
    final bool servicoHabilitado =
        await Geolocator.isLocationServiceEnabled();
    if (!servicoHabilitado) {
      return const ServicoLocalizacaoDesabilitado();
    }

    // 2. Verifica o status da permissão de localização
    LocationPermission permissao = await Geolocator.checkPermission();

    if (permissao == LocationPermission.denied) {
      // Solicita a permissão se ainda não foi definitivamente negada
      permissao = await Geolocator.requestPermission();
    }

    if (permissao == LocationPermission.denied) {
      return const PermissaoLocalizacaoNegada(permanente: false);
    }

    if (permissao == LocationPermission.deniedForever) {
      return const PermissaoLocalizacaoNegada(permanente: true);
    }

    // 3. Obtém a posição atual com alta precisão
    try {
      final Position posicao = await Geolocator.getCurrentPosition(
        locationSettings: const LocationSettings(
          accuracy: LocationAccuracy.high,
          timeLimit: Duration(seconds: 15), // timeout para evitar espera infinita
        ),
      );

      return LocalizacaoObtida(PosicaoGeografica(
        latitude: posicao.latitude,
        longitude: posicao.longitude,
        precisaoMetros: posicao.accuracy,
        timestamp: posicao.timestamp,
      ));
    } on TimeoutException {
      // O dispositivo não conseguiu fixar localização no tempo limite
      return const ServicoLocalizacaoDesabilitado();
    } catch (e) {
      rethrow;
    }
  }
}
import 'package:geolocator/geolocator.dart';

class PosicaoGeografica {
  const PosicaoGeografica({
    required this.latitude, required this.longitude,
    required this.precisaoMetros, required this.timestamp,
  });
  final double latitude, longitude, precisaoMetros;
  final DateTime timestamp;
}

sealed class ResultadoLocalizacao { const ResultadoLocalizacao(); }
final class LocalizacaoObtida         extends ResultadoLocalizacao {
  const LocalizacaoObtida(this.posicao); final PosicaoGeografica posicao; }
final class PermissaoLocalizacaoNegada extends ResultadoLocalizacao {
  const PermissaoLocalizacaoNegada({this.permanente = false}); final bool permanente; }
final class ServicoLocalizacaoDesabilitado extends ResultadoLocalizacao {
  const ServicoLocalizacaoDesabilitado(); }

class LocalizacaoServico {
  Future<ResultadoLocalizacao> obterPosicaoAtual() async {
    if (!await Geolocator.isLocationServiceEnabled()) {
      return const ServicoLocalizacaoDesabilitado();
    }
    var perm = await Geolocator.checkPermission();
    if (perm == LocationPermission.denied) perm = await Geolocator.requestPermission();
    if (perm == LocationPermission.denied) return const PermissaoLocalizacaoNegada();
    if (perm == LocationPermission.deniedForever) {
      return const PermissaoLocalizacaoNegada(permanente: true);
    }
    final p = await Geolocator.getCurrentPosition(
      locationSettings: const LocationSettings(
        accuracy: LocationAccuracy.high, timeLimit: Duration(seconds: 15),
      ),
    );
    return LocalizacaoObtida(PosicaoGeografica(
      latitude: p.latitude, longitude: p.longitude,
      precisaoMetros: p.accuracy, timestamp: p.timestamp,
    ));
  }
}

Convertendo Coordenadas em Endereço: Geocodificação Reversa

Obter a latitude e longitude do dispositivo é apenas o primeiro passo. Para exibir o endereço de entrega sugerido ao usuário, você precisa converter essas coordenadas em um endereço legível — um processo chamado geocodificação reversa. O pacote geocoding oferece essa funcionalidade usando as APIs de geocodificação nativas do Android e iOS:

dependencies:
  geocoding: ^3.0.0
import 'package:geocoding/geocoding.dart';

class LocalizacaoServico {
  // ... continuação

  /// Converte coordenadas geográficas em um endereço postal legível.
  /// Retorna null se não for possível obter um endereço para as coordenadas.
  Future<String?> obterEnderecoParaCoordenadas({
    required double latitude,
    required double longitude,
  }) async {
    try {
      final List<Placemark> resultados = await placemarkFromCoordinates(
        latitude,
        longitude,
        localeIdentifier: 'pt_BR', // formato de endereço em português
      );

      if (resultados.isEmpty) return null;

      // O primeiro resultado é geralmente o mais preciso
      final Placemark local = resultados.first;

      // Monta o endereço no formato "Rua, Número - Bairro, Cidade - UF"
      final StringBuffer endereco = StringBuffer();

      if (local.street != null && local.street!.isNotEmpty) {
        endereco.write(local.street);
      }
      if (local.subLocality != null && local.subLocality!.isNotEmpty) {
        if (endereco.isNotEmpty) endereco.write(' - ');
        endereco.write(local.subLocality);
      }
      if (local.locality != null && local.locality!.isNotEmpty) {
        if (endereco.isNotEmpty) endereco.write(', ');
        endereco.write(local.locality);
      }
      if (local.administrativeArea != null &&
          local.administrativeArea!.isNotEmpty) {
        if (endereco.isNotEmpty) endereco.write(' - ');
        endereco.write(local.administrativeArea);
      }

      return endereco.toString();
    } catch (e) {
      print('Erro na geocodificação reversa: $e');
      return null;
    }
  }
}
import 'package:geocoding/geocoding.dart';

  Future<String?> obterEnderecoParaCoordenadas({
    required double latitude, required double longitude,
  }) async {
    try {
      final marks = await placemarkFromCoordinates(
        latitude, longitude, localeIdentifier: 'pt_BR',
      );
      if (marks.isEmpty) return null;
      final m = marks.first;
      return [m.street, m.subLocality, m.locality, m.administrativeArea]
          .where((s) => s != null && s.isNotEmpty)
          .join(', ');
    } catch (_) {
      return null;
    }
  }

Seção 5 — Atualização Contínua de Posição

A obtenção de uma posição única é suficiente para sugerir o endereço de entrega. Mas há um cenário no delivery onde a posição precisa ser atualizada continuamente: o rastreamento do entregador em tempo real. Em um aplicativo de delivery completo, o entregador tem sua própria interface que transmite sua posição ao backend a cada poucos segundos, e o cliente pode acompanhar a entrega em um mapa ao vivo.

O geolocator oferece o método getPositionStream() que retorna um Stream<Position> com atualizações contínuas de posição. Você pode configurar a frequência mínima de atualização e a distância mínima de deslocamento antes de uma nova posição ser emitida:

import 'dart:async';
import 'package:geolocator/geolocator.dart';

class RastreamentoEntregadorServico {
  StreamSubscription<Position>? _subscricao;

  /// Inicia o rastreamento contínuo de posição.
  /// [aoAtualizarPosicao] é chamada a cada nova posição detectada.
  /// [distanciaMinMetros] define quantos metros o dispositivo deve se mover
  /// antes de uma nova posição ser emitida (padrão: 10 metros).
  void iniciarRastreamento({
    required void Function(PosicaoGeografica posicao) aoAtualizarPosicao,
    double distanciaMinMetros = 10,
  }) {
    // Cancela qualquer rastreamento anterior antes de iniciar um novo
    _subscricao?.cancel();

    final LocationSettings configuracao = AndroidSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: distanciaMinMetros.round(),
      // Em Android, podemos solicitar atualizações mesmo em background
      // apenas se o usuário concedeu a permissão "Sempre"
      foregroundNotificationConfig: const ForegroundNotificationConfig(
        notificationTitle: 'Delivery em andamento',
        notificationText: 'Rastreando sua posição para entrega',
        enableWakeLock: true,
      ),
    );

    _subscricao = Geolocator.getPositionStream(
      locationSettings: configuracao,
    ).listen(
      (Position posicao) {
        aoAtualizarPosicao(PosicaoGeografica(
          latitude: posicao.latitude,
          longitude: posicao.longitude,
          precisaoMetros: posicao.accuracy,
          timestamp: posicao.timestamp,
        ));
      },
      onError: (Object erro) {
        print('Erro no stream de localização: $erro');
      },
    );
  }

  /// Para o rastreamento e libera os recursos.
  void pararRastreamento() {
    _subscricao?.cancel();
    _subscricao = null;
  }
}
import 'dart:async';
import 'package:geolocator/geolocator.dart';

class RastreamentoEntregadorServico {
  StreamSubscription<Position>? _sub;

  void iniciarRastreamento({
    required void Function(PosicaoGeografica) aoAtualizar,
    double distanciaMinMetros = 10,
  }) {
    _sub?.cancel();
    _sub = Geolocator.getPositionStream(
      locationSettings: AndroidSettings(
        accuracy: LocationAccuracy.high,
        distanceFilter: distanciaMinMetros.round(),
        foregroundNotificationConfig: const ForegroundNotificationConfig(
          notificationTitle: 'Delivery em andamento',
          notificationText: 'Rastreando sua posição',
          enableWakeLock: true,
        ),
      ),
    ).listen(
      (p) => aoAtualizar(PosicaoGeografica(
        latitude: p.latitude, longitude: p.longitude,
        precisaoMetros: p.accuracy, timestamp: p.timestamp,
      )),
      onError: (e) => print('Erro no stream de localização: $e'),
    );
  }

  void pararRastreamento() { _sub?.cancel(); _sub = null; }
}

Considerações sobre Bateria no Rastreamento Contínuo

O rastreamento contínuo de posição é intensivo em termos de bateria, especialmente quando usa GPS de alta precisão. Para o aplicativo de delivery, há algumas estratégias para minimizar o impacto:

O parâmetro distanceFilter é o mais importante: ele garante que novas atualizações só sejam emitidas quando o dispositivo se mover pelo menos a distância especificada em metros. Um entregador parado em um semáforo não vai gerar dezenas de atualizações desnecessárias. Um valor de 10 a 20 metros é adequado para rastreamento de entregadores urbanos.

Reduzir a precisão para LocationAccuracy.medium quando uma posição aproximada é suficiente — por exemplo, quando o entregador ainda está a mais de 1 km do destino — e aumentar para LocationAccuracy.high apenas quando está se aproximando do endereço de entrega é uma estratégia de adaptação dinâmica que preserva bateria sem sacrificar a precisão no momento mais importante.


Seção 6 — Sensores do Dispositivo

Os smartphones modernos contêm uma variedade de sensores inerciais que medem as forças físicas atuando sobre o dispositivo. O acelerômetro mede a aceleração linear em três eixos, o giroscópio mede a velocidade angular de rotação, e o magnetômetro mede o campo magnético (funciona como uma bússola). Combinados, esses sensores permitem determinar com precisão a orientação e o movimento do dispositivo no espaço — uma capacidade que aplicativos de navegação, jogos e fitness exploram extensivamente.

O pacote sensors_plus fornece acesso a esses sensores como streams de dados em Dart:

dependencies:
  sensors_plus: ^5.0.1

A Matemática dos Sensores Inerciais

O acelerômetro retorna um vetor tridimensional \vec{a} = (a_x, a_y, a_z) em metros por segundo ao quadrado (m/s²), representando a aceleração total percebida pelo sensor. Em repouso sobre uma superfície plana, o acelerômetro não lê zero — ele lê aproximadamente (0, 0, 9.81), pois a gravidade terrestre atua continuamente com aceleração g \approx 9{,}81 \text{ m/s}^2. Esse é um ponto de confusão comum: o acelerômetro mede a aceleração específica (a diferença entre a aceleração real e a aceleração gravitacional), não a aceleração inercial absoluta.

Para detectar movimento do dispositivo — útil para saber se o entregador está andando, correndo ou parado —, você pode calcular a magnitude da aceleração removendo o componente gravitacional:

\|\vec{a}_{\text{linear}}\| = \sqrt{(a_x - g_x)^2 + (a_y - g_y)^2 + (a_z - g_z)^2}

onde (g_x, g_y, g_z) é o vetor gravidade estimado, geralmente obtido por um filtro passa-baixas sobre as leituras do acelerômetro. Quando \|\vec{a}_{\text{linear}}\| supera um limiar predefinido, o dispositivo está em movimento.

Implementando Detecção de Movimento com o Acelerômetro

import 'dart:async';
import 'dart:math';
import 'package:sensors_plus/sensors_plus.dart';

/// Serviço que usa o acelerômetro para detectar se o entregador está em movimento.
class DetectorMovimentoServico {
  /// Limiar de aceleração em m/s² acima do qual considera-se movimento.
  static const double _limiarMovimento = 1.5;

  /// Constante do filtro passa-baixas para estimar a gravidade.
  /// Quanto menor, mais suavizado o sinal (0 < alpha < 1).
  static const double _alpha = 0.8;

  double _gx = 0, _gy = 0, _gz = 9.81;
  StreamSubscription<AccelerometerEvent>? _subscricao;
  bool _emMovimento = false;

  /// Stream que emite true quando movimento é detectado e false quando o
  /// dispositivo está parado.
  final _controlador = StreamController<bool>.broadcast();
  Stream<bool> get onMovimentoDetectado => _controlador.stream;

  bool get estaEmMovimento => _emMovimento;

  void iniciar() {
    _subscricao = accelerometerEventStream(
      samplingPeriod: const Duration(milliseconds: 100),
    ).listen((AccelerometerEvent evento) {
      // Atualiza a estimativa da gravidade usando filtro passa-baixas
      _gx = _alpha * _gx + (1 - _alpha) * evento.x;
      _gy = _alpha * _gy + (1 - _alpha) * evento.y;
      _gz = _alpha * _gz + (1 - _alpha) * evento.z;

      // Calcula a aceleração linear (removendo a gravidade)
      final double ax = evento.x - _gx;
      final double ay = evento.y - _gy;
      final double az = evento.z - _gz;

      // Calcula a magnitude do vetor de aceleração linear
      final double magnitude = sqrt(ax * ax + ay * ay + az * az);

      final bool movimentoAtual = magnitude > _limiarMovimento;

      // Emite apenas quando o estado de movimento muda
      if (movimentoAtual != _emMovimento) {
        _emMovimento = movimentoAtual;
        _controlador.add(_emMovimento);
      }
    });
  }

  void parar() {
    _subscricao?.cancel();
    _subscricao = null;
  }

  void dispose() {
    parar();
    _controlador.close();
  }
}
import 'dart:async';
import 'dart:math';
import 'package:sensors_plus/sensors_plus.dart';

class DetectorMovimentoServico {
  static const _limiar = 1.5;
  static const _alpha = 0.8;

  double _gx = 0, _gy = 0, _gz = 9.81;
  bool _emMovimento = false;
  StreamSubscription<AccelerometerEvent>? _sub;
  final _ctrl = StreamController<bool>.broadcast();

  Stream<bool> get onMovimentoDetectado => _ctrl.stream;

  void iniciar() {
    _sub = accelerometerEventStream(
      samplingPeriod: const Duration(milliseconds: 100),
    ).listen((e) {
      _gx = _alpha * _gx + (1 - _alpha) * e.x;
      _gy = _alpha * _gy + (1 - _alpha) * e.y;
      _gz = _alpha * _gz + (1 - _alpha) * e.z;
      final mag = sqrt(
        pow(e.x - _gx, 2) + pow(e.y - _gy, 2) + pow(e.z - _gz, 2),
      );
      final moving = mag > _limiar;
      if (moving != _emMovimento) _ctrl.add(_emMovimento = moving);
    });
  }

  void dispose() { _sub?.cancel(); _ctrl.close(); }
}

Seção 7 — Compartilhamento de Conteúdo com Outros Aplicativos

O compartilhamento de conteúdo é um padrão de integração entre aplicativos que tanto o Android quanto o iOS suportam nativamente. No Android, o mecanismo é baseado em Intents — mensagens assíncronas que o sistema usa para permitir que aplicativos se comuniquem entre si. No iOS, o mecanismo equivalente se chama Activity View Controller. O pacote share_plus abstrai ambas as implementações em uma única chamada Dart.

dependencies:
  share_plus: ^10.0.0

Compartilhando Texto e URLs

import 'package:share_plus/share_plus.dart';

class CompartilhamentoServico {
  /// Compartilha um código de referral com outros aplicativos.
  /// O sistema exibe um seletor com os aplicativos disponíveis
  /// (WhatsApp, SMS, e-mail, etc.).
  Future<void> compartilharCodigoReferral({
    required String codigo,
    required String nomeUsuario,
  }) async {
    final String mensagem =
        'Ei! Use meu código $codigo no delivery e ganhe desconto '
        'no seu primeiro pedido! Baixe o app: https://delivery.example.com';

    final ShareResult resultado = await Share.share(
      mensagem,
      subject: 'Código de desconto do delivery',
    );

    switch (resultado.status) {
      case ShareResultStatus.success:
        print('Conteúdo compartilhado com sucesso');
      case ShareResultStatus.dismissed:
        print('Usuário fechou o seletor sem compartilhar');
      case ShareResultStatus.unavailable:
        print('Compartilhamento não disponível neste dispositivo');
    }
  }

  /// Compartilha os detalhes de um pedido como texto simples.
  Future<void> compartilharDetalhesPedido({
    required int pedidoId,
    required double total,
    required String nomeRestaurante,
  }) async {
    final String mensagem =
        'Pedido #$pedidoId no $nomeRestaurante — R\$ ${total.toStringAsFixed(2)}\n'
        'Acompanhe pelo app de delivery!';

    await Share.share(mensagem, subject: 'Meu pedido #$pedidoId');
  }

  /// Compartilha a imagem de um comprovante de pedido.
  Future<void> compartilharImagemComprovante(XFile imagem) async {
    await Share.shareXFiles(
      [imagem],
      text: 'Comprovante do meu pedido no delivery',
    );
  }
}
import 'package:share_plus/share_plus.dart';

class CompartilhamentoServico {
  Future<void> compartilharCodigoReferral({
    required String codigo,
    required String nomeUsuario,
  }) => Share.share(
        'Use meu código $codigo e ganhe desconto no primeiro pedido! '
        'https://delivery.example.com',
        subject: 'Código de desconto',
      );

  Future<void> compartilharDetalhesPedido({
    required int pedidoId,
    required double total,
    required String nomeRestaurante,
  }) => Share.share(
        'Pedido #$pedidoId no $nomeRestaurante — R\$ ${total.toStringAsFixed(2)}',
        subject: 'Meu pedido #$pedidoId',
      );

  Future<void> compartilharImagemComprovante(XFile imagem) =>
      Share.shareXFiles([imagem], text: 'Comprovante do meu pedido');
}

Seção 8 — Boas Práticas para o Gerenciamento de Permissões

As permissões são uma interface entre o seu aplicativo e a privacidade do usuário. Tratá-las descuidadamente — solicitando-as no momento errado, sem explicação ou reagindo mal à negativa — afasta usuários e prejudica a reputação do aplicativo. As boas práticas que você vai seguir no delivery são as mesmas usadas pelos melhores aplicativos do mercado.

Solicitar no Momento Certo, com Contexto

A regra mais importante é contextualizar o pedido de permissão. O usuário deve entender por que o aplicativo precisa de um determinado acesso antes de ver o diálogo do sistema. Isso se chama rationale (justificativa), e o Android recomenda explicitamente que os aplicativos a forneçam antes de solicitar permissões que o usuário pode não entender imediatamente.

O método shouldShowRequestPermissionRationale() do geolocator (e equivalentes no permission_handler) retorna true quando o Android detectou que o usuário negou a permissão anteriormente, indicando que uma justificativa adicional seria apropriada. Em outras situações — na primeira vez que a permissão é solicitada ou quando já foi permanentemente negada — ele retorna false.

O momento ideal para solicitar cada permissão no delivery é o seguinte. A permissão de localização deve ser solicitada no momento em que o usuário toca no campo de endereço de entrega e seleciona a opção “Usar localização atual”. A permissão de câmera deve ser solicitada quando o usuário toca na foto de perfil e seleciona a opção de câmera. A permissão de notificações — que você implementou no Módulo 12 — deve ser solicitada após a confirmação do primeiro pedido. Nenhuma dessas permissões deve ser solicitada na tela de splash ou logo após o login.

Nunca Bloquear a Interface por Permissão Negada

Quando o usuário nega uma permissão, o aplicativo deve continuar funcionando normalmente em todas as funcionalidades que não dependem daquela permissão. A funcionalidade específica que requer a permissão deve ser desabilitada ou substituída por uma alternativa — no caso da localização para endereço de entrega, o usuário pode digitar o endereço manualmente. O aplicativo jamais deve exibir uma tela de bloqueio ou impedir a navegação por causa de uma permissão negada.

Tratar a Negação Permanente com Diálogo de Orientação

Quando a permissão está permanentemente negada, você não pode exibir o diálogo do sistema — ele simplesmente não aparecerá. O único caminho para o usuário conceder a permissão é através das configurações do sistema. Nesses casos, exiba um diálogo explicativo com um botão que abre as configurações:

Future<void> exibirDialogoPermissaoNegada(BuildContext context) async {
  final bool? irParaConfiguracoes = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Permissão necessária'),
      content: const Text(
        'Para usar sua localização atual, ative o acesso à '
        'localização nas configurações do aplicativo.',
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(false),
          child: const Text('Cancelar'),
        ),
        FilledButton(
          onPressed: () => Navigator.of(context).pop(true),
          child: const Text('Abrir configurações'),
        ),
      ],
    ),
  );

  if (irParaConfiguracoes == true) {
    await openAppSettings();
  }
}
Future<void> exibirDialogoPermissaoNegada(BuildContext ctx) async {
  final ir = await showDialog<bool>(
    context: ctx,
    builder: (_) => AlertDialog(
      title: const Text('Permissão necessária'),
      content: const Text('Ative o acesso à localização nas configurações.'),
      actions: [
        TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')),
        FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Configurações')),
      ],
    ),
  );
  if (ir == true) openAppSettings();
}

Seção 9 — Integrando Recursos Nativos na Arquitetura do Projeto

Os serviços de imagem, localização, sensores e compartilhamento que você criou neste módulo seguem o mesmo padrão arquitetural que os repositórios HTTP e os serviços Firebase dos módulos anteriores: eles vivem na camada de infraestrutura, implementam interfaces definidas na camada de domínio, e são injetados nos providers e widgets através do GetIt.

A camada de domínio define o que o sistema precisa fazer com os recursos nativos — por exemplo, ILocalizacaoRepositorio com métodos obterPosicaoAtual() e obterEnderecoAtual() — sem nenhuma referência ao geolocator ou ao geocoding. A camada de infraestrutura fornece a implementação concreta com GeolocatorLocalizacaoRepositorio, que usa esses pacotes internamente.

Essa separação tem consequências práticas concretas para o desenvolvimento. Durante os testes unitários do EnderecoNotifier — o provider que usa a localização para sugerir endereços —, você substitui o GeolocatorLocalizacaoRepositorio por um mock que retorna posições predefinidas, sem precisar de GPS real. Isso permite testar todos os cenários de resposta da funcionalidade de endereço (posição obtida, permissão negada, serviço desabilitado) de forma rápida e repetível.

O EnderecoNotifier: Um Exemplo de Provider com Recursos Nativos

import 'package:flutter/foundation.dart';
import '../../dominio/localizacao/i_localizacao_repositorio.dart';

enum EstadoEndereco {
  inicial,
  carregando,
  sucesso,
  permissaoNegada,
  permissaoPermanentementeNegada,
  servicoDesabilitado,
  erro,
}

class EnderecoNotifier extends ChangeNotifier {
  final ILocalizacaoRepositorio _localizacaoRepositorio;

  EnderecoNotifier({required ILocalizacaoRepositorio localizacaoRepositorio})
      : _localizacaoRepositorio = localizacaoRepositorio;

  EstadoEndereco _estado = EstadoEndereco.inicial;
  String? _enderecoSugerido;
  double? _latitudeSugerida;
  double? _longitudeSugerida;

  EstadoEndereco get estado => _estado;
  String? get enderecoSugerido => _enderecoSugerido;

  /// Obtém a posição atual e converte em endereço para sugerir ao usuário.
  Future<void> sugerirEnderecoAtual() async {
    _estado = EstadoEndereco.carregando;
    notifyListeners();

    final ResultadoLocalizacao resultado =
        await _localizacaoRepositorio.obterPosicaoAtual();

    switch (resultado) {
      case LocalizacaoObtida(:final posicao):
        _latitudeSugerida = posicao.latitude;
        _longitudeSugerida = posicao.longitude;
        _enderecoSugerido = await _localizacaoRepositorio.obterEnderecoParaCoordenadas(
          latitude: posicao.latitude,
          longitude: posicao.longitude,
        );
        _estado = EstadoEndereco.sucesso;

      case PermissaoLocalizacaoNegada(permanente: final p):
        _estado = p
            ? EstadoEndereco.permissaoPermanentementeNegada
            : EstadoEndereco.permissaoNegada;

      case ServicoLocalizacaoDesabilitado():
        _estado = EstadoEndereco.servicoDesabilitado;
    }

    notifyListeners();
  }
}
import 'package:flutter/foundation.dart';
import '../../dominio/localizacao/i_localizacao_repositorio.dart';

enum EstadoEndereco { inicial, carregando, sucesso, permissaoNegada, permissaoPermanente, servicoOff, erro }

class EnderecoNotifier extends ChangeNotifier {
  EnderecoNotifier({required ILocalizacaoRepositorio repo}) : _repo = repo;
  final ILocalizacaoRepositorio _repo;

  var _estado = EstadoEndereco.inicial;
  String? enderecoSugerido;

  EstadoEndereco get estado => _estado;
  void _set(EstadoEndereco e) { _estado = e; notifyListeners(); }

  Future<void> sugerirEnderecoAtual() async {
    _set(EstadoEndereco.carregando);
    final r = await _repo.obterPosicaoAtual();
    switch (r) {
      case LocalizacaoObtida(:final posicao):
        enderecoSugerido = await _repo.obterEnderecoParaCoordenadas(
          latitude: posicao.latitude, longitude: posicao.longitude,
        );
        _set(EstadoEndereco.sucesso);
      case PermissaoLocalizacaoNegada(:final permanente):
        _set(permanente ? EstadoEndereco.permissaoPermanente : EstadoEndereco.permissaoNegada);
      case ServicoLocalizacaoDesabilitado():
        _set(EstadoEndereco.servicoOff);
    }
  }
}

Seção 10 — Visão Integrada: Recursos Nativos no Fluxo do Delivery

Para fechar o módulo com uma visão coesa do que você construiu, vale a pena mapear todos os recursos nativos às funcionalidades específicas do aplicativo de delivery e entender como eles colaboram para criar uma experiência de usuário fluida e completa.

graph TD
    subgraph "Fluxo do Cliente"
        A[Abrir app] --> B{Sessão ativa?}
        B -->|Sim| C[Tela principal]
        B -->|Não| D[Tela de login - Módulo 11]
        C --> E[Fazer pedido]
        E --> F[Campo de endereço]
        F --> G[Toca em 'usar localização']
        G --> H[LocalizacaoServico.obterPosicaoAtual]
        H --> I[Sugestão de endereço]
        E --> J[Confirma pedido]
        J --> K[Notificação push - Módulo 12]
    end
    subgraph "Fluxo do Perfil"
        C --> L[Editar perfil]
        L --> M[Foto de perfil]
        M --> N[ImagemServico.capturarFoto]
        M --> O[ImagemServico.selecionarGaleria]
    end
    subgraph "Fluxo de Compartilhamento"
        C --> P[Código referral]
        P --> Q[CompartilhamentoServico.compartilharCodigo]
        Q --> R[WhatsApp / SMS / Email]
    end

O diagrama mostra que os recursos nativos deste módulo se integram ao fluxo principal do delivery de forma não intrusiva: a geolocalização aparece como uma opção no campo de endereço (não é forçada), a câmera aparece como uma opção na edição de perfil, e o compartilhamento aparece como uma ação acessível mas não obrigatória. Esse é o design correto — os recursos nativos ampliam as capacidades do aplicativo, mas não são obstáculos para quem preferir não os usar.

A tabela a seguir consolida todos os recursos nativos estudados, suas permissões correspondentes e seus casos de uso no delivery:

Recurso Pacote Permissão Android Caso de uso no delivery
Câmera image_picker CAMERA Foto de perfil, foto de entrega
Galeria image_picker READ_MEDIA_IMAGES Selecionar foto de perfil
Localização (uma vez) geolocator ACCESS_FINE_LOCATION Sugerir endereço de entrega
Localização (contínua) geolocator ACCESS_FINE_LOCATION Rastrear entregador
Geocodificação reversa geocoding Nenhuma (usa API) Converter GPS em endereço
Acelerômetro sensors_plus Nenhuma (sensor passivo) Detectar entregador parado
Compartilhamento share_plus Nenhuma Código referral, comprovante

Resumo do Módulo

Neste módulo, você abriu o acesso ao hardware do smartphone, transformando o aplicativo de delivery de uma interface web disfarçada em uma experiência verdadeiramente móvel. A câmera permite que o usuário atualize sua foto de perfil com um toque; a geolocalização elimina a digitação do endereço de entrega na maioria dos pedidos; o acelerômetro revela o estado de movimento do entregador; e o compartilhamento conecta o aplicativo ao ecossistema de comunicação do usuário.

O fio condutor de todo o módulo foi o sistema de permissões. Você compreendeu a evolução do modelo de permissões do Android — de permissões na instalação para permissões em tempo de execução, com controles cada vez mais granulares a cada versão do sistema. Entendeu os quatro estados possíveis de uma permissão (granted, denied, permanentlyDenied, restricted) e implementou um PermissaoServico que trata cada um desses estados de forma apropriada. Aprendeu que o momento e o contexto da solicitação de permissão são tão importantes quanto a implementação técnica — solicitar permissões no momento errado, sem explicação, é a causa mais comum de negativas.

Os serviços LocalizacaoServico, ImagemServico, RastreamentoEntregadorServico, DetectorMovimentoServico e CompartilhamentoServico seguem o mesmo padrão arquitetural hexagonal que você vem aplicando desde o Módulo 07: interfaces no domínio, implementações na infraestrutura, injeção de dependência pelo GetIt. Essa consistência é um sinal de que a arquitetura está funcionando — novos recursos se encaixam no sistema sem forçar alterações nas camadas que não lhes dizem respeito.

No Módulo 14, você vai garantir que o aplicativo de delivery seja acessível a usuários de diferentes idiomas e com diferentes necessidades de acessibilidade. A internacionalização com o pacote i18n_extension tornará possível oferecer o delivery em português e inglês sem duplicar o código da interface, e os recursos de acessibilidade garantirão que usuários com deficiência visual possam usar o aplicativo com a mesma fluidez que qualquer outro usuário.