Icono del sitio Develou

Date Pickers En Android

Los Date Pickers son componentes que permiten al usuario seleccionar fechas o rangos de fechas. Estos son incorporados en diálogos debido a que su diseño ocupa gran área de la pantalla.

Adicionalmente, disponen de un modo Input en el cual la selección se da por un campo de texto:

La idea de este tutorial es que aprendas a implementar tanto un Date Picker como un Date Range Picker en Jetpack Compose.

Veamos cómo hacerlo.


Proyecto En Android Studio Para Date Pickers

1. En primer lugar crearemos un nuevo proyecto en Android Studio con la plantilla de creación Empty Activity de tipo Jetpack Compose. Nombralo como “Date Pickers”.

2. Una vez construido el proyecto, crearemos un archivo llamado DatePickersScreen.kt donde declararemos una función @Composable con el mismo nombre:

import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun DatePickersScreen() {    

    Scaffold(
        topBar = { TopBar() },
        content = { padding ->
            DatePickersContent(
                padding = padding
            )
        }
    )
}

@Preview
@Composable
private fun Preview() {
    DatePickersTheme {
        DatePickersScreen()
    }
}

3. La función DatePickersContent() la ubicamos en un nuevo archivo DatePickersContent.kt. Esta contiene a cada elemento de los ejemplos que veremos:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun DatePickersContent(
    padding: PaddingValues
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(padding)
            .padding(16.dp)
    ) {
        // Futuro contenido
    }
}

4. Luego abre MainActivity y reemplaza la invocación por defecto por la llamada a DatePickersScreen().

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DatePickersTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    DatePickersScreen()
                }
            }
        }
    }
}

5. Y debido a que vamos a usar fechas, habilitamos el Desugaring del JDK 8 para usar el paquete java.time. Esto lo logras añadiendo las siguientes líneas al archivo build.gradle.kts del módulo :app.

android {
    //..
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    //..
}

dependencies {

    // Desugaring JDK
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")

    // ...
}

Con estas preparaciones, ya te es posible comenzar a añadir el código de los ejemplos del tutorial.

Si deseas ver el código final de este proyecto puedes acceder al repositorio de GitHub. Te pido le des una estrella ✨como señal de tu apoyo:

Ahora sí, pasemos a ver el primer ejemplo sobre date pickers.


Crear Un Date Picker

Ejemplo de Date Picker

Caso De Uso:

Este ejemplo consiste de un Text Field que permite al usuario seleccionar una fecha. Para ello, este debe hacer click en el Leading Icon, lo que inicia el diálogo con un Date Picker en su interior. Una vez un valor sea confirmado, el valor del campo de texto será actualizado con la fecha formateada.

Herramientas:

Se requieren de dos componentes, a DatePickerDialog() para el diálogo y DatePicker() para la vista del selector de fechas.

Para DatePickerDialog tenemos la siguiente firma:

@ExperimentalMaterial3Api
@Composable
fun DatePickerDialog(
    onDismissRequest: () -> Unit,
    confirmButton: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    dismissButton: (@Composable () -> Unit)? = null,
    shape: Shape = DatePickerDefaults.shape,
    tonalElevation: Dp = DatePickerDefaults.TonalElevation,
    colors: DatePickerColors = DatePickerDefaults.colors(),
    properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
    content: @Composable ColumnScope.() -> Unit
): Unit

En el ejemplo usaremos los siguientes parámetros:

En el caso de DatePicker() tenemos:

