Encontre a partida perfeita!
Aplicativo mobile em Flutter para conectar pessoas que querem jogar futebol com partidas próximas.
- Aluno: Kaio Vinicius Corredor da Silva
- Vídeo demonstrativo no YouTube: https://www.youtube.com/watch?v=y-C73NpZy7Y
- Sobre o projeto
- Objetivo
- Funcionalidades
- Telas do aplicativo
- Arquitetura e tecnologias
- Estrutura de pastas
- Aplicação dos requisitos
- Injeção de Dependência
O FutJá é um aplicativo mobile desenvolvido em Flutter para facilitar a organização e a participação em partidas de futebol amador.
A proposta do app é conectar pessoas que querem jogar futebol com partidas disponíveis em sua cidade, permitindo que os usuários encontrem jogos, criem novas partidas, confirmem presença, visualizem detalhes da partida e gerenciem seu próprio perfil.
O projeto foi estruturado com Flutter + Firebase, utilizando MVVM, Provider para gerenciamento de estado e injeção de dependência, além de uma separação clara entre interface, lógica de apresentação e acesso aos dados.
O principal objetivo do aplicativo é oferecer uma experiência simples e funcional para:
- organizar partidas de futebol amador;
- permitir a criação de jogos com informações completas;
- facilitar a busca por partidas disponíveis por cidade;
- permitir a entrada e saída de participantes de forma dinâmica;
- gerenciar o perfil do usuário;
- manter uma estrutura preparada para notificações push com Firebase Cloud Messaging.
- Cadastro com e-mail e senha usando Firebase Authentication.
- Login com e-mail e senha.
- Controle do estado de autenticação com
AuthViewModel. - Logout do usuário.
- Salvamento do token FCM do usuário para futuras notificações.
- Criação e atualização do documento do usuário na coleção
users.
- Listagem de partidas futuras.
- Filtro de partidas por cidade.
- Exibição de informações resumidas em cards:
- nome da partida;
- local;
- data e horário;
- nível técnico;
- número de vagas disponíveis;
- imagem do local, quando cadastrada.
- Criação de partidas com:
- nome da partida;
- cidade;
- local;
- data e horário;
- nível técnico;
- número de vagas;
- imagem opcional do local.
- Persistência dos dados da partida no Cloud Firestore.
- Estratégia de salvamento de imagem desacoplada da interface.
- Visualização completa de uma partida.
- Exibição da lista de jogadores confirmados.
- Entrada e saída da partida.
- Cancelamento da partida pelo organizador.
- Exibição do nome e da foto do usuário confirmado quando disponíveis.
- Edição de:
- nome;
- posição em que joga;
- idade;
- peso;
- foto de perfil.
- Salvamento das informações do perfil no Firestore.
- Exibição da foto do usuário na interface.
- Inicialização do Firebase Messaging.
- Configuração para receber notificações:
- em foreground;
- em background;
- com o app encerrado.
- Estrutura pronta para futuras notificações relacionadas às partidas.
O aplicativo possui múltiplas telas funcionais para diferentes fluxos de uso.
Tela inicial exibida ao abrir o aplicativo.
Tela de autenticação com e-mail e senha.
Tela principal do app, com:
- aba para visualizar partidas;
- aba para criar partidas;
- menu lateral com acesso ao perfil e logout.
Tela com informações completas da partida e ações de participação.
Tela para edição de dados do usuário.
Tela dedicada para cadastro de uma nova partida.
- Flutter
- Dart
- Provider
- Firebase Core
- Firebase Auth
- Cloud Firestore
- Firebase Messaging
- Image Picker
O projeto utiliza uma arquitetura MVVM (Model-View-ViewModel), organizada da seguinte forma:
- View: telas e widgets responsáveis pela interface;
- ViewModel: classes responsáveis pelo estado e pela lógica de apresentação;
- Model: entidades de domínio da aplicação;
- Service: camada responsável pela integração com Firebase e persistência dos dados.
O fluxo principal da aplicação segue esta estrutura:
View → ViewModel → Service → Firebase
- A View exibe dados e captura interações do usuário.
- O ViewModel controla estado, loading, mensagens de erro e regras de fluxo.
- O Service concentra operações externas, como autenticação, leitura e escrita no banco.
- Os Models representam os dados utilizados na aplicação.
Essa organização ajuda a manter o projeto mais limpo, modular e fácil de evoluir.
A injeção de dependência no projeto foi implementada com Provider, utilizando MultiProvider no arquivo main.dart.
No ponto de entrada da aplicação, as dependências principais são registradas de forma global. Isso permite que os serviços e ViewModels sejam disponibilizados para toda a árvore de widgets sem que precisem ser instanciados manualmente em cada tela.
No main.dart, são registradas dependências como:
IAuthServiceIMatchServiceIStorageServiceIProfileServiceAuthViewModel
Essas dependências são disponibilizadas através do Provider e depois consumidas nas telas e nos widgets com context.read(), context.watch() e Consumer.
O fluxo da injeção de dependência acontece da seguinte forma:
- O app inicia no
main.dart; - o
MultiProviderregistra as dependências principais; - essas dependências ficam disponíveis para a aplicação;
- as telas acessam essas dependências com
context.read()oucontext.watch(); - os ViewModels recebem os serviços via construtor;
- os ViewModels utilizam esses serviços para executar autenticação, leitura e escrita de dados.
main.dartregistraIAuthService;AuthViewModelrecebeIAuthServiceno construtor;LoginPageconsomeAuthViewModelpara executar login e cadastro.
Nesse fluxo, a tela não acessa o Firebase diretamente.
Ela delega a ação para o ViewModel, que por sua vez utiliza o serviço injetado.
HomePageacessaIMatchService;MatchListViewModelrecebeIMatchServiceno construtor;- o ViewModel utiliza esse serviço para carregar, filtrar e observar partidas.
MatchFormViewModelrecebeIMatchServiceeIStorageService;- a lógica de criação da partida fica concentrada no ViewModel;
- a tela apenas envia os dados preenchidos no formulário.
ProfileViewModelrecebeIProfileService;- a tela de perfil não conhece os detalhes de persistência;
- o ViewModel delega a leitura e a gravação dos dados ao serviço injetado.
No projeto, os ViewModels não criam diretamente os serviços que utilizam.
Em vez disso, essas dependências são fornecidas de fora para dentro da aplicação.
Isso caracteriza injeção de dependência porque:
- a classe não depende da criação manual da dependência;
- a dependência é recebida externamente;
- a responsabilidade de montagem do objeto fica centralizada na configuração da aplicação.
A aplicação dessa abordagem trouxe os seguintes benefícios:
- redução de acoplamento entre classes;
- maior modularidade;
- melhor organização do projeto;
- facilidade de manutenção;
- facilidade para testes unitários;
- possibilidade de trocar implementações com menos impacto.
Em resumo, a injeção de dependência no projeto foi aplicada para desacoplar os ViewModels da criação direta dos serviços, deixando a arquitetura mais limpa, mais organizada e mais fácil de evoluir.
lib/
├─ core/
│ ├─ app_constants.dart
│ ├─ app_theme.dart
│ ├─ auth_error_mapper.dart
│ ├─ date_time_formatter.dart
│ └─ form_validators.dart
│
├─ models/
│ ├─ app_user.dart
│ ├─ match.dart
│ └─ user_profile.dart
│
├─ services/
│ ├─ auth_service.dart
│ ├─ match_service.dart
│ ├─ profile_service.dart
│ └─ storage_service.dart
│
├─ viewmodels/
│ ├─ auth_view_model.dart
│ ├─ match_form_view_model.dart
│ ├─ match_list_view_model.dart
│ └─ profile_view_model.dart
│
├─ views/
│ ├─ home_page.dart
│ ├─ login_page.dart
│ ├─ match_detail_page.dart
│ ├─ match_form_page.dart
│ └─ profile_page.dart
│
├─ widgets/
│ └─ match_form_widget.dart
│
├─ app.dart
└─ main.dart