Los Time Pickers son componentes que te permiten seleccionar e ingresar valores de tiempo a partir de la especificación de horas, minutos o periodos de tiempo. Además de la elección para la franja del día (AM o PM).
Existen dos presentaciones visuales para la selección del tiempo: Dial e Input. En el primer caso se despliega un reloj circular, donde podrás seleccionar cada elemento del tiempo con toques en los números de guía.
Y la presentación del estilo Input facilita el ingreso de texto para cada componente del tiempo.
1. Componentes Para Usar El Time Picker En Android
En este tutorial veremos como implementar un Time Picker con la librería Jetpack Compose. La cual nos provee la función componible TimePicker()
para materializar el modo dial:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickerExample() {
val timePickerState = rememberTimePickerState(
initialHour = 10,
initialMinute = 30,
)
TimePicker(state = timePickerState)
}
En su forma más sencilla recibe su estado del tipo TimePickerState
producido con la función rememberTimePickerState()
.
Por otro lado, la función TimeInput()
se encarga de producir un Time Picker en modo input:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickerInputExample() {
val timePickerState = rememberTimePickerState(
initialHour = 10,
initialMinute = 30,
)
TimeInput(state = timePickerState)
}
No obstante, ambos elementos por lo general van envueltos en un diálogo. Requerimiento que podemos satisfacer a través de TimePickerDialog()
.
Veamos su uso a través de un ejemplo que nos guíe con un propósito definido.
2. Configurar Un Proyecto Android Para Usar Time Picker
2.1 Crear Un Nuevo Proyecto En Android Studio
Lo primero será crear un nuevo proyecto en Android Studio con una plantilla Empty Activity.
Al presionar Next, se te pedirá información general para configurar tu proyecto. En este caso usa el nombre que desees para este proyecto y ubícalo donde te parezca mejor:
Presiona Finish y espera a que el sistema de construcción termine.
2.2 Añadir Dependencias Gradle
Para este proyecto necesitamos las dependencias de Compose como ya hemos venido haciendo en los tutoriales anteriores. Pero adicionalmente añadiremos a la librería de kotlinx.datetime
.
dependencies {
//...
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
}
Usaremos la librería de fecha y tiempo de Kotlin a fin de evitar aplicar Desugaring para la librería java.time
.
2.3 Definir Punto De Entrada De La UI
Nuestro punto de entrada será la actividad MainActivity.kt
preconstruida por Android Studio. En su método onCreate()
invocaremos la función componible que representa la pantalla del ejemplo. A esta la llamaremos TimePickersScreen()
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TimePickersTheme {
TimePickersScreen()
}
}
}
}
2.4 Ver Código Completo De Time Pickers En GitHub
Si quieres explorar el código completo de este proyecto, puedes encontrarlo en el repositorio de GitHub. ¡No olvides dejar una estrella ✨ para mostrar tu apoyo!
3. Implementación Del Time Picker En Android
Con el espacio establecido para trabajar, comenzaremos a explorar varias necesidades que te ayudaran a interiorizar el uso de Time Pickers en Android.
Para ello he preparado un ejercicio donde a partir de la selección de una fecha inicial y una final, calculamos la duración o intervalo entre ambos valores. El formato que usaremos para la presentación de los tiempos será el sistema de doce horas, donde los sufijos am y pm indican la franja del día.
A partir de este aplicativo probaremos distintas funcionalidades asociadas a los Time Pickers y al manejo de tiempos en Kotlin.
3.1 Seleccionar Hora Con Un Reloj Digital
La primera meta que cumpliremos será mostrar un Time Picker que permita seleccionar el tiempo desde una vista con forma de reloj. Una vez el usuario confirme el valor, mostraremos ese valor en un texto en pantalla (puedes ver el resultado en la imagen de arriba).
¿Cómo lo resolvemos?
- Creamos el contenido principal
- Crear estado del contenido
- Creamos el Diálogo de Time Picker que será mostrado
- Mostramos el Time Picker en la acción de click
- Actualizamos el estado del texto cuando el usuario confirme el tiempo
Pasemos a implementar cada tarea.
3.1.1 Crear Contenido Principal
Abrimos el archivo TimePickersScreen.kt
y nos ubicamos en la función que comparte el mismo nombre. Debido a que tendremos el contenido principal y dos diálogos más, expresaremos la existencia de estos tres elementos de la siguiente forma:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickersScreen() {
var startPickerIsVisible by remember { mutableStateOf(false) }
var endPickerIsVisible by remember { mutableStateOf(false) }
TimePickersContent(
state = state, // Lo veremos al crear el ViewModel
onStartClick = { startPickerIsVisible = true },
onEndClick = { endPickerIsVisible = true }
)
// StartTimePicker()
// EndTimePicker()
}
Es necesario declarar los estados booleanos para visibilidad de ambos time pickers al mismo nivel del contenido principal. Esto con el fin de permitir disparar los eventos onStartClick
y onEndClick
para cambiar la visibilidad.
La imagen del ejemplo nos muestra un sencillo diseño para el contenido. Crearemos un componente Row
con dos TextFields
para mostrar las fechas y un Text
para la duración. Con esto en mente, crea un nuevo archivo Kotlin llamado TimePickersState.kt
y añade la siguiente implementación:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickersContent(
state: TimePickersState, // Vincular estado
onStartClick: () -> Unit,
onEndClick: () -> Unit
) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(stringResource(R.string.app_name))
}
)
},
content = { padding ->
Row(
modifier = Modifier
.padding(padding)
.padding(16.dp)
) {
TimeTextField(
label = "Inicio",
text = state.startTimeString(), // Vincular estado
onClick = onStartClick // Vincular evento
)
MediumSpace()
TimeTextField(
label = "Fin",
text = state.endTimeString(), // Vincular estado
onClick = onEndClick // Vincular evento
)
}
MediumSpace()
Text(text = "Duración: ${state.duration()}") // Vincular estado
}
)
}
Tenemos un campo de texto personalizado llamado TimeTextField
el cual recibe el evento de click para vincularlo al leading icon:
@Composable
private fun RowScope.TimeTextField(
label: String,
text: String,
onClick: () -> Unit
) {
val focusRequester = remember { FocusRequester() }
OutlinedTextField(
modifier = Modifier
.weight(1f)
.focusRequester(focusRequester),
label = {
Text(label)
},
value = text,
onValueChange = {},
readOnly = true,
leadingIcon = {
IconButton(
onClick = {
focusRequester.requestFocus()
onClick() // ← Muestra al Time Picker
}
) {
Icon(
imageVector = Icons.Filled.AccessTime,
contentDescription = null
)
}
}
)
}
3.1.2 Crear Estado De La Pantalla
Crea una nueva clase de datos para el estado y nómbrala TimePickersState
. Añade los strings necesarios para mostrar las fechas y la duración:
data class TimePickersState(
val startTimeHour: Int = 10,
val startTimeMinute: Int = 0,
val endTimeHour: Int = 11,
val endTimeMinute: Int = 0
) {
fun startTimeString(): String {
return LocalTime(startTimeHour, startTimeMinute).as12HoursFormat()
}
fun endTimeString(): String {
return LocalTime(endTimeHour, endTimeMinute).as12HoursFormat()
}
fun duration(): String {
val startDuration = startTimeHour.hours + startTimeMinute.minutes
val endDuration = endTimeHour.hours + endTimeMinute.minutes
var duration = (endDuration - startDuration)
if (duration.isNegative())
duration += 24.hours
return duration.toString()
}
}
El objetivo de función duration()
es tomar la hora y minuto de ambos tiempos y convertirlos a tipos Duration
. Esta clase representa una unidad de tiempo en una escala comparable, por lo que nos facilita la resta entre el intervalo.
Para ello usamos las propiedades de extensión asHour
y asMinute
. Al sumar ambos elementos de cada tiempo tendremos la duración de cada uno. Esto permitirá encontrar la diferencia del intervalo y expresarlo con Duration.toString()
.
3.1.3 Crear Diálogo Con Time Picker
Lo siguiente es crear un archivo llamado CustomTimePickerDialog.kt
. Su código tendrá la invocación de la función TimePickerDialog
junto al componente TimePicker
.
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun CustomTimePickerDialog(
currentHour: Int = currentHour(),
currentMinute: Int = currentMinute(),
onTimeSelected: (Int, Int) -> Unit,
onDismiss: () -> Unit,
) {
val timePickerState = rememberTimePickerState(
initialHour = currentHour,
initialMinute = currentMinute,
is24Hour = false
)
TimePickerDialog(
onDismissRequest = onDismiss,
title = {
Headline()
},
confirmButton = {
TextButton(onClick = {
onTimeSelected(timePickerState.hour, timePickerState.minute)
onDismiss()
}) {
Text("Aceptar")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancelar")
}
}
) {
TimePicker(state = timePickerState)
}
}
@Composable
private fun Headline() {
Box(modifier = Modifier
.fillMaxWidth()
.padding(bottom = 20.dp)) {
Text(
text = "Selecciona la hora",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
¿Cuál es la utilidad de los parámetros de nuestro componente CustomTimePickerDialog?
currentHour
: Valor inicial de la hora que se mostrarácurrentMinute
: Valor inicial de los minutos que se mostraránonTimeSelected
: Acción a ejecutar en el momento que se haga click en el botón de confirmaciónonDismiss
: Acción a ejecutar cuando se cierre el diálogo
Nota que el estado del time picker permite definir:
initialHour
: Hora inicial mostrada. Le pasamos parámetro del componenteinitialMinute
: Minutos iniciales mostrados. Le pasamos parámetro del componenteis24Hour
: Determinar si el formato mostrado es de 24 o 12 horas (aquí se muestra AM y PM). En nuestro caso usamosfalse
.
3.1.4 Mostrar El Time Picker
Con el componente base creado, ahora es posible crear tanto el diálogo para selección de fecha inicial, como el de la fecha final. Es solo asociar los estados correspondientes:
@Composable
fun StartTimePicker(
isVisible: Boolean,
state: TimePickersState,
onTimeSelected: (Int, Int) -> Unit,
onDismiss: () -> Unit
) {
if (isVisible) {
CustomTimePickerDialog(
currentHour = state.startTimeHour,
currentMinute = state.startTimeMinute,
onTimeSelected = onTimeSelected,
onDismiss = onDismiss
)
}
}
@Composable
fun EndTimePicker(
isVisible: Boolean,
state: TimePickersState,
onTimeSelected: (Int, Int) -> Unit,
onDismiss: () -> Unit
) {
if (isVisible) {
CustomTimePickerDialog(
currentHour = state.endTimeHour,
currentMinute = state.startTimeMinute,
onTimeSelected = onTimeSelected,
onDismiss = onDismiss
)
}
}
Así que podemos ir a TimePickersExample
e invocar ambos elementos:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickersScreen(
viewModel: TimePickersViewModel = viewModel() // En breve
) {
val state by viewModel.state.collectAsStateWithLifecycle() // En breve
var startPickerIsVisible by remember { mutableStateOf(false) }
var endPickerIsVisible by remember { mutableStateOf(false) }
TimePickersContent(
state = state,
onStartClick = { startPickerIsVisible = true },
onEndClick = { endPickerIsVisible = true }
)
StartTimePicker(
isVisible = startPickerIsVisible,
state = state,
onTimeSelected = viewModel::updateStartTime, // En breve
onDismiss = { startPickerIsVisible = false }
)
EndTimePicker(
isVisible = endPickerIsVisible,
state = state,
onTimeSelected = viewModel::updateEndTime, // En breve
onDismiss = { endPickerIsVisible = false }
)
}
Con eso completamos nuestra interfaz gráfica, pero aún necesitamos a nuestro ViewModel para entregarle la responsabilidad de manejar el estado y los eventos.
3.1.5 Actualizar Estado Con ViewModel
En el apartado anterior observaste que se expresó la necesidad de tener funciones updateStart*()
para modificar los estados de las fechas al seleccionarse.
De esta forma es posible actualizar el estado general de nuestra pantalla que es colectado por collectAsStateWithLifecycle()
.
Teniendo eso en cuenta, crea un nuevo view model llamado TimePickersViewModel
con la siguiente implementación:
class TimePickersViewModel : ViewModel() {
private val _state = MutableStateFlow(TimePickersState())
val state = _state.asStateFlow()
fun updateStartTime(hour: Int, minute: Int) {
_state.update { currentState ->
currentState.copy(
startTimeHour = hour,
startTimeMinute = minute
)
}
}
fun updateEndTime(hour: Int, minute: Int) {
_state.update { currentState ->
currentState.copy(
endTimeHour = hour,
endTimeMinute = minute
)
}
}
}
El código evidencia que updateStartTime()
reemplaza el estado por una nueva copia con los valores seleccionados de startTimeHour
y startTimeMinute
. De la misma manera updateEndTime()
actualiza los valores asociados a la hora final.
Ejecuta el ejemplo: prueba clickando el icono del campo de texto, selecciona el tiempo y confirma. Esto debe cambiar el campo de texto y la duración:
3.2 Permitir Ingreso Manual De La Hora
Lo siguiente será habilitar la modificación de ambos tiempos con el Time Picker estilo Input.
Para que CustomTimePickerDialog
posea esta característica, precisamos de:
- Un estado booleano que determine el modo actual
- Un icono que al ser clickeado, alterne entre ambos modos y cambie su imagen basada en el estado mencionado
- Un condicional que evalúe el estado y decida invocar a
TimePicker()
oTimeInput()
Fundamentado en los pasos anteriores, ahora nuestro componente luciría de esta forma:
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun CustomTimePickerDialog(
currentHour: Int = currentHour(),
currentMinute: Int = currentMinute(),
onTimeSelected: (Int, Int) -> Unit,
onDismiss: () -> Unit,
) {
val timePickerState = rememberTimePickerState(
initialHour = currentHour,
initialMinute = currentMinute,
is24Hour = false
)
var isDialType by remember { mutableStateOf(true) } // ← 1. Nuevo estado
TimePickerDialog(
onDismissRequest = onDismiss,
title = {
Headline()
},
modeToggleButton = { // ← 2. Icono de alternación
IconButton(onClick = { isDialType = !isDialType }) {
Icon(imageVector = toggleIconFor(isDialType), contentDescription = null)
}
},
confirmButton = {
TextButton(onClick = {
onTimeSelected(timePickerState.hour, timePickerState.minute)
onDismiss()
}) {
Text("Aceptar")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancelar")
}
}
) {
if (isDialType) { // ← 3. Condicional para invocación
TimePicker(state = timePickerState)
} else {
TimeInput(state = timePickerState)
}
}
}
private fun toggleIconFor(isDialType: Boolean): ImageVector {
return if (isDialType) {
Icons.Outlined.Keyboard
} else {
Icons.Outlined.AccessTime
}
}
Observa que el slot para el icono es soportado por el parámetro modeToggleButton
de TimePickerDialog
. Según se había planeado, en su interior creamos un IconButton
que alterna al estado isDialType
y a su vez elige el icono con toggleIconFor()
.
Seguido, modificamos la lambda de contenido para añadir un if
que seleccione la función a invocar según el tipo actual.
3.4 Usar Formato De 24 Horas
En el inicio viste que el sistema de horario del Time Picker está definido por la propiedad booleana TimePickerState.is24Hours
. A la cual le asignamos false
para ver nuestra UI con una presentación AM/PM.
Ahora, ¿Qué hacer si deseas el sistema 24 horas?
Evidentemente asignarle true
. Por defecto el valor está dado por el valor en la App de ajustes del usuario en Android. Pero si lo deseas, permite que venga desde la firma como parámetro de la siguiente manera:
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun CustomTimePickerDialog(
currentHour: Int = currentHour(),
currentMinute: Int = currentMinute(),
is24Hour: Boolean = false, // ← Estado elevado
onTimeSelected: (Int, Int) -> Unit,
onDismiss: () -> Unit,
) {
val timePickerState = rememberTimePickerState(
initialHour = currentHour,
initialMinute = currentMinute,
is24Hour = is24Hour // ← Aplicamos
)
//...
De esta forma solo debes pasar true
en el picker. Por ejemplo, el time picker de la hora final:
@Composable
fun EndTimePicker(
isVisible: Boolean,
state: TimePickersState,
onTimeSelected: (Int, Int) -> Unit,
onDismiss: () -> Unit
) {
if (isVisible) {
CustomTimePickerDialog(
currentHour = state.endTimeHour,
currentMinute = state.startTimeMinute,
is24Hour = true, // ← Habilitamos formato 24 horas
onTimeSelected = onTimeSelected,
onDismiss = onDismiss
)
}
}
Conclusión
En este tutorial hemos explorado a fondo la implementación de Time Pickers en Android utilizando Jetpack Compose. Desde su configuración inicial hasta la creación de una interfaz funcional con TimePickerDialog
, hemos cubierto los aspectos clave para integrar una selección de tiempo eficiente en una aplicación.
Al emplear rememberTimePickerState()
junto con componentes como TimePicker
y TimeInput
, logramos crear una experiencia intuitiva para el usuario, permitiendo seleccionar y mostrar intervalos de tiempo de manera clara y precisa. Además, aprovechamos las capacidades de kotlinx.datetime
para manejar la duración entre dos horarios sin depender de la API de java.time
.
Con este conocimiento, ahora puedes adaptar y expandir la funcionalidad según las necesidades de tu aplicación, incorporando validaciones, formatos personalizados o incluso combinando el Time Picker con un Date Picker para ofrecer una experiencia completa de selección de fecha y hora.
¿Estás Creando Una App De tareas?
Te comparto una plantilla Android profesional con arquitectura limpia, interfaz moderna y funcionalidades listas para usar. Ideal para acelerar tu desarrollo.

Únete Al Discord De Develou
Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!