@ExperimentalMaterial3Api
@Composable
fun DatePicker(
    state: DatePickerState,
    modifier: Modifier = Modifier,
    dateFormatter: DatePickerFormatter = remember { DatePickerDefaults.dateFormatter() },
    title: (@Composable () -> Unit)? = {
        DatePickerDefaults.DatePickerTitle(
            displayMode = state.displayMode,
            modifier = Modifier.padding(DatePickerTitlePadding)
        )
    },
    headline: (@Composable () -> Unit)? = {
        DatePickerDefaults.DatePickerHeadline(
            selectedDateMillis = state.selectedDateMillis,
            displayMode = state.displayMode,
            dateFormatter = dateFormatter,
            modifier = Modifier.padding(DatePickerHeadlinePadding)
        )
    },
    showModeToggle: Boolean = true,
    colors: DatePickerColors = DatePickerDefaults.colors()
): Unit

La mayoría de sus parámetros tienen valores por defecto que nos facilita la invocación de DatePicker(). Por lo que solo usaremos a state, para obtener la fecha seleccionada a través de la propiedad DatePickerState.selectedDateMillis.

Solución:

Crear Estado 

Lo primero será crear el estado para el diálogo del date picker. ¿De qué consiste su UI?

Las propiedades anteriores las declaramos en una nueva clase de datos llamada DatePickerDialogState:

data class DatePickerDialogState(
    val date: Long?,
    val isPickerVisible: Boolean,
    val onStart: () -> Unit,
    val onConfirmationClick: (Long?) -> Unit,
    val onDismissClick: () -> Unit
){
    val isPickerHidden get() = !isPickerVisible
}

Crear ViewModel

Para mutar el estado y ejecutar acciones para los eventos de UI, crearemos la clase DatePickerDialogViewModel:

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

class DatePickerDialogViewModel : ViewModel() {

    private val _state = MutableStateFlow(
        DatePickerDialogState(
            date = null,
            isPickerVisible = false,
            onStart = ::onStart,
            onDismissClick = ::onDismissClick,
            onConfirmationClick = ::onConfirmationClick
        )
    )

    val state = _state.asStateFlow()

    private fun onStart() {
        _state.update { it.copy(isPickerVisible = true) }
    }

    private fun onDismissClick() {
        _state.update { it.copy(isPickerVisible = false) }
    }

    private fun onConfirmationClick(date: Long?) {
        _state.update { it.copy(isPickerVisible = false, date = date) }
    }
}

Si prestas atención, hemos creado tres funciones para procesar los eventos propuestos en el estado:

Crear Date Picker Dialog

El siguiente paso es implementar la interfaz gráfica en una función de Compose. Para ello crea el archivo CustomDatePickerDialog.kt y añade una función con el mismo nombre.

Esta debe:

  1. Parámetros: Recibir el estado que hemos creado previamente y un parámetro tipo función que comunica al exterior el momento en que se confirma la selección de la fecha
  2. Prevenir la invocación del diálogo si este está oculto
  3. Estados internos: Inicializar estado de tipo DatePickerState con la utilidad rememberDatePickerState() y pasar a initialSelectedDateMillis la fecha de nuestro estado
  4. Invocar a DatePickerDialog()
    • onDismissRequest: Le pasamos la propiedad onDismissClick
    • confirmButton: Creamos un elemento TextButton(), cuyo parámetro onClick es la invocación de onConfirmationClick y onDateSelected. Además, deshabilitamos el botón si la fecha seleccionada es null.
    • dismissButton: Creamos un TextButton y pasamos a onDismissClick en su acción
    • content: Invocamos a la función DatePicker() con internalState

Implementando el algoritmo anterior en código tendrás:

import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun CustomDatePickerDialog(
    state: DatePickerDialogState,
    onDateSelected: (Long?) -> Unit
) {
    if (state.isPickerHidden) return

    val internalState = rememberDatePickerState(
        initialSelectedDateMillis = state.date
    )

    DatePickerDialog(
        onDismissRequest = state.onDismissClick,
        confirmButton = {
            TextButton(
                onClick = {
                    state.onConfirmationClick(internalState.selectedDateMillis)
                    onDateSelected(internalState.selectedDateMillis)
                },
                enabled = internalState.selectedDateMillis != null
            ) {
                Text("Aceptar")
            }
        },
        dismissButton = {
            TextButton(onClick = state.onDismissClick) {
                Text("Cancelar")
            }
        },
        content = { DatePicker(state = internalState) },
    )
}

Mostrar Fecha En Text Field

Text Field para fecha

Para iniciar a CustomDatePickerDialog y mostrar la fecha seleccionada, necesitamos modificar nuestra función componible del contenido principal.

Los siguientes son los pasos para realizarlo:

1. Crea el estado del contenido principal en una nueva clase de datos llamada DatePickersState. Declara una propiedad por las siguientes datos e interacciones:

Implementado es:

data class DatePickersState(
    val dateSelected: String,
    val rangeSelected: String,
    val onDateSelected: (Long?) -> Unit,
    val onRangeSelected: (Long?, Long?) -> Unit
)

2. Luego creamos el viewmodel para sostener el estado anterior y responder a los clicks:

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

class DatePickersViewModel : ViewModel() {

    private val _state = MutableStateFlow(
        DatePickersState(
            dateSelected = "Seleccionar fecha",
            rangeSelected = "Seleccionar rango",
            onDateSelected = ::onDateConfirmation,
            onRangeSelected = ::onRangeConfirmation
        )
    )

    val state = _state.asStateFlow()

    fun onDateConfirmation(date: Long?) {
        _state.update { it.copy(dateSelected = formatDate(date)) }
    }

    fun onRangeConfirmation(startDate: Long?, endDate: Long?) {
        _state.update { it.copy(rangeSelected = formatRange(startDate, endDate)) }
    }
}

3. Los métodos onDateConfirmation() y onRangeConfirmation() usan funciones que formatean los milisegundos a Strings con un patrón de fechas presentables.

Para lograr este cometido usamos el paquete java.time y la clase DateTimeFormatter. Dichas utilidades las ubicaremos en un nuevo archivo DateExtensions.kt:

import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

fun formatDate(date: Long?): String {
    return date?.asInstant()?.asShortString() ?: "Seleccionar fecha"
}

fun formatRange(start: Long?, end: Long?): String {
    if (start == null || end == null) return "Seleccionar rango"

    val startDate = Instant.ofEpochMilli(start).asShortString()
    val endDate = Instant.ofEpochMilli(end).asShortString()

    return "De $startDate a $endDate"
}

fun Instant.asShortString(): String {
    return format("dd/MM/yyyy")
}

fun Instant.asMonthAndDayString(): String {
    return format("MMM dd").replaceFirstChar { it.titlecase() }
}

fun Instant.format(pattern: String): String {
    return DateTimeFormatter
        .ofPattern(pattern)
        .withZone(ZoneOffset.UTC)
        .format(this)
}

fun Long?.asInstant(): Instant? = this?.let { Instant.ofEpochMilli(it) }

4. Mostrar el Date Picker requiere de un elemento de interacción. En nuestro caso es el botón inicial de un campo de texto que se encuentra en la pantalla principal.

Esto significa que es momento de modificar la función DatePickersContent(). En ella, necesitamos:

  1. Parámetros: El padding del Scaffold, el estado principal, la función al clickarse el botón para iniciar el date picker y la función para iniciar el date range picker (lo veremos más adelante)
  2. Contenedor principal: Usaremos un layout Column() para ordenar un texto y un Text Field verticalmente
  3. OutlinedTextField()
    • value: Será la propiedad DatePickersState.dateSelected
    • trailingIcon: Asignaremos como argumento al parámetro onDateButtonClick

Planteado lo anterior, el código resultante es:

@Composable
fun DatePickersContent(
    padding: PaddingValues,
    state: DatePickersState,
    onDateButtonClick: () -> Unit,
    onRangeButtonClick: () -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(padding)
            .padding(16.dp)
    ) {
        Text("Date Picker")
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = state.dateSelected,
            onValueChange = {},
            readOnly = true,
            leadingIcon = {
                IconButton(onClick = onDateButtonClick) {
                    Icon(imageVector = Icons.Filled.CalendarToday, contentDescription = null)
                }
            }
        )
    }
}

