Voltar para todos os artigos
Projeto de Síntese: Arquitetando um App Real do Zero

Projeto de Síntese: Arquitetando um App Real do Zero

Blueprint prático para construir um app Android completo do zero: GitHub Repository Finder. Integra Kotlin idiomático, motor Android, MVVM, Retrofit e...

Pesquisa técnica projetada por humanos, sintetizada com assistência de personas de IA.
11 min de leitura

TL;DR / Sumário Executivo

Blueprint prático para construir um app Android completo do zero: GitHub Repository Finder. Integra Kotlin idiomático, motor Android, MVVM, Retrofit e...

💡 TL;DR (Resumo)

  • Projeto GitHub Repository Finder: App completo que busca usuários GitHub, lista repositórios e mostra detalhes, exercitando todos os conceitos aprendidos
  • Camada de Dados: Retrofit para networking com suspend fun, Repository pattern para abstração, Kotlin data classes para modelos
  • Camada de Apresentação: ViewModel com StateFlow para gerenciar estado, sealed class para estados da UI, viewModelScope para corrotinas
  • Camada de Visualização: Fragments com View Binding, flowWithLifecycle para observação segura, RecyclerView com ListAdapter para performance
  • Takeaway Principal: Este projeto é a prova de fogo que solidifica Kotlin idiomático + Motor Android + MVVM em uma arquitetura real, completa e profissional

Tempo de Leitura Estimado: 30 minutos

Você percorreu um caminho intenso. Dominou a sintaxe idiomática de Kotlin, entendeu o motor rígido do Android e aprendeu a arquitetura MVVM que o protege. Agora, é hora de juntar tudo.

Este artigo não é mais um tutorial. É um blueprint arquitetônico para um projeto de síntese que você construirá nos próximos 2-3 dias. O objetivo não é apenas criar um app que funcione, mas construir um app que seja bem arquitetado, testável e reflita as melhores práticas modernas que exploramos.

O Projeto: GitHub Repository Finder

Um app simples, mas completo o suficiente para exercitar todos os conceitos-chave. Ele permitirá que um usuário busque um perfil no GitHub, visualize seus repositórios públicos e veja os detalhes de cada um.


Especificação Funcional do App

  1. Tela de Busca: Uma tela inicial com um EditText para o nome de usuário do GitHub e um botão "Buscar".
  2. Tela de Lista de Repositórios: Após a busca, o app navega para uma tela que exibe uma lista dos repositórios públicos do usuário. Cada item da lista deve mostrar o nome e a descrição do repositório.
  3. Tela de Detalhes do Repositório: Ao clicar em um repositório da lista, o app navega para uma tela de detalhes, mostrando:
    • Nome completo
    • Descrição
    • Linguagem principal
    • Contagem de estrelas (stars)
    • Contagem de forks
  4. Tratamento de Estados: O app deve lidar elegantemente com os estados de carregamento (mostrar um ProgressBar), sucesso (mostrar os dados) e erro (mostrar uma mensagem de erro).

Blueprint Arquitetônico: Camada por Camada

Vamos dissecar a estrutura do projeto, aplicando cada bloco de conhecimento.

Camada 1: Dados (The Model)

Esta camada é responsável por obter e gerenciar os dados, completamente isolada da UI.

1.1. Modelos de Dados (Kotlin Idioms)

Crie data classes para representar a resposta da API do GitHub. Use a API do GitHub como referência.

kotlin
// data/models/Repo.kt data class Repo( val id: Long, val name: String, val fullName: String, val description: String?, val language: String?, val stargazersCount: Int, val forksCount: Int ) // data/models/User.kt data class User( val id: Long, val login: String, val name: String?, val publicRepos: Int )

Conceito Aplicado: data classes para modelos de dados limpos e concisos. Kotlin gera automaticamente equals(), hashCode(), toString(), copy().

1.2. Fonte de Dados Remota (Retrofit + Corrotinas)

