Datas no Android com o MaterialDatePicker

Datas no Android com o MaterialDatePicker
Alex Felipe
Alex Felipe

Compartilhe

Projeto de exemplo

Para este artigo, vou usar o projeto Eventos, um App Android para cadastrar eventos com um título e data do evento. Você pode acessar o código fonte do projeto neste repositório do GitHub ou baixá-lo caso queira simular os exemplos.

Banner de divulgação da Imersão IA da Alura em colaboração com o Google. Mergulhe em Inteligência artificial com a Alura e o Google. Serão cinco aulas gratuitas para você aprender a usar IA na prática e desenvolver habilidades essenciais para o mercado de trabalho. Inscreva-se gratuitamente agora!

Estrutura do App

Na implementação do App, temos um RecyclerView que apresenta cada evento cadastrado. Para cadastrar o evento, clicamos no FAB (FloatingActionButton), preenchemos o formulário com título e data e clicamos em Salvar.

O grande detalhe da implementação, é que o usuário é responsável em escrever a data com o formato esperado, o que pode ocasionar em dúvidas ou frustrações...

No caso desta implementação, um formato inesperado quebra o App! Uma das piores experiências para o usuário... Sendo assim, precisamos evitar ao máximo esse comportamento e podemos considerar as seguintes técnicas na regra de negócio:

  • Realizar tratamentos com try-catch;
  • Implementar validações para orientar o formato correto para o usuário.

Ambas as técnicas são comuns na maioria das linguagens, porém, dado que estamos no ambiente Android, podemos também utilizar um componente que auxilie na parte visual, o MaterialDatePicker:

Adicionando os componentes do Material Design

Para utilizar o MaterialDatePicker, o projeto precisa da dependência dos componentes do Material Design:

dependencies {
    // outras dependências
    implementation 'com.google.android.material:material:1.3.0'
}

O projeto fornecido já tem a dependência configurada!

Após sincronizar, temos acesso ao MaterialDatePicker.

Criando o MaterialDatePicker

Para utilizar o MaterialDatePicker, temos uma abordagem similar ao AlertDialog, ou seja, um builder para nos auxiliar:

class ListaEventosActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.recyclerview.adapter = adapter
        configuraFAB()

        val selecionadorDeData = MaterialDatePicker
            .Builder.datePicker().build()
    }

    // restante do código

}
selecionadorDeData.show(supportFragmentManager, "MATERIAL_DATE_PICKER")

Com apenas esse código, o usuário é capaz de selecionar a data desejada e confirmar. Porém, o código não é o suficiente para obtermos a data selecionada.

Pegando a data do MaterialDatePicker

Para pegar a data selecionada do MaterialDatePicker, precisamos adicionar um listener no dialog com a função addOnPositiveButtonClickListener() que recebe uma expressão lambda com o valor da data em milisegundos:

selecionadorDeData
    .addOnPositiveButtonClickListener { dataEmMilisegundos ->
        Log.i("MaterialDatePicker", "data em milisegundos: $dataEmMilisegundos")
    }

Então temos o seguinte resultado selecionando a data 23/02/2021:

2021-02-23 10:41:45.392 16915-16915/br.com.alura.eventos I/MaterialDatePicker: data em milisegundos: 1614038400000

A partir do valor em milisegundos, podemos converter para uma data com uma API disponível, como por exemplo, as classes do pacote java.time que oferece a Instant capaz de fazer isso:

val data = Instant.ofEpochMilli(dataEmMilisegundos)
                    .atZone(ZoneId.of("America/Sao_Paulo"))
                    .withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.UTC))
                    .toLocalDate()
Log.i("MaterialDatePicker", "data com LocalDate: $data")

Basicamente, criamos uma instância de Instant por meio dos milisegundos, então configuramos o time zone da America São Paulo com deslocamento em UTC e convertemos para a API LocalDate que facilita o trabalho de datas...

Depois de todo esse código assustador para fazer uma conversão, temos a data esperada:

2021-02-23 11:44:20.465 17940-17940/br.com.alura.eventos I/MaterialDatePicker: data com LocalDate: 2021-02-23

Com a data pronta, precisamos integrar a nossa solução com o fluxo do App.

Adicionado o MaterialDatePicker no formulário

No formulário, precisamos chamar o MaterialDatePicker ao clicar no campo de data, portanto, podemos adicionar todo o nosso código dentro do listener de clique do campo de data:

class FormEventoDialog(private val context: Context) {

    fun show(
        supportFragmentManager: FragmentManager,
        quandoEventoCriado: (eventoCriado: Evento) -> Unit
    ) {
        val binding = FormEventoBinding
            .inflate(LayoutInflater.from(context))

        binding.data.setOnClickListener {
            val selecionadorDeData = MaterialDatePicker
                .Builder.datePicker().build()
            selecionadorDeData.show(supportFragmentManager, "MATERIAL_DATE_PICKER")
            selecionadorDeData
                .addOnPositiveButtonClickListener { dataEmMilisegundos ->
                    val data = Instant.ofEpochMilli(dataEmMilisegundos)
                        .atZone(ZoneId.of("America/Sao_Paulo"))
                        .withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.UTC))
                        .toLocalDate()
                    Log.i("MaterialDatePicker", "data com LocalDate: $data")
                }
        }

        // restante do código

    }

}

Note que ao mover o código, precisamos também adicionar o FragmentManager como parâmetro do método show() da FormEventoDialog. Na Activity, no listener do FAB, precisamos também enviar o FragmentManager como argumento:

class ListaEventosActivity : AppCompatActivity() {

    // restante do código 

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.recyclerview.adapter = adapter
        configuraFAB()
    }

    private fun configuraFAB() {
        binding.floatingActionButton.setOnClickListener {
            FormEventoDialog(this)
                .show(supportFragmentManager) { eventoCriado ->
                    dao.salva(eventoCriado)
                    adapter.atualiza(dao.eventos)
                }
        }
    }

}

Esse código é o suficiente para testar o App e abrir o MaterialDatePicker clicando no campo de data.

Por mais que funcione, a experiência do usuário não é bacana, pois precisamos de um segundo clique após focar no campo de data para apresentar o MaterialDatePicker!

Melhorando a experiência de clique no campo de data

Para evitar esse comportamento, modificamos o TextInputEditText para que não seja focável:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!-- restante do layout -->

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
                        android:focusable="false"
            android:hint="Data" />

</androidx.constraintlayout.widget.ConstraintLayout>

Em seguida, precisamos pegar a data selecionada e preencher o campo assim que o usuário confirma:

selecionadorDeData
    .addOnPositiveButtonClickListener { dataEmMilisegundos ->
        val data = Instant.ofEpochMilli(dataEmMilisegundos)
            .atZone(ZoneId.of("America/Sao_Paulo"))
            .withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.UTC))
            .toLocalDate()
        binding.data.setText(data.toString())
    }

Observe que funciona, porém, ao salvar a nota nesse padrão, temos o mesmo problema de quebrar o App, pois a configuração do padrão esperado é dd/MM/yyyy, ou seja, 23/02/2021 considerando esse exemplo.

Formatando data

Para formatar a data com um padrão diferente, podemos utilizar as APIs do próprio java.time também, a DateTimeFormatter:

val formatador = DateTimeFormatter
    .ofPattern("dd/MM/yyyy", Locale("pt-br"))
val dataFormatada = formatador.format(data)
binding.data.setText(dataFormatada)

E temos este resultado ao fazer o mesmo teste:

E agora podemos salvar sem problema algum:

Utilizando funções de extensão de LocalDate

Depois de finalizar a implementação, podemos simplificar a nossa solução utilizando algumas funções de extensão já adicionadas no projeto:

package br.com.alura.eventos.extensions

import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*

private val formatador = DateTimeFormatter
    .ofPattern("dd/MM/yyyy", Locale("UTC"))

fun LocalDate.paraFormatoBrasileiro(): String = this.format(formatador)

fun String.paraLocalDate(): LocalDate = LocalDate.parse(this, formatador)

Ao invés de criar todo aquele código para formatar a data no padrão brasileiro, basta apenas chamar a função paraFormatoBrasileiro() a partir da data:

val data = Instant.ofEpochMilli(dataEmMilisegundos)
    .atZone(ZoneId.of("America/Sao_Paulo"))
    .withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.UTC))
    .toLocalDate()
binding.data.setText(data.paraFormatoBrasileiro())

Inclusive, podemos transformar em uma função de extensão, o código que converte de milisegundos para LocalData:

fun Long.paraLocalDate(): LocalDate = Instant.ofEpochMilli(this)
    .atZone(ZoneId.of("America/Sao_Paulo"))
    .withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.UTC))
    .toLocalDate()

Essa mesma implementação poderia receber as configurações de time zone via parâmetro também.

Então temos um resultado bem mais simplificado:

selecionadorDeData
    .addOnPositiveButtonClickListener { dataEmMilisegundos ->
        val data = dataEmMilisegundos.paraLocalDate()
        binding.data.setText(data.paraFormatoBrasileiro())
    }

Para saber mais: Outras implementações com o MaterialDatePicker

Além desta implementação mais simples, o MaterialDatePicker também oferece outros recursos, como a seleção de período:

Você pode obter mais informações sobre as possibilidades e implementações na página do material.io.

Alex Felipe
Alex Felipe

Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Atualmente cria conteúdo no canal https://www.youtube.com/@AlexFelipeDev.

Veja outros artigos sobre Mobile