5. Luego inicializa los view models del date picker y del contenido principal. Con dichas instancias, ya es posible leer sus estados para invocar a CustomDatePickerDialog():

@Composable
fun DatePickersScreen(
    datePickerViewModel: DatePickerDialogViewModel = viewModel(),
    mainContentViewModel: DatePickersViewModel = viewModel()
) {
    val datePickerState by datePickerViewModel.state.collectAsState()
    val mainContentState by mainContentViewModel.state.collectAsState()

    CustomDatePickerDialog(
        state = datePickerState,
        onDateSelected = mainContentState.onDateSelected
    )

    Scaffold(
        topBar = { TopBar() },
        content = { padding ->
            DatePickersContent(
                padding = padding,
                state = mainContentState,
                onDateButtonClick = datePickerState.onStart
            )
        }
    )
}

Observa que tanto el diálogo le confirma la selección de fecha al contenido principal con onDateSelected; y el contenido principal le confirma al diálogo su inicio con onStart.

Termina este ejemplo, corriendo el proyecto Android Studio y comprobando el funcionamiento de la selección de fechas.


Crear Date Range Picker

Ejemplo Date Range Picker

Caso De Uso:

Este ejemplo es similar al anterior, la diferencia es que esta vez seleccionaremos dos fechas para establecer un rango y presentarlo en el contenido del Text Field.

Herramientas:

La creación de un selector de rango de fechas involucra la creación del diálogo con AlertDialog() y la creación del widget con DateRangePicker().

De la función DateRangePicker() solo usaremos su parámetro state de tipo DateRangePickerState, cuya firma recibe los siguientes parámetros:

@ExperimentalMaterial3Api
@Composable
fun DateRangePicker(
    state: DateRangePickerState,
    modifier: Modifier = Modifier,
    dateFormatter: DatePickerFormatter = remember { DatePickerDefaults.dateFormatter() },
    title: (@Composable () -> Unit)? = {
        DateRangePickerDefaults.DateRangePickerTitle(
            displayMode = state.displayMode,
            modifier = Modifier.padding(DateRangePickerTitlePadding)
        )
    },
    headline: (@Composable () -> Unit)? = {
        DateRangePickerDefaults.DateRangePickerHeadline(
            selectedStartDateMillis = state.selectedStartDateMillis,
            selectedEndDateMillis = state.selectedEndDateMillis,
            displayMode = state.displayMode,
            dateFormatter,
            modifier = Modifier.padding(DateRangePickerHeadlinePadding)
        )
    },
    showModeToggle: Boolean = true,
    colors: DatePickerColors = DatePickerDefaults.colors()
): Unit

También usaremos el parámetro headline para personalizar la presentación de las fechas en el rango.

Solución:

Crear Estado

El estado para un selector de rangos es exactamente igual al del date picker. La única diferencia es la fecha adicional. Teniendo en cuenta crea la data class DateRangePickerDialogState:

data class DateRangePickerDialogState(
    val startDate: Long?,
    val endDate: Long?,
    val isPickerVisible: Boolean,
    val onStart:()->Unit,
    val onConfirmationClick: (Long?, Long?) -> Unit,
    val onDismissClick: () -> Unit
) {
    val isPickerHidden get() = !isPickerVisible
}

Cómo ves, el rango se representa por [startDate, endDate]; y onConfirmationClick recibe ambos argumentos para mutar el valor actual.

Crear ViewModel

Acto seguido, añade la clase RangeDatePickerDialogViewModel. Declara el estado y las funciones que responden a los eventos de UI como hicimos con DatePickerDialogViewModel. La única diferencia a remarcar es la existencia de una segunda fecha:

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

class RangeDatePickerDialogViewModel : ViewModel() {

    private val _state = MutableStateFlow(
        DateRangePickerDialogState(
            startDate = null,
            endDate = null,
            isPickerVisible = false,
            onStart = ::onStart,
            onConfirmationClick = ::onRangeConfirmationClick,
            onDismissClick = ::onDismissClick
        )
    )

    val state = _state.asStateFlow()

    private fun onStart() {
        _state.update { it.copy(isPickerVisible = true) }
    }

    private fun onRangeConfirmationClick(startDate: Long?, endDate: Long?) {
        _state.update {
            it.copy(startDate = startDate, endDate = endDate, isPickerVisible = false)
        }
    }

    private fun onDismissClick() {
        _state.update { it.copy(isPickerVisible = false) }
    }
}

Crear Interfaz

Pasemos a crear un nuevo archivo Kotlin llamado CustomDateRangePickerDialog.kt con la función @Composable que declarará el diálogo para la selección de rangos.

¿Cómo construirla?

  1. Parámetros: Pasa el estado y un tipo función para confirmar al exterior la selección del rango
  2. Visibilidad: Antes de toda declaración, verifica si el date range picker está oculto, para evitar el dibujado en la composición
  3. Estados internos: Construye el estado inicial del tipo DateRangPickerState con la función de utilidad rememberDateRangePickerState(). Pasa las fechas de nuestro estado a los parámetros initialSelectedStartDateMillis y initialSelectedEndDateMillis
  4. Invoca el componente AlertDialog()
    • properties: Pasa un elemento DialogProperties, donde su propiedad usePlatformDefaultWidth sea determinada por el valor de displayMode del range picker. Esto permitirá expandir el diálogo completamente si está en modo DisplayMode.Picker
    • onDismissRequest: Pasamos la propiedad onDismissClick
    • content: Dibujaremos una Surface que contenga una Column, la cual distribuirá los botones del diálogo y el componente DateRangePicker()

La implementación con respecto a los pasos anteriores es:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomDateRangePickerDialog(
    state: DateRangePickerDialogState,
    onConfirmation: (Long?, Long?) -> Unit
) {

    if (state.isPickerHidden) return

    val internalState = rememberDateRangePickerState(
        initialSelectedStartDateMillis = state.startDate,
        initialSelectedEndDateMillis = state.endDate
    )

    val headline by remember { derivedStateOf { internalState.headline } }

    AlertDialog(
        properties = DialogProperties(usePlatformDefaultWidth = internalState.displayMode == DisplayMode.Input),
        onDismissRequest = state.onDismissClick,
        content = {
            Surface(shape = MaterialTheme.shapes.large) {
                Column(
                    modifier = Modifier.dateRangeDialogModifier(internalState),
                    verticalArrangement = Arrangement.Top
                ) {
                    ModalButtons(state, internalState, onConfirmation)
                    DateRangePicker(
                        state = internalState,
                        headline = {
                            Text(headline, modifier = Modifier.padding(start = 64.dp))
                        }
                    )
                    InputButtons(state, internalState, onConfirmation)
                }
            }
        }
    )
}

Si te fijas, las funciones ModalButtons() e InputButtons() son los botones asociados según el tipo de visualización. Ambos reciben los estados para definir su visibilidad y delegan las propiedades para confirmación y cancelación de la selección.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ColumnScope.InputButtons(
    state: DateRangePickerDialogState,
    pickerState: DateRangePickerState,
    onRangeConfirmation: (Long?, Long?) -> Unit
) {
    if (pickerState.displayMode == DisplayMode.Picker) return

    Row(modifier = Modifier.align(Alignment.End)) {
        TextButton(onClick = state.onDismissClick) {
            Text(text = "Cancelar")
        }

        TextButton(
            onClick = {
                state.onConfirmationClick(
                    pickerState.selectedStartDateMillis,
                    pickerState.selectedEndDateMillis
                )
                onRangeConfirmation(
                    pickerState.selectedStartDateMillis,
                    pickerState.selectedEndDateMillis
                )
            },
            enabled = pickerState.selectedEndDateMillis != null
        ) {
            Text(text = "Guardar")
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ModalButtons(
    state: DateRangePickerDialogState,
    pickerState: DateRangePickerState,
    onRangeConfirmation: (Long?, Long?) -> Unit
) {
    if (pickerState.displayMode == DisplayMode.Input) return

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 12.dp, end = 12.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        IconButton(onClick = state.onDismissClick) {
            Icon(Icons.Filled.Close, contentDescription = "Cerrar")
        }
        TextButton(
            onClick = {
                state.onConfirmationClick(
                    pickerState.selectedStartDateMillis,
                    pickerState.selectedEndDateMillis
                )
                onRangeConfirmation(
                    pickerState.selectedStartDateMillis,
                    pickerState.selectedEndDateMillis
                )
            },
            enabled = pickerState.selectedEndDateMillis != null
        ) {
            Text(text = "Guardar")
        }
    }
}

Ahora, hemos creado el estado derivado headline para formatear el valor de la cabeza cada que el usuario cambia la selección de una fecha. Su valor es el uso de nuestras utilidades de fechas:

@OptIn(ExperimentalMaterial3Api::class)
val DateRangePickerState.headline:String get() {
    val start = selectedStartDateMillis.asInstant()?.asMonthAndDayString()?:"Inicio"
    val end = selectedEndDateMillis.asInstant()?.asMonthAndDayString()?:"Fin"

    return "$start - $end"
}

Mostrar Rango De Fechas

Text Field para rango de fechas

Con el diálogo de selección de rangos definido, resta añadir el Text Field.

Inicia el range date picker desde DatePickersContent() a partir de un parámetro onRangeButtonClick:

@Composable
fun DatePickersContent(
    padding: PaddingValues,
    state: DatePickersState,
    onDateButtonClick: () -> Unit,
    onRangeButtonClick: () -> Unit // <-
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(padding)
            .padding(16.dp)
    ) {
        //...

        Spacer(Modifier.size(16.dp))

        Text("Date Picker con Rango")
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = state.rangeSelected,
            onValueChange = {},
            readOnly = true,
            leadingIcon = {
                IconButton(onClick = onRangeButtonClick) {
                    Icon(imageVector = Icons.Filled.DateRange, contentDescription = null)
                }
            }
        )
    }
}

Acto seguido, desde DatePickersScren() y:

  1. Crea la instancia de DateRangePickerDialogViewModel
  2. Invoca a CustomerDateRangePickerDialog()
  3. Pasa a onStart() al parámetro onRangeConfirmed de DatePickersContent()
@Composable
fun DatePickersScreen(
    datePickerViewModel: DatePickerDialogViewModel = viewModel(),
    rangePickerViewModel: DateRangePickerDialogViewModel = viewModel(), // (1)
    mainContentViewModel: DatePickersViewModel = viewModel()
) {
    val datePickerState by datePickerViewModel.state.collectAsState()
    val rangePickerState by rangePickerViewModel.state.collectAsState()
    val mainContentState by mainContentViewModel.state.collectAsState()

    CustomDatePickerDialog(
        state = datePickerState,
        onDateSelected = mainContentState.onDateSelected
    )

    CustomDateRangePickerDialog(// (2)
        state = rangePickerState,
        onConfirmation = mainContentState.onRangeSelected
    )

    Scaffold(
        topBar = { TopBar() },
        content = { padding ->
            DatePickersContent(
                padding = padding,
                state = mainContentState,
                onDateButtonClick = datePickerState.onStart,
                onRangeButtonClick = rangePickerState.onStart // (3)
            )
        }
    )
}

Finaliza ejecutando el proyecto Android Studio, haz click en el leading icon del Text Field de rango, selecciona las fechas, confirma y visualiza el cambio.

Salir de la versión móvil