Datas no Jetpack Compose com Date Picker
Tags: kotlin, android, jetpackcompose
Published: 2023-06-19 11:00:00




Utilizar datas é uma tarefa bastante comum em diversos Apps, seja para um cadastro, agendamento, indicação de conclusão etc... Nas bibliotecas padrões do Jetpack Compose, não temos acesso a um componente para lidar com datas! 

Em outras palavras, ou precisamos implementar manualmente um composable ou podemos usar os pickers do SDK do Android... Felizmente, a partir da versão [1.2.0 dos componentes do Material Design 3 para Jetpack Compose](https://developer.android.com/jetpack/androidx/releases/compose-material3), temos acesso a novos pickers, como por exemplo, o [DateTimerPicker](https://m3.material.io/components/date-pickers/overview)!

E neste artigo eu vou te mostrar como usá-lo no seu App com o Jetpack Compose.

## Verificando as dependências

Antes de tudo, é muito importante que você verifique a versão da dependência `androidx.compose.material3:material3`. Geralmente, as versões não são especificadas, ou seja, dependendo do momento que implementar, pode ser que utilize uma versão anterior a 1.2.0!

Nesse caso, vá até o `build.gradle.kts` e adicione manualmente a versão:

```kotlin
dependencies {
    // dependências
    implementation("androidx.compose.material3:material3:1.2.0-alpha02")
}
```

> Caso existir um aviso indicando que tem versões mais recentes, talvez não precise especificar a versão, pois provavelmente a 1.2.0 ou uma mais recente entrou em release.

Com a dependência adicionada, podemos implementar o composable.

---

## TL;DR

Para você que quer apenas um código de amostra, segue a versão final do composable:

```kotlin
val focusManager = LocalFocusManager.current
var showDatePickerDialog by remember {
    mutableStateOf(false)
}
val datePickerState = rememberDatePickerState()
var selectedDate by remember {
    mutableStateOf("")
}
if (showDatePickerDialog) {
    DatePickerDialog(
        onDismissRequest = { showDatePickerDialog = false },
        confirmButton = {
            Button(
                onClick = {
                    datePickerState
                        .selectedDateMillis?.let { millis ->
                            selectedDate = millis.toBrazilianDateFormat()
                        }
                    showDatePickerDialog = false
                }) {
                Text(text = "Escolher data")
            }
        }) {
        DatePicker(state = datePickerState)
    }
}
TextField(
    value = selectedDate,
    onValueChange = { },
    Modifier
        .padding(8.dp)
        .fillMaxWidth()
        .onFocusEvent {
            if (it.isFocused) {
                showDatePickerDialog = true
                focusManager.clearFocus(force = true)
            }
        },
    label = {
        Text("Date")
    },
    readOnly = true
)
```

O formatador de data você pode implementar como preferir, mas também pode usar essa extensão que criei:

```kotlin
fun Long.toBrazilianDateFormat(
    pattern: String = "dd/MM/yyyy"
): String {
    val date = Date(this)
    val formatter = SimpleDateFormat(
        pattern, Locale("pt-br")
    ).apply {
        timeZone = TimeZone.getTimeZone("GMT")
    }
    return formatter.format(date)
}
```

Esse código deve apresentar o seguinte resultado:

![App em execução, apresenta caixa de diálogo com o date picker ao clicar no campo de texto 'Date'. Ao clicar no botão 'Escolher data', fecha a caixa de diálogo e mostra a data formatada no padrão dd/MM/yyyy no campo de texto. Ao abrir novamente a caixa de diálogo e clicar fora do escopo, apenas fecha a caixa de diálogo.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yoeudklfgnccbx4jcmhp.gif)

Para entender as motivações desse código, acompanhe o restante do conteúdo. 😎

--- 

## Utilizando o DatePicker na tela

A utilização do picker é relativamente simples:

```kotlin
val datePickerState = rememberDatePickerState()
DatePicker(state = datePickerState)
```

![App em execução apresentando o DatePicker na tela inteira](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ee0fbqsgyc8kjv6vezqo.png)

Veja que apenas chamando o composable `DatePicker`, temos uma tela dedicada para selecionar a data. Porém, apenas esse código não é o suficiente para obter a data escolhida!

Para isso, o `DatePicker` oferece a propriedade `selectedDateMillis` a partir do `DatePickerState`, mas, apenas ter acesso ao valor não vai entregar uma experiência de uso completa! 

## Adicionando o campo de texto para a data

Quando queremos apresentar uma data a partir de pickers, geralmente utilizamos um outro componente para abrir o picker e exibir a data escolhida, como por exemplo, em um formulário, teríamos um campo de texto pra isso:

```kotlin
var selectedDate by remember() {
    mutableStateOf("")
}
TextField(
    value = selectedDate,
    onValueChange = { },
    Modifier
        .padding(8.dp)
        .fillMaxWidth(),
    label = {
        Text("Date")
    },
    readOnly = true
)
```

![App em execução apresentando um campo de texto com a label 'Date'](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ot9j5gwq794ku0hlqr51.png)

Então, precisamos integrar o `TextField` com o `DatePicker`. Embora seja possível usar o `DatePicker` para essa solução, a implementação apenas com o picker tende ser um pouco mais complexa, pois existe a lógica para mostrá-lo ou escondê-lo... 



## Utilizando caixa de diálogo para o date picker

Para facilitar a implementação do `DatePicker`, vamos o `DatePickerDialog`. Basicamente, temos o comportamento padrão de [caixa de diálogo do Android](https://www.alura.com.br/artigos/criando-caixas-de-dialogo-no-android-dialogs). Um dos casos comuns é não querer selecionar uma data, com a caixa de diálogo basta apenas clicar fora do contexto:

```kotlin
var selectedDate by remember {
    mutableStateOf("")
}
val datePickerState = rememberDatePickerState()
DatePickerDialog(
    onDismissRequest = { /*TODO*/ },
    confirmButton = { /*TODO*/ }) {
    DatePicker(state = datePickerState)
}
TextField(
    value = selectedDate,
    onValueChange = { },
    Modifier
        .padding(8.dp)
        .fillMaxWidth(),
    label = {
        Text("Date")
    },
    readOnly = true
)
```

![App em execução exibindo a caixa de dialogo apresentado o date picker](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lckp7pfp9zghi81hywih.png)

Agora, precisamos integrar a lógica para exibir o date picker apenas quando clicar no `TextField`.

## Exibindo o DatePickerDialog a partir do TextField

Dado que usamos o `readOnly`, ele não reage mais a cliques! Logo, precisamos reagir a outros eventos, como a mudança de foco:

```kotlin
var showDatePickerDialog by remember {
    mutableStateOf(false)
}
var selectedDate by remember {
    mutableStateOf("")
}
val datePickerState = rememberDatePickerState()
if (showDatePickerDialog) {
    DatePickerDialog(
        onDismissRequest = { /*TODO*/ },
        confirmButton = { /*TODO*/ }) {
        DatePicker(state = datePickerState)
    }
}
TextField(
    value = selectedDate,
    onValueChange = { },
    Modifier
        .padding(8.dp)
        .fillMaxWidth()
        .onFocusChanged {
            if (it.isFocused) {
                showDatePickerDialog = true
            }
        },
    label = {
        Text("Date")
    },
    readOnly = true
)
```

![App em execução, ao clicar no campo de texto para data, abre a caixa de diálogo e exibe o Data Picker. Ao clicar fora do diálogo ele não é fechado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y5qp8v4bc5zx5wrvvaau.gif)

Veja que a caixa de diálogo exibe o date picker, porém, ele não é fechado ao clicar fora do seu contexto, um comportamento que não esperamos neste tipo de componente. 

## Implementando a lógica para fechar a caixa de diálogo

Para implementar essa solução, precisamos configurar os parâmetros `onDismissRequest` ou `confirmButton` e adicionar esse comportamento:

```kotlin
if (showDatePickerDialog) {
    DatePickerDialog(
        onDismissRequest = { showDatePickerDialog = false },
        confirmButton = {
            Button(onClick = { showDatePickerDialog = false }) {
                Text(text = "Escolher data")
            }
        }) {
        DatePicker(state = datePickerState)
    }
}
```

![App em execução, abrindo caixa de diálogo com Date Picker e botão de escolher data ao clicar no campo de texto 'Date'. Ao clicar fora do escopo do dialog, ele é fechado novamente. Ao tentar abrir novamente a caixa de diálogo, ela não é aberta](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fu76j9ewndqthid9lrkd.gif)

Observe que o comportamento para fechar a caixa de diálogo funciona, mas não é possível abrí-la novamente! Isso acontece, pois no Jetpack Compose, ao focar em um campo de texto, o evento de foco só muda se interagirmos com outro elemento que ganhe o foco, ou então, quando modificamos via código.

## Manipulando o foco dos elementos

Dado que não temos um outro elemento para ganhar foco na tela, e também, depender de outro elemento para exibir o date picker, é uma péssima experiência de uso, nós iremos modificar o foco manualmente!

Para isso, utilizamos o gerenciador de foco e limpamos o foco no evento desejado, nesse caso, ao ganhar o foco no `TextField`:

```kotlin
val focusManager = LocalFocusManager.current
...
TextField(
    value = selectedDate,
    onValueChange = { },
    Modifier
        .padding(8.dp)
        .fillMaxWidth()
        .onFocusEvent {
            if (it.isFocused) {
                showDatePickerDialog = true
                focusManager.clearFocus(force = true)
            }
        },
    label = {
        Text("Date")
    },
    readOnly = true
)
```

![App em execução, ao clicar no campo de texto 'Date', a caixa diálogo com o Date Picker é apresentada. Ao clicar fora do escopo da caixa de diálogo, a caixa de diálogo é fechada e apresenta o campo de texto 'Date' sem foco, ao clicar novamente no campo de texto, abre a caixa de diálogo. Ao clicar no botão 'Escolher data' a caixa de diálogo é fechada também](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mcef0k36r27gt2y104ij.gif)

Veja que agora a caixa de diálogo é aberta e fechada nas situações esperadas! O que falta é pegar o valor da data escolhida e apresentá-la no campo de texto.

## Exibindo a data escolhida no TextField

Para isso, precisamos reagir ao evento correto que deve preencher o campo de texto. No contexto atual, temos o botão 'Escolher data', ele será o responsável em manipular o estado `selectedDate` que preenche o `TextField`.

### Formatando data em Long (milisegundos) para String

Considerando que a exibição de datas em Apps geralmente não é apresentada via milisegundos, vamos adicionar um formatador de `Long` para `String` que faça isso pra gente, podemos até mesmo considerar uma extension:

```kotlin
fun Long.toBrazilianDateFormat(
    pattern: String = "dd/MM/yyyy"
): String {
    val date = Date(this)
    val formatter = SimpleDateFormat(
        pattern, Locale("pt-br")
    ).apply {
        timeZone = TimeZone.getTimeZone("GMT")
    }
    return formatter.format(date)
}
```

E com a função de conversão pronta, precisamos apenas ajustar o evento de clique do botão:

```kotlin
DatePickerDialog(
    onDismissRequest = { showDatePickerDialog = false },
    confirmButton = {
        Button(
            onClick = {
                datePickerState
                    .selectedDateMillis?.let { millis ->
                        selectedDate = millis.toBrazilianDateFormat()
                    }
                    showDatePickerDialog = false
            }) {
            Text(text = "Escolher data")
        }
    }) {
    DatePicker(state = datePickerState)
}
...
```

![App em execução, apresenta caixa de diálogo com o date picker ao clicar no campo de texto 'Date'. Ao clicar no botão 'Escolher data', fecha a caixa de diálogo e mostra a data formatada no padrão dd/MM/yyyy no campo de texto. Ao abrir novamente a caixa de diálogo e clicar fora do escopo, apenas fecha a caixa de diálogo.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yoeudklfgnccbx4jcmhp.gif)

Veja que agora o nosso date picker funciona corretamente!

O que achou dessa implementação de date picker no Jetpack Compose? Gostou do conteúdo? Aproveita para deixar um like e compartilhar com a galera 😉