Configure o Retrofit para se comunicar com a API do GitHub. A chave aqui é usar suspend fun para tornar as chamadas de rede assíncronas e integradas às corrotinas.

kotlin
// network/GitHubApiService.kt interface GitHubApiService { @GET("users/{user}/repos") suspend fun listRepositories(@Path("user") user: String): List<Repo> @GET("repos/{owner}/{repo}") suspend fun getRepositoryDetails( @Path("owner") owner: String, @Path("repo") repo: String ): Repo }

Conceito Aplicado: Retrofit para networking e suspend fun para operações assíncronas integradas com corrotinas.

1.3. O Repositório (The Repository Pattern)

Crie uma classe GitHubRepository que servirá como a única fonte de verdade para a UI. Ela decidirá de onde vêm os dados.

kotlin
// data/GitHubRepository.kt class GitHubRepository(private val apiService: GitHubApiService) { // A função é suspensa para ser chamada de uma corrotina no ViewModel suspend fun getUserRepositories(username: String): Result<List<Repo>> { return try { val repos = apiService.listRepositories(username) Result.success(repos) } catch (e: Exception) { Result.failure(e) } } suspend fun getRepositoryDetails( owner: String, repoName: String ): Result<Repo> { return try { val repo = apiService.getRepositoryDetails(owner, repoName) Result.success(repo) } catch (e: Exception) { Result.failure(e) } } }

Conceito Aplicado: O padrão Repository para abstrair a fonte de dados e gerenciar erros de forma centralizada.


Camada 2: Apresentação (The ViewModel)

Esta camada contém a lógica de negócio da UI e expõe os dados de forma reativa.

2.1. Gerenciamento de Estado da UI (StateFlow + Sealed Class)

Use uma sealed class para representar os diferentes estados da UI. Isso é muito mais seguro e type-safe do que usar booleanos ou enums.

kotlin
// ui/RepoUiState.kt sealed class RepoUiState { object Idle : RepoUiState() object Loading : RepoUiState() data class Success(val repos: List<Repo>) : RepoUiState() data class Error(val message: String) : RepoUiState() } // ui/RepoDetailUiState.kt sealed class RepoDetailUiState { object Idle : RepoDetailUiState() object Loading : RepoDetailUiState() data class Success(val repo: Repo) : RepoDetailUiState() data class Error(val message: String) : RepoDetailUiState() }

2.2. O ViewModel em Ação

Crie um RepoListViewModel que depende do GitHubRepository.

kotlin
// ui/RepoListViewModel.kt class RepoListViewModel(private val repository: GitHubRepository) : ViewModel() { private val _uiState = MutableStateFlow<RepoUiState>(RepoUiState.Idle) val uiState: StateFlow<RepoUiState> = _uiState.asStateFlow() fun searchRepositories(username: String) { if (username.isBlank()) return viewModelScope.launch { _uiState.value = RepoUiState.Loading val result = repository.getUserRepositories(username) _uiState.value = when { result.isSuccess -> RepoUiState.Success(result.getOrNull() ?: emptyList()) result.isFailure -> RepoUiState.Error("Falha ao buscar repositórios.") } } } } // ui/RepoDetailViewModel.kt class RepoDetailViewModel(private val repository: GitHubRepository) : ViewModel() { private val _uiState = MutableStateFlow<RepoDetailUiState>(RepoDetailUiState.Idle) val uiState: StateFlow<RepoDetailUiState> = _uiState.asStateFlow() fun loadRepositoryDetails(owner: String, repoName: String) { viewModelScope.launch { _uiState.value = RepoDetailUiState.Loading val result = repository.getRepositoryDetails(owner, repoName) _uiState.value = when { result.isSuccess -> RepoDetailUiState.Success(result.getOrNull()!!) result.isFailure -> RepoDetailUiState.Error("Falha ao carregar detalhes.") } } } }

Conceito Aplicado: ViewModel para sobreviver a mudanças de configuração, StateFlow para reatividade, viewModelScope para gerenciar corrotinas, sealed class para estados type-safe.


