Jetpack Compose: Working with DatePickers

Jetpack Compose, Android’s modern UI toolkit, is revolutionizing the way we build user interfaces. Date pickers are a common and crucial UI element in many applications, allowing users to select dates easily and intuitively. In this comprehensive guide, we’ll explore how to implement and customize date pickers in Jetpack Compose, providing you with a practical understanding and numerous code examples.

Understanding DatePickers in Jetpack Compose

Date pickers provide a user-friendly interface for selecting dates. They typically display a calendar-like view, enabling users to navigate through months and years to choose a specific date. With Jetpack Compose, you can easily integrate date pickers into your applications using Material Design components or custom implementations.

Why Use DatePickers?

  • Improved User Experience: Simplifies date selection with an intuitive UI.
  • Data Accuracy: Reduces errors associated with manual date input.
  • Consistent Design: Provides a familiar interface for date selection across different platforms.

Implementing DatePickers in Jetpack Compose

Jetpack Compose offers multiple ways to implement date pickers, including using Material Design components and creating custom date pickers. We’ll start with the simplest approach and gradually move to more complex customizations.

Method 1: Using androidx.compose.material3.DatePicker

The Material 3 DatePicker is part of the androidx.compose.material3 library. It offers a straightforward and customizable way to integrate date pickers into your Compose UI.

Step 1: Add Dependencies

Ensure that you have the Material 3 library added to your build.gradle file:


dependencies {
    implementation("androidx.compose.material3:material3:1.1.2")
    implementation("androidx.compose.material3:material3-window-size-class:1.1.2") //optional if using window size class
    implementation("androidx.compose.material:material-icons-extended:1.5.4")
    implementation("androidx.compose.runtime:runtime-livedata:1.5.4")
}
Step 2: Basic Implementation

Here’s a simple example of using the DatePicker composable:


import androidx.compose.foundation.layout.Column
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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.tooling.preview.Preview
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleDatePicker() {
    val openDialog = remember { mutableStateOf(false) }
    val date = remember { mutableStateOf("") }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        TextButton(onClick = { openDialog.value = true }) {
            Text("Select Date")
        }
        Text(text = "Selected date: ${date.value}")
    }
    

    if (openDialog.value) {
        val datePickerState = rememberDatePickerState()
        val confirmEnabled = remember {
            derivedStateOf { datePickerState.selectedDateMillis != null }
        }
        DatePickerDialog(
            onDismissRequest = { openDialog.value = false },
            confirmButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                        datePickerState.selectedDateMillis?.let { millis ->
                            date.value = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(millis))
                        }
                    },
                    enabled = confirmEnabled.value
                ) {
                    Text("Confirm")
                }
            },
            dismissButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                    }
                ) {
                    Text("Dismiss")
                }
            }
        ) {
            DatePicker(state = datePickerState)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun SimpleDatePickerPreview() {
    SimpleDatePicker()
}

In this example:

  • A button opens a dialog with a DatePicker inside.
  • The rememberDatePickerState stores the selected date.
  • The dialog shows ‘Confirm’ and ‘Dismiss’ buttons for selecting and cancelling.
  • On confirmation, the selected date is formatted and displayed.
Step 3: Configuring the DatePicker State

The rememberDatePickerState can be customized. It includes options for setting initial dates and date validator.


import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.DatePickerState
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import java.time.Instant
import java.time.ZoneId

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfiguredDatePickerState(): DatePickerState {
    // Define range for valid dates
    val startDateMillis = Instant.now().minusMillis(365L * 24 * 3600 * 1000).toEpochMilli() // one year ago
    val endDateMillis = Instant.now().plusMillis(365L * 24 * 3600 * 1000).toEpochMilli()   // one year ahead
    val initialDateMillis = Instant.now().toEpochMilli()

    val datePickerState = rememberDatePickerState(
        initialSelectedDateMillis = initialDateMillis,
        selectableDates = object : SelectableDates {
            override fun isSelectableDate(utcTimeMillis: Long): Boolean {
                return utcTimeMillis in startDateMillis..endDateMillis
            }
        }
    )
    
    return datePickerState
}

Here, the selectableDates allow to choose available dates.

Method 2: Using MaterialDatePicker from the Legacy Support Library

If you prefer to use the legacy MaterialDatePicker (from the support library), you can integrate it into your Compose application by wrapping it in a ComposeView.

Step 1: Add Dependencies

Add the Material Components for Android dependency in your build.gradle file:


dependencies {
    implementation("com.google.android.material:material:1.11.0")
}
Step 2: Wrap MaterialDatePicker in ComposeView

Create a ComposeView that hosts the MaterialDatePicker. Here’s how:


import android.view.LayoutInflater
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.FragmentActivity
import com.google.android.material.datepicker.MaterialDatePicker
import androidx.compose.ui.tooling.preview.Preview
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

@Composable
fun LegacyDatePicker() {
    AndroidView({ context ->
        ComposeView(context).apply {
            setContent {
                MaterialDatePickerWrapper()
            }
        }
    })
}

@Composable
fun MaterialDatePickerWrapper() {
    val activity = LocalContext.current as? FragmentActivity
    val selectedDate = remember { mutableStateOf("") }

    val materialDatePicker =
        MaterialDatePicker.Builder.datePicker()
            .setTitleText("Select date")
            .setSelection(MaterialDatePicker.todayInUtcMilliseconds())
            .build()

    Column {
        Button(onClick = {
            materialDatePicker.show(activity?.supportFragmentManager!!, "DATE_PICKER")
            materialDatePicker.addOnPositiveButtonClickListener { timeInMillis ->
                val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
                calendar.timeInMillis = timeInMillis
                selectedDate.value = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
            }
        }) {
            Text("Open Date Picker")
        }
        Text(text = "Selected Date: ${selectedDate.value}")
    }
}

@Preview(showBackground = true)
@Composable
fun LegacyDatePickerPreview() {
    LegacyDatePicker()
}

Key points in this implementation:

  • We wrap MaterialDatePicker using AndroidView to make it compatible with Compose.
  • The MaterialDatePicker is shown using supportFragmentManager.
  • A listener captures the selected date and updates a state variable.

Advanced Customization Techniques

Custom Theming

Customizing the appearance of the date picker ensures that it aligns with your application’s design system. Jetpack Compose allows extensive theming to control colors, fonts, and other visual elements.


import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color

@Composable
fun ThemedDatePicker() {
    MaterialTheme {
        // Override the DatePicker's colors
        val themedDatePickerColors = DatePickerDefaults.colors(
            containerColor = Color.LightGray, // Background color
            titleContentColor = Color.Black,   // Header text color
            yearContentColor = Color.DarkGray   // Year picker color
        )
        val state = rememberDatePickerState()
        DatePickerDialog(
            onDismissRequest = {},
            confirmButton = {},
            dismissButton = {}
        ) {
           DatePicker(state = state, colors = themedDatePickerColors)
        }
    }
}

Conclusion

Jetpack Compose provides multiple options for integrating date pickers into your Android applications. Whether using the straightforward Material 3 DatePicker or wrapping the legacy MaterialDatePicker in a ComposeView, you can easily add this crucial UI component. The flexibility of Jetpack Compose also allows for extensive customization, ensuring that your date pickers align perfectly with your application’s design and branding.