Criar seu primeiro aplicativo Android com Kotlin é uma experiência empolgante e recompensadora. Neste tutorial, vamos construir juntos um app de lista de tarefas (To-Do List) completo, utilizando Jetpack Compose para a interface e conceitos fundamentais de Kotlin. Ao final, você terá uma aplicação funcional e entenderá os conceitos básicos do desenvolvimento Android moderno.
Planejando o App de Lista de Tarefas
Antes de escrever qualquer código, é importante planejar o que nosso app vai fazer. Nosso To-Do List terá as seguintes funcionalidades: adicionar novas tarefas, marcar tarefas como concluídas, remover tarefas da lista e exibir a lista atualizada em tempo real.
A arquitetura que vamos seguir é simples, mas segue as boas práticas recomendadas pelo Google. Teremos uma Activity principal que hospeda nossa UI em Jetpack Compose, uma class de dados para representar cada tarefa e um ViewModel para gerenciar o estado da aplicação.
Certifique-se de que você já tem o Android Studio configurado conforme nosso tutorial de Configurando Kotlin no Android Studio. Com o ambiente pronto, crie um novo projeto com o template “Empty Activity” e o nome “ListaDeTarefas”.
Criando o Modelo de Dados
O primeiro passo é definir a estrutura de dados que representa uma tarefa. Em Kotlin, usamos Data Classes para isso, pois elas geram automaticamente métodos como equals(), hashCode(), toString() e copy().
// modelo/Tarefa.kt
package com.exemplo.listadetarefas.modelo
import java.util.UUID
data class Tarefa(
val id: String = UUID.randomUUID().toString(),
val titulo: String,
val descricao: String = "",
val concluida: Boolean = false
)
Note que usamos val para declarar propriedades imutáveis. O id é gerado automaticamente usando UUID, garantindo que cada tarefa tenha um identificador único. A descricao e concluida possuem valores padrão, tornando a criação de tarefas mais simples.
A imutabilidade é uma prática recomendada em Kotlin. Em vez de modificar um objeto existente, criamos uma nova cópia com as alterações desejadas usando o método copy() da Data Class.
Implementando o ViewModel
O ViewModel é responsável por gerenciar o estado da nossa lista de tarefas. Ele sobrevive a mudanças de configuração (como rotação de tela) e fornece os dados para a UI de forma reativa usando StateFlow.
// viewmodel/TarefaViewModel.kt
package com.exemplo.listadetarefas.viewmodel
import androidx.lifecycle.ViewModel
import com.exemplo.listadetarefas.modelo.Tarefa
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class TarefaViewModel : ViewModel() {
private val _tarefas = MutableStateFlow<List<Tarefa>>(emptyList())
val tarefas: StateFlow<List<Tarefa>> = _tarefas.asStateFlow()
fun adicionarTarefa(titulo: String, descricao: String = "") {
if (titulo.isBlank()) return
val novaTarefa = Tarefa(titulo = titulo.trim(), descricao = descricao.trim())
_tarefas.update { listaAtual -> listaAtual + novaTarefa }
}
fun alternarConclusao(tarefaId: String) {
_tarefas.update { listaAtual ->
listaAtual.map { tarefa ->
if (tarefa.id == tarefaId) {
tarefa.copy(concluida = !tarefa.concluida)
} else {
tarefa
}
}
}
}
fun removerTarefa(tarefaId: String) {
_tarefas.update { listaAtual ->
listaAtual.filter { it.id != tarefaId }
}
}
val totalTarefas: Int
get() = _tarefas.value.size
val tarefasConcluidas: Int
get() = _tarefas.value.count { it.concluida }
}
Observe como usamos Lambda expressions e Higher-Order Functions como map, filter e count para manipular a lista de tarefas. Essas são funcionalidades poderosas de Kotlin que tornam o código mais conciso e legível.
O MutableStateFlow é usado internamente para permitir atualizações, enquanto o StateFlow exposto publicamente é somente leitura, seguindo o princípio de encapsulamento.
Construindo a Interface com Jetpack Compose
Agora vamos criar a interface do nosso app usando Jetpack Compose. A UI será composta por um campo de texto para adicionar tarefas e uma lista rolável que exibe as tarefas existentes.
// ui/TelaPrincipal.kt
package com.exemplo.listadetarefas.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.exemplo.listadetarefas.modelo.Tarefa
import com.exemplo.listadetarefas.viewmodel.TarefaViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TelaPrincipal(viewModel: TarefaViewModel) {
val tarefas by viewModel.tarefas.collectAsState()
var novoTitulo by remember { mutableStateOf("") }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Lista de Tarefas") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = novoTitulo,
onValueChange = { novoTitulo = it },
label = { Text("Nova tarefa") },
modifier = Modifier.weight(1f),
singleLine = true
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
viewModel.adicionarTarefa(novoTitulo)
novoTitulo = ""
}
) {
Icon(Icons.Default.Add, contentDescription = "Adicionar")
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Total: ${tarefas.size} | Concluídas: ${tarefas.count { it.concluida }}",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
LazyColumn {
items(tarefas, key = { it.id }) { tarefa ->
CartaoTarefa(
tarefa = tarefa,
onAlternar = { viewModel.alternarConclusao(tarefa.id) },
onRemover = { viewModel.removerTarefa(tarefa.id) }
)
}
}
}
}
}
@Composable
fun CartaoTarefa(
tarefa: Tarefa,
onAlternar: () -> Unit,
onRemover: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = tarefa.concluida,
onCheckedChange = { onAlternar() }
)
Text(
text = tarefa.titulo,
modifier = Modifier.weight(1f),
textDecoration = if (tarefa.concluida) {
TextDecoration.LineThrough
} else {
TextDecoration.None
}
)
IconButton(onClick = onRemover) {
Icon(Icons.Default.Delete, contentDescription = "Remover")
}
}
}
}
O Jetpack Compose usa o paradigma declarativo: descrevemos como a UI deve parecer para cada estado, e o framework cuida de atualizar a tela automaticamente quando o estado muda. O collectAsState() converte nosso StateFlow em um estado observável pelo Compose.
Conectando Tudo na MainActivity
Por fim, precisamos conectar o ViewModel à nossa tela principal na MainActivity. O Android fornece a função viewModel() para criar e gerenciar instâncias de ViewModel automaticamente.
// MainActivity.kt
package com.exemplo.listadetarefas
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import com.exemplo.listadetarefas.ui.TelaPrincipal
import com.exemplo.listadetarefas.ui.theme.ListaDeTarefasTheme
import com.exemplo.listadetarefas.viewmodel.TarefaViewModel
class MainActivity : ComponentActivity() {
private val viewModel: TarefaViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ListaDeTarefasTheme {
TelaPrincipal(viewModel = viewModel)
}
}
}
}
O by viewModels() utiliza o padrão de Delegação do Kotlin para criar o ViewModel de forma lazy, ou seja, ele só é instanciado quando acessado pela primeira vez. Além disso, o ViewModel sobrevive a mudanças de configuração, como rotação de tela.
Dicas e Erros Comuns
Estado perdido ao girar a tela: se você armazenar estado diretamente no Composable com
remember, ele será perdido ao girar a tela. Use ViewModel para dados que precisam sobreviver a mudanças de configuração.LazyColumn sem
key: sempre forneça umakeyúnica para itens deLazyColumn. Sem ela, o Compose pode ter problemas ao reordenar ou remover itens, causando comportamentos estranhos na interface.Não usar
collectAsState(): esquecer de converter o StateFlow comcollectAsState()faz com que a UI não seja atualizada quando o estado muda.Modificar estado fora do ViewModel: toda modificação de estado deve passar pelo ViewModel. Manipular dados diretamente no Composable viola o padrão de arquitetura e dificulta os testes.
Esquecer o
Modifier.fillMaxSize(): sem definir o tamanho dos componentes, a UI pode não ocupar o espaço esperado na tela.
Conclusão e Próximos Passos
Parabéns! Você criou seu primeiro aplicativo Android com Kotlin. Nosso app de lista de tarefas, embora simples, demonstra conceitos fundamentais como Data Classes, ViewModel, StateFlow e Jetpack Compose. Esses são os pilares do desenvolvimento Android moderno.
Para evoluir este projeto, você poderia adicionar persistência de dados com Room, navegação entre telas com Navigation Compose, notificações e categorias para as tarefas.
Para continuar aprendendo, recomendamos os seguintes tutoriais:
- Jetpack Compose: Introdução para aprofundar seus conhecimentos em UI declarativa
- Layouts com Jetpack Compose para criar interfaces mais complexas
- Coroutines Avançadas: Flow e Channel para entender melhor o StateFlow usado neste projeto
- Testes Unitários com Kotlin para aprender a testar seu ViewModel