Camada 3: Visualização (The View)

Esta camada consiste em Fragments que observam o ViewModel e renderizam a UI.

3.1. Tela de Busca (Fragment + View Binding)

kotlin
// ui/SearchFragment.kt class SearchFragment : Fragment(R.layout.fragment_search) { private var _binding: FragmentSearchBinding? = null private val binding get() = _binding!! private val viewModel: RepoListViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentSearchBinding.bind(view) binding.searchButton.setOnClickListener { val username = binding.usernameEditText.text.toString() viewModel.searchRepositories(username) } setupObservers() } private fun setupObservers() { viewLifecycleOwner.lifecycleScope.launch { viewModel.uiState .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) .collect { state -> when (state) { is RepoUiState.Loading -> { binding.progressBar.isVisible = true binding.errorMessage.isVisible = false } is RepoUiState.Success -> { binding.progressBar.isVisible = false // Navegar para a lista de repositórios val action = SearchFragmentDirections .actionSearchToRepoList(state.repos.toTypedArray()) findNavController().navigate(action) } is RepoUiState.Error -> { binding.progressBar.isVisible = false binding.errorMessage.isVisible = true binding.errorMessage.text = state.message } RepoUiState.Idle -> {} } } } } override fun onDestroyView() { super.onDestroyView() _binding = null } }

Conceito Aplicado: Fragment para UI modular, View Binding para acesso seguro às views, observação com flowWithLifecycle para uma conexão reativa e lifecycle-aware.

3.2. Tela de Lista (RecyclerView + ListAdapter)

Use ListAdapter para comparar automaticamente as diferenças entre listas usando DiffUtil.

kotlin
// ui/RepoListAdapter.kt class RepoListAdapter( private val onItemClick: (Repo) -> Unit ) : ListAdapter<Repo, RepoListAdapter.RepoViewHolder>(RepoDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoViewHolder { val binding = ItemRepoBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return RepoViewHolder(binding, onItemClick) } override fun onBindViewHolder(holder: RepoViewHolder, position: Int) { holder.bind(getItem(position)) } class RepoViewHolder( private val binding: ItemRepoBinding, private val onItemClick: (Repo) -> Unit ) : RecyclerView.ViewHolder(binding.root) { fun bind(repo: Repo) { binding.repoName.text = repo.name binding.repoDescription.text = repo.description ?: "Sem descrição" binding.root.setOnClickListener { onItemClick(repo) } } } class RepoDiffCallback : DiffUtil.ItemCallback<Repo>() { override fun areItemsTheSame(oldItem: Repo, newItem: Repo) = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Repo, newItem: Repo) = oldItem == newItem } } // ui/RepoListFragment.kt class RepoListFragment : Fragment(R.layout.fragment_repo_list) { private var _binding: FragmentRepoListBinding? = null private val binding get() = _binding!! private val viewModel: RepoListViewModel by viewModels() private lateinit var adapter: RepoListAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentRepoListBinding.bind(view) adapter = RepoListAdapter { repo -> val action = RepoListFragmentDirections .actionRepoListToRepoDetail(repo.fullName) findNavController().navigate(action) } binding.recyclerView.adapter = adapter setupObservers() } private fun setupObservers() { // A lista é passada via arguments do Navigation val repos = RepoListFragmentArgs.fromBundle(requireArguments()).repos.toList() adapter.submitList(repos) } override fun onDestroyView() { super.onDestroyView() _binding = null } }

Conceito Aplicado: ListAdapter com DiffUtil para performance otimizada, RecyclerView para listas grandes, View Binding para acesso seguro às views.


O Fluxo Completo: Do Click à Tela

Vamos traçar o caminho completo de uma ação do usuário:

  1. Usuário digita "google" e clica em "Buscar".

  2. SearchFragment: O OnClickListener do botão chama viewModel.searchRepositories("google").

  3. RepoListViewModel: A função searchRepositories é chamada. Ela atualiza o _uiState para Loading e inicia uma corrotina em viewModelScope.

  4. Dentro da Corrotina: O ViewModel chama repository.getUserRepositories("google").

  5. GitHubRepository: A função getUserRepositories chama apiService.listRepositories("google").

  6. Retrofit: Faz a chamada HTTP à API do GitHub em uma thread de I/O (graças ao suspend).

  7. Resposta da API: A API retorna uma lista de repositórios. O Retrofit a desserializa em List<Repo>.

  8. GitHubRepository: Recebe a lista, a envolve em Result.success() e a retorna.

  9. RepoListViewModel: Recebe o Result, atualiza o _uiState para RepoUiState.Success(repos).

  10. SearchFragment: O coletor flowWithLifecycle que está observando o uiState é acionado com o novo estado Success.

  11. Navegação: O bloco when navega para a tela de lista de repositórios, passando os repos como argumentos.

  12. Renderização: A lista é exibida via RecyclerView com ListAdapter, que calcula as diferenças com DiffUtil e anima apenas os itens que mudaram.

O Milagre da Rotação

Se o usuário rotacionar a tela durante o carregamento ou após a exibição dos dados:

  • O Fragment é destruído e recriado.
  • Ele se reconecta ao mesmo RepoListViewModel, que ainda contém o último estado.
  • A UI é repintada instantaneamente, sem nenhuma chamada de rede.
  • A experiência é perfeita.

Estrutura de Arquivos Recomendada

app/src/main/java/com/example/githubrepofinder/
├── data/
│   ├── models/
│   │   ├── Repo.kt
│   │   └── User.kt
│   ├── network/
│   │   └── GitHubApiService.kt
│   └── GitHubRepository.kt
├── ui/
│   ├── search/
│   │   └── SearchFragment.kt
│   ├── repolist/
│   │   ├── RepoListFragment.kt
│   │   ├── RepoListViewModel.kt
│   │   └── RepoListAdapter.kt
│   ├── repodetail/
│   │   ├── RepoDetailFragment.kt
│   │   └── RepoDetailViewModel.kt
│   ├── MainActivity.kt
│   ├── RepoUiState.kt
│   └── RepoDetailUiState.kt
└── app/
    └── (Configurações do Gradle, strings, styles, etc.)

Próximas Evoluções: Do Protótipo ao Profissional

Construir este projeto do zero, seguindo este blueprint, é um marco. Ao completar este projeto, você terá aplicado de forma integrada:

  • Kotlin Idioms: data class, suspend fun, Scope Functions.
  • Motor Android: Gradle, Ciclo de Vida, View Binding, Navegação.
  • Arquitetura MVVM: ViewModel, StateFlow, Repository, Retrofit, RecyclerView.

A partir daqui, o caminho para a especialização se torna claro:

  1. Persistência Local: Integre Room para implementar "Favoritos" com banco de dados local.
  2. Injeção de Dependência: Use Hilt para remover instanciação manual e desacoplar ainda mais.
  3. Testes: Escreva testes de unidade para ViewModel (com Repository falso) e testes de UI com Espresso.
  4. Jetpack Compose: Reescreva uma tela usando a nova toolkit declarativa de UI do Android.

Conclusão: Você Agora É um Arquiteto Android

Parabéns. Você não apenas aprendeu a sintaxe de Kotlin, o motor do Android e o padrão MVVM. Você integrou tudo isso em um projeto coerente e profissional. Você não está mais seguindo tutoriais; você está arquitetando soluções.

Este é o fim do começo. O próximo passo é a especialização, e agora você tem a base sólida para escolher seu caminho: desenvolvimento de features mais complexas, testes de qualidade industrial, performance optimization, ou qualquer outra especialização que the interesse.

A jornada de um engenheiro Android nunca termina. Mas você agora tem o mapa e as ferramentas certas.

Receba novos artigos

Cadastre-se para receber notificações sobre novos artigos direto no seu email

Não enviaremos spam. Você pode cancelar a inscrição a qualquer momento.