Cómo pedir una fecha en Android usando DatePicker

En este tutorial Android aprenderás a solicitar a los usuarios que ingresen una fecha a través de un DatePicker.

DatePicker

Un DatePicker se ve genial, y funciona de maravilla.

El detalle es que no viene enlazado a ningún campo.

Y resulta muchas veces inviable para nosotros situarlo directamente sobre nuestros formularios, por el espacio que demanda.

Mostrando un DatePicker a través de un Dialog

Para solucionar este inconveniente es que vamos a mostrarlo a través de un cuadro de diálogo.

Entonces:

  • Cuando el usuario haga clic sobre un EditText de solo lectura (donde necesitamos la fecha),
  • vamos a mostrar un DialogFragment con un DatePicker en su interior, y
  • cuando el usuario seleccione una fecha, vamos a capturar la fecha ingresada.
Cómo luce un DatePicker Dialog

Existen varias formas de capturar la fecha ingresada por el usuario.

En este caso vamos a usar un método fácil de recordar, y que además nos permitirá capturar tantas fechas como necesitemos.

También recuerda que: por tratarse de una biblioteca estándar de Android, el nombre de los meses y días coincidirá con el idioma configurado en el dispositivo.

Primer paso: EditText de solo lectura

Este paso puede parecer muy obvio si ya tienes experiencia programando en Android.

Pero, es importante mencionar que:

Debemos definir nuestro EditText como un campo de solo lectura en nuestro layout.

<EditText
    android:id="@+id/etPlannedDate"
    android:hint="@string/report_planned_date"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="date"
    android:focusable="false"
    android:clickable="true"
    android:maxLines="1" />

Lo importante aquí es:

  • Definir un id para el EditText.

    En el ejemplo es etPlannedDate, porque el usuario ingresará una fecha planificada para un evento.

  • Asignar los atributos focusable en false y clickable en true.

    Así no se podrá escribir en él, pero sí funcionará el evento de clic.

Y por favor, no uses disabled en true, porque en ese caso no podrás usar el evento de clic.

Yo perdí casi 1 hora intentándolo de esta forma sin entender por qué no funcionaba :b

Segundo paso: Asociar un evento de clic al EditText

Ahora vamos a escuchar el evento de click sobre nuestro EditText.

Así, cuando se haga clic sobre él, vamos a mostrar nuestro dialog.

Podemos asignar la clase en la que estamos situados como listener del evento click de esta manera:

EditText etPlannedDate = (EditText) view.findViewById(R.id.etPlannedDate);
etPlannedDate.setOnClickListener(this);

Y por último definir la acción así:

@Override
public void onClick(View view) {
    switch (view.getId()) {
        case R.id.etPlannedDate:
            showDatePickerDialog();
            break;
    }
}

Eso hará que ante un clic, se ejecute el método showDatePickerDialog().

En cambio:

Si estás usando Kotlin, y tienes un EditText para capturar la fecha de nacimiento de tus usuarios, podrías usar lo siguiente:

etBirthDate.setOnClickListener {
    showDatePickerDialog()
}

Tercer paso: Mostrar el DatePicker en un dialog

Vamos a definir el método que antes usamos:

private void showDatePickerDialog() {
    DatePickerFragment newFragment = new DatePickerFragment();
    newFragment.show(getActivity().getSupportFragmentManager(), "datePicker")
}

En el código anterior estoy usando getActivity() porque estoy usando el EditText desde un Fragment.

Si en tu caso estás en un Activity, entonces puedes usar directamente getSupportFragmentManager().

Y este es el equivalente del método en Kotlin:

private fun showDatePickerDialog() {
    val newFragment = DatePickerFragment()
    newFragment.show(supportFragmentManager, "datePicker")
}

Hasta este punto:

De seguro que te aparece resaltado en rojo DatePickerFragment, como si de un error se tratase.

Eso es porque aún nos falta definir dicha clase:

