
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...
✨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
- Tela de Busca: Uma tela inicial com um
EditTextpara o nome de usuário do GitHub e um botão "Buscar". - 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.
- 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
- 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.
// 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.
// 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.
// 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.
// 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.
// 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)
// 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.
// 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:
-
Usuário digita "google" e clica em "Buscar".
-
SearchFragment: OOnClickListenerdo botão chamaviewModel.searchRepositories("google"). -
RepoListViewModel: A funçãosearchRepositoriesé chamada. Ela atualiza o_uiStateparaLoadinge inicia uma corrotina emviewModelScope. -
Dentro da Corrotina: O ViewModel chama
repository.getUserRepositories("google"). -
GitHubRepository: A funçãogetUserRepositorieschamaapiService.listRepositories("google"). -
Retrofit: Faz a chamada HTTP à API do GitHub em uma thread de I/O (graças aosuspend). -
Resposta da API: A API retorna uma lista de repositórios. O Retrofit a desserializa em
List<Repo>. -
GitHubRepository: Recebe a lista, a envolve emResult.success()e a retorna. -
RepoListViewModel: Recebe oResult, atualiza o_uiStateparaRepoUiState.Success(repos). -
SearchFragment: O coletorflowWithLifecycleque está observando ouiStateé acionado com o novo estadoSuccess. -
Navegação: O bloco
whennavega para a tela de lista de repositórios, passando os repos como argumentos. -
Renderização: A lista é exibida via
RecyclerViewcomListAdapter, que calcula as diferenças comDiffUtile 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:
- Persistência Local: Integre Room para implementar "Favoritos" com banco de dados local.
- Injeção de Dependência: Use Hilt para remover instanciação manual e desacoplar ainda mais.
- Testes: Escreva testes de unidade para
ViewModel(com Repository falso) e testes de UI com Espresso. - 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.