Cómo pedir una fecha en Android usando DatePicker
Tiempo de lectura: 8.46 minutos
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.
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
enfalse
yclickable
entrue
.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.
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 eldatePickerDialog
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
llamadoonNewDateListener
. - 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!