public class DatePickerFragment extends DialogFragment
        implements DatePickerDialog.OnDateSetListener {

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Use the current date as the default date in the picker
        final Calendar c = Calendar.getInstance();
        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH);
        int day = c.get(Calendar.DAY_OF_MONTH);

        // Create a new instance of DatePickerDialog and return it
        return new DatePickerDialog(getActivity(), this, year, month, day);
    }

    public void onDateSet(DatePicker view, int year, int month, int day) {
        // Do something with the date chosen by the user
    }

}

Lo que esta clase hace es instanciar un DatePickerDialog y pre-seleccionar la fecha actual.

En su interior también podemos ver un método onDateSet. Este método es invocado cada vez que el usuario cambia la fecha en el DatePicker.

Esta clase la puedes ubicar en donde creas conveniente. Por ejemplo, si tienes una carpeta ui, en su interior puedes crear una carpeta dialog y situar allí la clase.

Estructura de carpetas

Y aquí su equivalente en Kotlin:

class DatePickerFragment : DialogFragment(), DatePickerDialog.OnDateSetListener {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        // Use the current date as the default date in the picker
        val c = Calendar.getInstance()
        val year = c.get(Calendar.YEAR)
        val month = c.get(Calendar.MONTH)
        val day = c.get(Calendar.DAY_OF_MONTH)

        // Create a new instance of DatePickerDialog and return it
        return DatePickerDialog(activity, this, year, month, day)
    }

    override fun onDateSet(view: DatePicker, year: Int, month: Int, day: Int) {
        // Do something with the date chosen by the user
    }

}

Cuarto paso: Capturar la fecha seleccionada

La interfaz DatePickerDialog.OnDateSetListener es la encargada de invocar el método onDateSet cada vez que una nueva fecha es seleccionada.

Pero si el método está definido sobre el mismo DatePickerFragment no nos resulta muy útil.

Nuestra intención es acceder a la fecha seleccionada desde nuestro fragment o activity (desde donde sea que estemos creando nuestro DatePickerFragment).

Para lograr ello, lo que tenemos que hacer es definir el listener en la clase que hará uso del DatePickerFragment, mas no en el mismo DatePickerFragment.

¿En dónde es que se está definiendo el listener actualmente y dónde es que debe definirse?

El listener (que escucha el evento de cambio de fecha) se está definiendo en nuestra clase DatePickerFragment.

En el método onCreateDialog estamos devolviendo una instancia de DatePickerDialog, tal como aquí se muestra:

return new DatePickerDialog(getActivity(), this, year, month, day);

El this que se usa como segundo parámetro es lo que hace que la misma clase sea la que tenga que actuar de listener, y es por eso que en la cabecera de nuestra clase nos encontramos con implements DatePickerDialog.OnDateSetListener.

Entonces te estarás preguntando, ¿cómo hacemos que el listener se defina en la clase desde la que creamos el dialog?

Para ello debemos crear el listener en la clase que invocará el dialog (sea un fragment o un activity, no hay problema). Y una vez creado, debemos pasar este listener a nuestro DialogFragment.

Para pasar el listener como parámetro a nuestro DialogFragment vamos a definir un método newInstance, que será el encargado de recibirlo. No podemos pasar el listener en el mismo constructor, porque no es seguro (y por tanto Android no lo permite).

Entonces nuestra clase quedaría de esta forma:

public class DatePickerFragment extends DialogFragment {

    private DatePickerDialog.OnDateSetListener listener;

    public static DatePickerFragment newInstance(DatePickerDialog.OnDateSetListener listener) {
        DatePickerFragment fragment = new DatePickerFragment();
        fragment.setListener(listener);
        return fragment;
    }

    public void setListener(DatePickerDialog.OnDateSetListener listener) {
        this.listener = listener;
    }

    @Override
    @NonNull
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final Calendar c = Calendar.getInstance();
        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH);
        int day = c.get(Calendar.DAY_OF_MONTH);

        return new DatePickerDialog(getActivity(), listener, year, month, day);
    }

}

Y en el caso de Kotlin:

class DatePickerFragment : DialogFragment() {

    private var listener: DatePickerDialog.OnDateSetListener? = null

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val c = Calendar.getInstance()
        val year = c.get(Calendar.YEAR)
        val month = c.get(Calendar.MONTH)
        val day = c.get(Calendar.DAY_OF_MONTH)

        return DatePickerDialog(activity, listener, year, month, day)
    }

    companion object {
        fun newInstance(listener: DatePickerDialog.OnDateSetListener): DatePickerFragment {
            val fragment = DatePickerFragment()
            fragment.listener = listener
            return fragment
        }
    }

}

Último paso: Usar nuestro DatePickerFragment

Nuestra clase DatePickerFragment nos va a permitir pedir una fecha al usuario, ¡tantas veces como sea necesario!

Solo tenemos que instanciar dicha clase y decirle qué hacer con la fecha escogida:

private void showDatePickerDialog() {
  DatePickerFragment newFragment = DatePickerFragment.newInstance(new DatePickerDialog.OnDateSetListener() {
      @Override
      public void onDateSet(DatePicker datePicker, int year, int month, int day) {
          // +1 because January is zero
          final String selectedDate = day + " / " + (month+1) + " / " + year;
          etPlannedDate.setText(selectedDate);
      }
  });

  newFragment.show(getActivity().getSupportFragmentManager(), "datePicker");
}

En el caso de Kotlin, si estamos trabajando sobre un Activity, podemos usar:

private fun showDatePickerDialog() {
    val newFragment = DatePickerFragment.newInstance(DatePickerDialog.OnDateSetListener { _, year, month, day ->
        // +1 because January is zero
        val selectedDate = day.toString() + " / " + (month + 1) + " / " + year
        etBirthDate.setText(selectedDate)
    })

    newFragment.show(supportFragmentManager, "datePicker")
}

Resumen

Podemos decir que todo se resume a 2 snippets:

  • la clase DatePickerFragment,
  • y el método showDatePickerDialog.

Pero he querido explicar paso a paso cómo es que llegamos a ese código.

Si entendemos el código que usamos, entonces nos será fácil recordar la lógica que hay detrás cuando necesitemos hacer cambios en el futuro.

Extra 1: Solicitar y capturar varias fechas con el formato adecuado

Si necesitas leer varias fechas, entonces necesitas un EditText para cada una.

En mi caso voy a leer 2 fechas, una fecha de planificación y una de cierre.

Entonces un fragmento de mi XML luce así:

<EditText
    android:id="@+id/etPlannedDate"
    android:hint="@string/report_planned_date"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="date"
    android:focusable="false"
    android:clickable="true"
    android:maxLines="1" />

<EditText
    android:id="@+id/etDeadline"
    android:hint="@string/report_deadline"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="date"
    android:focusable="false"
    android:clickable="true"
    android:maxLines="1" />

Luego en mi Fragment (tú podrías estar usando un activity, también funciona) hago lo siguiente:

etPlannedDate = (EditText) view.findViewById(R.id.etPlannedDate);
etPlannedDate.setOnClickListener(this);

etDeadline = (EditText) view.findViewById(R.id.etDeadline);
etDeadline.setOnClickListener(this);

Y resuelvo los eventos de clic de esta forma:

@Override
public void onClick(View view) {
    switch (view.getId()) {
        case R.id.etPlannedDate:
            showDatePickerDialog(etPlannedDate);
            break;

        case R.id.etDeadline:
            showDatePickerDialog(etDeadline);
            break;
    }
}

Finalmente el método showDatePickerDialog muestra el diálogo al usuario, y se encarga de asignar la cadena dd/mm/YYYY al EditText correspondiente para cada caso:

private void showDatePickerDialog(final EditText editText) {
    DatePickerFragment newFragment = DatePickerFragment.newInstance(new DatePickerDialog.OnDateSetListener() {
        @Override
        public void onDateSet(DatePicker datePicker, int year, int month, int day) {
            final String selectedDate = twoDigits(day) + "/" + twoDigits(month+1) + "/" + year;
            editText.setText(selectedDate);
        }
    });

    newFragment.show(getActivity().getSupportFragmentManager(), "datePicker");
}

Te preguntarás, ¿qué hace el método twoDigits?

Es para que los días o meses se muestren con 2 dígitos.

En vez de 7/7/2080 mostrará 07/07/2080.

Y aquí está:

private String twoDigits(int n) {
    return (n<=9) ? ("0"+n) : String.valueOf(n);
}

Aquí los paréntesis son opcionales, pero te recomiendo dejarlos, por legibilidad.

Si estás usando Kotlin, de hecho podemos definir una extension function y adjuntar el método twoDigits sobre la clase Int:

fun Int.twoDigits() =
    if (this <= 9) "0$this" else this.toString()

De modo que nuestro método showDatePickerDialog podría definirse de esta forma:

 private fun showDatePickerDialog() {
        val newFragment = DatePickerFragment.newInstance(DatePickerDialog.OnDateSetListener { _, year, month, day ->
        val dayStr = day.twoDigits()
        val monthStr = (month + 1).twoDigits() // +1 because January is zero

        val selectedDate = "$dayStr/$monthStr/$year"
        etBirthDate.setText(selectedDate)
    })

    newFragment.show(supportFragmentManager, "datePicker")
}

Por cierto:

Si aún no has usado Kotlin, te invito al curso de Laravel y Android que publiqué recientemente.

Puedes encontrar un enlace al mismo aquí debajo del artículo.

Extra 2: Valor mínimo y máximo para la fecha

En nuestra clase DatePickerFragment tenemos un método onCreateDialog.

Este método es el encargado de definir el DatePickerDialog que vamos a usar.

Por tanto podemos actualizarlo de esta manera:

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    // Current date
    val c = Calendar.getInstance()
    val year = c.get(Calendar.YEAR)
    val month = c.get(Calendar.MONTH)
    val day = c.get(Calendar.DAY_OF_MONTH)

    // Selected date (initial value)
    val datePickerDialog = DatePickerDialog(activity, listener, year - 18, month, day)

    // Min and max date
    c.set(Calendar.YEAR, year - 100)
    datePickerDialog.datePicker.minDate = c.timeInMillis
    c.set(Calendar.YEAR, year - 10)
    datePickerDialog.datePicker.maxDate = c.timeInMillis

    return datePickerDialog
}
  • En las primeras líneas sólo estamos capturando la fecha actual (en variables que guardan el año, mes y día).
  • Tales variables nos resultan útiles al momento de instanciar al DatePickerDialog (aquí definimos la fecha inicial que vendrá pre-seleccionada).
  • Por último accedemos al datePicker contenido en nuestro dialog y definimos el valor mínimo y máximo permitidos. Tras modificar, devolvemos el datePickerDialog tal como es requerido por el métodoonCreateDialog.

En este caso, el valor más antiguo que puede seleccionarse es 100 años atrás, y lo más reciente (valor máximo) es 10 años atrás, a partir de la fecha actual.

En este último ejemplo lo uso de esta manera porque necesito seleccionar fechas de nacimiento.

Recuerda que también puedes agregar atributos a la clase DatePickerFragment, para que la fecha mínima y máxima puedan definirse externamente, en vez de tratar con valores constantes.

Extra 3: Valor inicial a partir del EditText

Tal como comentaba en la sección anterior, podemos recibir parámetros adicionales en nuestra clase DatePickerFragment. Esto es posible a través del método newInstance.

Por ejemplo podemos definir nuevos atributos en la clase, que acompañen al listener que ya tenemos:

private var initialYear: Int = -1
private var initialMonth: Int = -1
private var initialDay: Int = -1

Y podemos sobrecargar el método newInstance de esta manera:

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

fun newInstance(listener: DatePickerDialog.OnDateSetListener, year: Int, month: Int, day: Int): DatePickerFragment {
    val fragment = DatePickerFragment()
    fragment.listener = listener
    fragment.initialYear = year
    fragment.initialMonth = month
    fragment.initialDay = day
    return fragment
}

Así, cuando queramos definir un valor inicial, podremos hacerlo; y cuando no, seguiremos usando un valor específico:

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    // Current date
    val c = Calendar.getInstance()
    val year = c.get(Calendar.YEAR)

    // Initial selected value
    if (initialYear == -1)
        initialYear = year - 18

    if (initialMonth == -1)
        initialMonth = c.get(Calendar.MONTH)

    if (initialDay == -1)
        initialDay = c.get(Calendar.DAY_OF_MONTH)

    val datePickerDialog = DatePickerDialog(activity, listener, initialYear, initialMonth, initialDay)

    // Min and max date
    c.set(Calendar.YEAR, year - 100)
    datePickerDialog.datePicker.minDate = c.timeInMillis
    c.set(Calendar.YEAR, year - 10)
    datePickerDialog.datePicker.maxDate = c.timeInMillis

    return datePickerDialog
}

El valor inicial dependerá de tus propios requerimientos.

Si quieres que el valor inicial dependa de la fecha escrita en el EditText, por ejemplo, puedes hacer lo siguiente:

private fun showDatePickerDialog() {
    val onNewDateListener = DatePickerDialog.OnDateSetListener { _, year, month, day ->
        val dayStr = day.twoDigits()
        val monthStr = (month + 1).twoDigits() // +1 because January is zero

        val selectedDate = "$dayStr/$monthStr/$year"
        etBirthDate.setText(selectedDate)
    }

    val birthDate = etBirthDate.text.toString()

    val newFragment = if (birthDate.isEmpty())
        DatePickerFragment.newInstance(onNewDateListener)
    else {
        val parts = birthDate.split('/')
        DatePickerFragment.newInstance(
            onNewDateListener,
            parts[2].toInt(), parts[1].toInt(), parts[0].toInt()
        )
    }

    newFragment.show(supportFragmentManager, "datePicker")
}

Este último fragmento actúa de la siguiente manera:

  • Primero definimos un listener en un val llamado onNewDateListener.
  • Luego leemos la fecha ingresada en etBirthDate.
  • Si está vacío el EditText, instanciamos sin definir una fecha inicial,
  • sino, hacemos split de la fecha, para obtener cada parte (día, mes y año a partir de la cadena).

Despedida

Si tienes alguna duda, o crees que podemos mejorar el código, puedes dejar un comentario. Así aprendemos todos.

Y si te ha sido de ayuda este artículo, por favor ayúdame a compartirlo.

¡Gracias por leer hasta el final!

# android

Logo de Programación y más

Comparte este post si te fue de ayuda 🙂.

Regístrate

Accede a todos los cursos, y resuelve todas tus dudas.

Cursos Recomendados

Imagen para el curso Laravel y Android

Laravel y Android

Curso intensivo. Incluye el desarrollo de una API, su consumo, y autenticación vía JWT. También vemos Kotlin desde 0.

Iniciar curso
Imagen para el curso Docker y Microservicios

Docker y Microservicios

Aprende por qué es importante y cómo funciona Docker, con este nuevo curso práctico!

Iniciar curso
Imagen para el curso Aprende Python

Aprende Python

Desarrolla tu primer Chatbot para Facebook Messenger sobre Google Cloud, y aprende Python en el camino!

Iniciar curso

Espera un momento ...

¿Te gustaría llevar mi curso de Laravel, gratis?

Sólo debes ingresar tus datos: