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

  1. 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.

  2. LazyColumn sem key: sempre forneça uma key única para itens de LazyColumn. Sem ela, o Compose pode ter problemas ao reordenar ou remover itens, causando comportamentos estranhos na interface.

  3. Não usar collectAsState(): esquecer de converter o StateFlow com collectAsState() faz com que a UI não seja atualizada quando o estado muda.

  4. 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.

  5. 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: