Seleccionar Fecha Con DatePicker En Android

El DatePicker en Android es un view group compuesto por varios elementos que permiten al usuario seleccionar una fecha a partir de la configuración individual de año, mes y día.

Aspecto de DatePicker en Android

Google te recomienda mostrarlo como un dialogo en vez de incrustarlo directamente en tus layouts, por lo que el framework de Android te provee a la clase DatePickerDialog para aislar la aparición y comportamiento del widget.

Y a su vez, también recomiendan envolver al DatePickerDialog con un DialogFragment, para aislar su manejo del ciclo de vida y posibilitar la construcción de interfaces flexibles.

En este tutorial aprenderás a cómo crear, mostrar y procesar eventos de un DatePicker a través del siguiente ejemplo:

Puedes descargar el proyecto Android Studio desde el siguiente enlace:

Crear DatePicker En Android

Antes que nada, vamos a habilitar varias funcionalidades de java.time del JDK 8 para acceder a las clases LocalDate y LocalDateTime. Esto requiere que abras el archivo build.gradle del módulo y habilites las siguientes características:

android {

    defaultConfig {
        multiDexEnabled true
    }

    compileOptions {
        coreLibraryDesugaringEnabled true
    }
}

dependencies {


    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:[ultima-version]'
}

Ahora bien, el primer paso es crear un fragmento que herede de DialogFragment y sobrescriba al método onCreateDialog(). Como viste en el tutorial de diálogos, este método debe retornar la creación de una instancia AlertDialog. Y como ya sabes, DatePickerDialog es subclase de ella.

class DatePickerFragment() : DialogFragment() {

    private var day: Int
    private var month: Int
    private var year: Int

    init {
        LocalDate.now().let { now ->
            year = now.year
            month = now.monthValue
            day = now.dayOfMonth
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let { args ->
            year = args.getInt(YEAR_ARG)
            month = args.getInt(MONTH_ARG)
            day = args.getInt(DAY_ARG)
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return DatePickerDialog(requireContext(), null, year, month, day)
    }

    override fun onCancel(dialog: DialogInterface) {
        super.onCancel(dialog)
        // [Acciones si deseas sobrescribir al presionar "CANCELAR"]
    }   
}

Analicemos el código para comprender el funcionamiento:

  1. Añadimos tres propiedades enteras para representar el año, el mes y el día
  2. Usamos el bloque de init para inicializar las variables con valores por defecto. En este caso usaremos el paquete java.time para acceder a la clase LocalDate y obtener la fecha actual con la función now().
  3. En onCreateDialog() usamos el constructor publico de DatePickerDialog() y pasamos el contexto, null (luego veremos este argumento) y los valores de la fecha.
  4. E incluimos la sobrescritura de onCancel() por si deseas realizar alguna acción al cancelar el diálogo

Mostrar El DatePicker

El diálogo selector de fechas construido previamente lucirá así cuando lo despliegues:

Crear DatePicker en Android

Recuerda que puedes usar el método show() para mostrar al DialogFragment cuando lo desees. Lo importante es definir el punto donde se dispara esta invocación.

Nuestra App de ejemplo usa un EditText como origen del evento de selección dentro de un formulario hipotético. La configuración del layout es la siguiente:

<?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="match_parent"
    android:padding="16dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/date_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fecha"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
        android:textColor="@color/purple_700"
        app:layout_constraintBottom_toTopOf="@+id/date_field"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <EditText
        android:id="@+id/date_field"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:importantForAutofill="no"
        android:focusable="false"
        android:focusableInTouchMode="false"
        app:layout_constraintBottom_toTopOf="@+id/guideline"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
EditText con DatePicker

Si te fijas, el EditText lleva el valor del atributo android:focusable en false, para evitar que el usuario pase el foco al campo de texto y no permitir la entrada de texto. Esto permitirá disparar el evento de click al primer toque.

El anterior diseño te permitirá obtener la referencia del campo de texto desde tu código Kotlin y añadirle un observador OnClickListener. En el momento que ocurra el click, mostramos al diálogo selector de fecha:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    dateField = findViewById(R.id.date_field)
    dateField.setOnClickListener {
        showDatePicker()
    }
}

private fun showDatePicker() {
    DatePickerFragment().show(supportFragmentManager, "date-picker")
}

Como ves, en la lambda que representa la escucha del click sobre el campo de texto, se ejecuta el método showDatePicker(), el cual muestra una instancia de DatePickerFragment.

Observar Eventos De Selección De Fecha

Para darle significado a la selección de fechas necesitamos tomar el valor y darle utilidad. Por ejemplo, mostrarle al usuario en el EditText que el estado ha cambiado al actualizar el texto.

OnDateSetListener en DatePicker

Es aquí donde usas la interfaz DatePickerDialog.OnDateSetListener para detectar cuando el usuario cambia la fecha.

Esta cuenta con el controlador onDateSet(), donde escribes las sentencias que deseas ejecutar en los cambios de fecha. Debido a que necesitamos delegar este comportamiento hacia nuestra actividad, añadimos una propiedad llamada observer que le redireccione la ejecución.

class DatePickerFragment : DialogFragment() {

    private var observer: DatePickerDialog.OnDateSetListener? = null

    //...

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return DatePickerDialog(requireContext(), observer, year, month, day)
    }



    // ...

    companion object {

        fun newInstance(            
            listener: DatePickerDialog.OnDateSetListener
        ): DatePickerFragment {
            val datePicker = DatePickerFragment()            
            datePicker.listener = listener
            return datePicker
        }
    }
}

Adicionalmente añadiremos un objeto compañero para encapsular la creación de la instancia de DatePickerFragment, con el fin de asignar el observador y otros parámetros.

Luego, desde la actividad, creamos una instancia de DatePickerFragment y le asignamos el observador de cambios de fecha. Este puede ser representado por una lambda con los mismos parámetros de onDateSet() y el cuerpo será la asignación del texto al EditText:

class MainActivity : AppCompatActivity() {

    private val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")

    //...

    private fun showDatePicker() {       
        DatePickerFragment.newInstance{ _, year, month, day ->
            dateField.setText(formatDate(year, month, day))
        }.show(supportFragmentManager, "date-picker")
    }

    private fun formatDate(year: Int, month: Int, day: Int): String {
        val sanitizeMonth = month + 1
        return LocalDate.of(year, sanitizeMonth, day).format(formatter)
    }

}

Puntos a tener en cuenta:

  1. showDatePicker(): Este método fue modificado para invocar a newInstance() con el observador de cambios de fecha. En el cuerpo de la lambda que representa a la instancia OnDateSetListener, asignamos un texto que crea un string a partir de los parámetros year, month y day.
  2. formatDate(): Recibe el año, mes y día para crear un String formateado para mostrar en el campo de texto. Para ello se usa format() con el formateador existente. Adicionalmente sumamos la unidad al mes, ya que DatePickerDialog interpreta al mes con el rango [0, 11].

Establecer Fecha Desde Argumentos

Hasta el momento nuestro date picker se muestra y actualiza el campo de texto, pero siempre que se abre, comienza seleccionado el mismo día. La idea es iniciar el selector con el valor previo seleccionado.

Asignar valores iniciales a DatePickerDialog

Una de las formas de asignar los valores iniciales para la fecha es pasar argumentos al DialogFragment. Por lo que luego de usar su constructor, asignamos la propiedad arguments al Bundle con el día, mes y año:

companion object {
    const val YEAR_ARG = "args.year"
    const val MONTH_ARG = "args.month"
    const val DAY_ARG = "args.day"

    fun newInstance(
        year: Int, month: Int, day: Int,
        observer: DatePickerDialog.OnDateSetListener
    ): DatePickerFragment {
        val datePicker = DatePickerFragment()
        datePicker.arguments = Bundle().apply {
            val adjustMonth = month - 1

            putInt(YEAR_ARG, year)
            putInt(MONTH_ARG, adjustMonth)
            putInt(DAY_ARG, day)
        }
        datePicker.observer = observer
        return datePicker
    }
}

Como LocalDate trabaja con el rango [1, 12] para el valor entero del mes, entonces aprovechamos para restar la unidad al argumento MONTH_ARG.

Luego desde onCreate() en el fragmento, los extraemos y almacenamos en propiedades que serán usadas en el constructor del DatePickerDialog():

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    arguments?.let { args ->
        year = args.getInt(YEAR_ARG)
        month = args.getInt(MONTH_ARG)
        day = args.getInt(DAY_ARG)
    }
}

En seguida, vamos a la actividad y obtenemos la fecha actual desde el EditText:

private fun showDatePicker() {
    val date = getCurrentDate()
    DatePickerFragment.newInstance(
        date.year,
        date.monthValue,
        date.dayOfMonth
    ) { _, year, month, day ->
        dateField.setText(formatDate(year, month, day))
    }.show(supportFragmentManager, "date-picker")
}

private fun getCurrentDate(): LocalDate {
    val date = dateField.text.toString()
    return LocalDate.parse(date, formatter)
}

getCurrentDate() obtiene el texto actual del campo de texto y luego lo parsea a LocalDate. Tanto parse() como format() requieren el mismo formateador para mantener la consistencia de fechas.

Cambiar Color De DatePicker

Supón que vamos a personalizar el color usado sobre el DatePicker de nuestra App de ejemplo con el siguiente azul:

Cambiar color de DatePickerDialog

¿Cómo lograrlo?

Depende del objetivo. Si quieres que todos los Date Pickers en tu App sean del mismo color, entonces realiza una superposición del tema con el atributo de estilo android:datePickerDialogTheme:

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.DatePickerEnAndroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        
        <item name="android:datePickerDialogTheme">@style/DatePickerStyle</item>
    </style>

    <style name="DatePickerStyle" parent="ThemeOverlay.MaterialComponents.Dialog">
        <item name="colorPrimary">@color/blue_500</item>
        <item name="colorSecondary">@color/blue_500</item>
    </style>

</resources>

En el caso que solo desees aplicarlo a un DatePicker en específico, entonces usa la versión del constructor público, que acepta el tema personalizado, sobre el método onCreateDialog():

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    return DatePickerDialog(
        requireActivity(),
        R.style.DatePickerStyle,
        observer,
        year,
        month,
        day
    )
}

Añadir Límites A La Fecha

Considera un cambio de reglas de negocio que te exigen acotar la selección de fechas a los sietes últimos días a partir de hoy. El DatePicker tiene que deshabilitar los demás valores para evitar que el usuario rompa esta acotación:

Limitar DatePicker con minDate y maxDate

Para establecer la fecha mínima y la fecha máxima, usa los atributos minDate y maxDate de la clase DatePicker respectivamente:

import java.time.LocalDateTime
import java.time.ZoneId

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val dialog = DatePickerDialog(
        requireActivity(),
        R.style.DatePickerStyle,
        observer,
        year,
        month,
        day
    )

    val today = LocalDateTime.now()
    val sevenDaysAgo = today.minusDays(7)
    dialog.datePicker.minDate= sevenDaysAgo.toMs()
    dialog.datePicker.maxDate = today.toMs()

    return dialog
}

private fun LocalDateTime.toMs(): Long {
    return atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
}

La clase DatePickerDialog tiene como propiedad a la instancia del DatePicker. Por lo que es posible acceder directamente a los atributos de límite de fechas.

Ambos son de tipo Long, ya que reciben la fecha en milisegundos. Para ello usamos la clase LocalDataTime y una función de extensión que convierta a milisegundos estas instancias.

Obtener la fecha de siete días previos se realiza con el método minusDays() sobre la fecha actual today. De esta forma asignamos los límites al selector.

Mas Contenidos