'Jetpack compose. Date time picker
Does the jetpack compose have the date time picker view or I should create it by myself? I have tried to google it but I couldn't find ready to use component.
Solution 1:[1]
It seems they are still working on it, in the meantime if you need something very simple you can use the inter-ops of compose with Android Views:
@Composable
fun DatePicker(onDateSelected: (LocalDate) -> Unit, onDismissRequest: () -> Unit) {
val selDate = remember { mutableStateOf(LocalDate.now()) }
//todo - add strings to resource after POC
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties()) {
Column(
modifier = Modifier
.wrapContentSize()
.background(
color = MaterialTheme.colors.surface,
shape = RoundedCornerShape(size = 16.dp)
)
) {
Column(
Modifier
.defaultMinSize(minHeight = 72.dp)
.fillMaxWidth()
.background(
color = MaterialTheme.colors.primary,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
.padding(16.dp)
) {
Text(
text = "Select date".toUpperCase(Locale.ENGLISH),
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onPrimary
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = selDate.value.format(DateTimeFormatter.ofPattern("MMM d, YYYY")),
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.onPrimary
)
Spacer(modifier = Modifier.size(16.dp))
}
CustomCalendarView(onDateSelected = {
selDate.value = it
})
Spacer(modifier = Modifier.size(8.dp))
Row(
modifier = Modifier
.align(Alignment.End)
.padding(bottom = 16.dp, end = 16.dp)
) {
TextButton(
onClick = onDismissRequest
) {
//TODO - hardcode string
Text(
text = "Cancel",
style = MaterialTheme.typography.button,
color = MaterialTheme.colors.onPrimary
)
}
TextButton(
onClick = {
onDateSelected(selDate.value)
onDismissRequest()
}
) {
//TODO - hardcode string
Text(
text = "OK",
style = MaterialTheme.typography.button,
color = MaterialTheme.colors.onPrimary
)
}
}
}
}
}
@Composable
fun CustomCalendarView(onDateSelected: (LocalDate) -> Unit) {
// Adds view to Compose
AndroidView(
modifier = Modifier.wrapContentSize(),
factory = { context ->
CalendarView(ContextThemeWrapper(context, R.style.CalenderViewCustom))
},
update = { view ->
view.minDate = // contraints
view.maxDate = // contraints
view.setOnDateChangeListener { _, year, month, dayOfMonth ->
onDateSelected(
LocalDate
.now()
.withMonth(month + 1)
.withYear(year)
.withDayOfMonth(dayOfMonth)
)
}
}
)
}
<style name="CalenderViewCustom" parent="ThemeOverlay.MaterialComponents.MaterialCalendar">
<item name="colorAccent"><day selection color></item>
<item name="colorOnPrimary"><text color></item>
<item name="colorSurface"><background color></item>
</style>
You will end up with a result like this:
Solution 2:[2]
Using a library
As of writing, It's not implemented yet. But there is an open-source library Compose Material Dialogs
Without a library
You can Also do this without a library, you can put it inside onclick action
private fun showDatePicker() {
val picker = MaterialDatePicker.Builder.datePicker().build()
activity?.let {
picker.show(it.supportFragmentManager, picker.toString())
picker.addOnPositiveButtonClickListener {
}
}
}
Solution 3:[3]
Currently, jetpack compose is in Alpha state and there is no DateTime Picker in androidx.ui.*. But, Later android team will add it or make jetpack compose to interoperate with other components like android.widget.DatePicker Or we can develop beautiful Date Time Picker from stratch using jetpack compose like something flutter does
For now,
class DateTimeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val c = Calendar.getInstance()
val year = c.get(Calendar.YEAR)
val month = c.get(Calendar.MONTH)
val day = c.get(Calendar.DAY_OF_MONTH)
val mHour = c[Calendar.HOUR_OF_DAY]
val mMinute = c[Calendar.MINUTE]
val datePickerDialog = DatePickerDialog(
this, DatePickerDialog.OnDateSetListener
{ datePicker: DatePicker, day: Int, month: Int, year: Int ->
setContent {
Column {
Text("$day, $month, $year")
}
}
}, year, month, day
)
val timePickerDialog = TimePickerDialog(
this,
TimePickerDialog.OnTimeSetListener { view, hourOfDay, minute ->
setContent {
Column {
Text("$hourOfDay:$minute")
}
}
}, mHour, mMinute, false
)
setContent {
Column {
Button(text = "Date",
style = OutlinedButtonStyle(),
onClick = {
datePickerDialog.show()
})
Button(text = "Time",
style = OutlinedButtonStyle(),
onClick = {
timePickerDialog.show()
})
}
}
}
}
Solution 4:[4]
You can show date picker with few line in jetpack compose
AndroidView(
{ CalendarView(it) },
modifier = Modifier.wrapContentWidth(),
update = { views ->
views.setOnDateChangeListener { calendarView, i, i2, i3 ->
}
}
)
Solution 5:[5]
Without 3rd party library, Using Android View solution:
import android.app.TimePickerDialog
@Composable
fun ShowTimePicker(context: Context, initHour: Int, initMinute: Int) {
val time = remember { mutableStateOf("") }
val timePickerDialog = TimePickerDialog(
context,
{_, hour : Int, minute: Int ->
time.value = "$hour:$minute"
}, initHour, initMinute, false
)
Button(onClick = {
timePickerDialog.show()
}) {
Text(text = "Open Time Picker")
}
}
Thanks to Kiran-Bahalaskar
Solution 6:[6]
You could do something like this
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.datepicker.MaterialPickerOnPositiveButtonClickListener
import java.util.*
import kotlin.time.Duration.Companion.hours
import kotlin.time.DurationUnit
@Composable
fun rememberDatePickerDialog(
@StringRes title: Int,
select: Date? = null,
bounds: CalendarConstraints? = null,
onDateSelected: (Date) -> Unit = {},
): MaterialDatePicker<Long> {
val datePicker = remember {
MaterialDatePicker.Builder.datePicker()
.setSelection((select?.time
?: Date().time) + 24.hours.toLong(DurationUnit.MILLISECONDS))
.setCalendarConstraints(bounds)
.setTitleText(title)
.build()
}
DisposableEffect(datePicker) {
val listener = MaterialPickerOnPositiveButtonClickListener<Long> {
if (it != null) onDateSelected(Date(it))
}
datePicker.addOnPositiveButtonClickListener(listener)
onDispose {
datePicker.removeOnPositiveButtonClickListener(listener)
}
}
return datePicker
}
FragmentManager for showing the picker (on button click). Make sure your activity extends from a FragmentActivity before using the following.
@Composable
fun rememberFragmentManager(): FragmentManager {
val context = LocalContext.current
return remember(context) {
(context as FragmentActivity).supportFragmentManager
}
}
Example use case.
val DATE_FORMAT = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
var date by remember {
mutableStateOf(TextFieldValue(DATE_FORMAT.format(Date())))
}
val fragmentManager = rememberFragmentManager()
val datePicker = rememberDatePickerDialog(
select = DATE_FORMAT.parse(date.text),
title = R.string.select_date,
) { date = TextFieldValue(DATE_FORMAT.format(it)) }
TextField(
readOnly = true,
value = date,
onValueChange = { date = it },
trailingIcon = {
IconButton({ datePicker.show(fragmentManager, "Date") }) {
Icon(Icons.Default.DateRange,contentDescription = null)
}
},
)
Solution 7:[7]
Wrap created MaterialDatePicker (thanks to @Mitch) but also try to search for wrapped Fragment – this is important in case of orientation change – you have to re-attach listeners to the instance if any.
@Composable
fun rememberDatePickerDialog(
title: String,
selection: LocalDate? = null,
bounds: CalendarConstraints? = null,
onDateSelected: (LocalDate) -> Unit = {},
): DatePicker {
val fm = rememberFragmentManager()
val tag = "date_picker"
val datePicker = remember {
@Suppress("UNCHECKED_CAST")
fm.findFragmentByTag(tag) as? MaterialDatePicker<Long> ?: createMaterialDatePicker(
selection = selection,
bounds = bounds,
title = title
)
}
DisposableEffect(datePicker) {
val listener = MaterialPickerOnPositiveButtonClickListener<Long> {
if (it != null) onDateSelected(Instant.ofEpochMilli(it).toLocalDateUTC())
}
datePicker.addOnPositiveButtonClickListener(listener)
onDispose {
datePicker.removeOnPositiveButtonClickListener(listener)
}
}
return DatePicker(datePicker, fm, tag)
}
class DatePicker(
private val materialDatePicker: MaterialDatePicker<Long>,
private val fragmentManager: FragmentManager,
private val tag: String,
) {
fun show() = materialDatePicker.show(fragmentManager, tag)
fun hide() = materialDatePicker.dismiss()
}
private fun createMaterialDatePicker(
title: String,
selection: LocalDate?,
bounds: CalendarConstraints?,
) = MaterialDatePicker.Builder.datePicker()
.setSelection(selection?.toEpochMillisUTC())
.setCalendarConstraints(bounds)
.setTitleText(title)
.build()
@Composable
fun rememberFragmentManager(): FragmentManager {
val context = LocalContext.current
return remember(context) {
(context as FragmentActivity).supportFragmentManager
}
}
Solution 8:[8]
I have created a small library for rendering the calendar in Compose: https://github.com/boguszpawlowski/ComposeCalendar. It isn't the date picker per se, but you can configure it to behave like one.
Solution 9:[9]
you can use DatePickerDialog
val calendar = Calendar.getInstance()
val listener = DatePickerDialog.OnDateSetListener { view, year, month, dayOfMonth ->
calendar[Calendar.DAY_OF_MONTH] = dayOfMonth
calendar[Calendar.MONTH] = month
calendar[Calendar.YEAR] = year
selectedDate = calendar.timeInMillis
}
DatePickerDialog(
context,
listener,
calendar[Calendar.YEAR],
calendar[Calendar.MONTH],
calendar[Calendar.DAY_OF_MONTH]
).show()
Solution 10:[10]
As far as I could find, there are still lots of widgets that are missing in Jetpack Compose. The only way to implement them is by using AndroidView and inflating layouts. Here's my code for the time picker: XML file:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TimePicker
android:id="@+id/simpleTimePicker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:datePickerMode="spinner"
android:timePickerMode="spinner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_set_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set Time"
android:textColor="#FFFFFF"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/simpleTimePicker"
tools:ignore="TextContrastCheck" />
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancel"
android:textColor="#FFFFFF"
app:layout_constraintEnd_toStartOf="@+id/btn_set_time"
app:layout_constraintTop_toBottomOf="@+id/simpleTimePicker"
tools:ignore="TextContrastCheck" />
</androidx.constraintlayout.widget.ConstraintLayout>
And also my code in Jetpack compose window in Kotlin:
Dialog(onDismissRequest = { timePickerStart = false }) {
AndroidView(
factory = { context: Context ->
LayoutInflater.from(context).inflate(R.layout.customtimepicker, null, false)
},
modifier = Modifier.fillMaxWidth()
.background(Color.White),
update = { view ->
val setTimebutton = view.findViewById<Button>(R.id.btn_set_time)
val cancelButton = view.findViewById<Button>(R.id.btn_cancel)
val simpleTimePicker = view.findViewById<TimePicker>(R.id.simpleTimePicker)
simpleTimePicker.setIs24HourView(true)
setTimebutton.setOnClickListener {
var hour = simpleTimePicker.hour
var minute = simpleTimePicker.minute
addNotePlusViewModel.startTime.value = convertToInstant(hour, minute)
timePickerStart = false
}
cancelButton.setOnClickListener {
timePickerStart = false
}
}
)
}
Solution 11:[11]
I think the best way to add a date picker in Compose is to leverage what Compose already has.
For instance, for a "select" control, we have the ExposedDropdownMenuBox which expects a Composable possessing a TextField and ExposedDropdownMenuItem.
We can follow suit with that to create a DatePickerSelect. You could even reuse most of it if you want to custom implement the picker's calendar interface in a dropdown menu. However, we can minimize the impementation to just a textfield and make use of the circumstances of the DatePicker dialog in building an attractive control.
So, this Compose-like implementation would go as follows:
Usage.kt
/* I used the following imports */
import android.icu.util.Calendar
import android.app.DatePickerDialog
// ...
@Composable
fun SomeScreenContent(
// ...
) {
// We need an instance of the calendar utility
val calendar = Calendar.getInstance()
// Variable to store the DatePicker selection
var selectedDate by rememberSaveable{ mutableStateOf( "" ) }
// Helper for the dialog
val pickedDate = dateStringParser( calendar, selectedDate )
/* NOTE: Extract the listener
We don't have to separate the listener, but by doing so we can wrap
this implementation into a Composable implementation, where we would
need to pass this listener and the selectedRange variable into said
Composable. I'll show this later on
*/
val onSelectionChanged = DatePickerDialog.OnDateSetListener {
_, year, month, dayOfMonth ->
calendar[Calendar.YEAR] = year
calendar[Calendar.MONTH] = month
calendar[Calendar.DAY_OF_MONTH] = dayOfMonth
// Add 1 to the month so it looks right to the user, since
// it is actually the months indice, not its value
selectedDate = "${calendar[Calendar.MONTH] + 1}/${calendar[Calendar.DAY_OF_MONTH]}/${calendar[Calendar.YEAR]}"
}
// For supporting text input
val onTextChanged: ( String ) -> Unit = { newValue -> selectedDate = newValue }
// The dialog itself, ensured a default - but also that it'll reflect
// a previously chosen date if already set
val dialog = DatePickerDialog(
LocalContext.current,
onSelectionChanged,
pickedDate[0],
pickedDate[1],
pickedDate[2]
)
// Helper for the control ("DatePicker" field) display
val onClicked = {
dialog.show()
}
// Now we can present our date picker
OutlinedTextField(
value = selectedDate,
onValueChange = onTextChanged,
label = { Text( "Select Date" ) },
leadingIcon = {
Icon(
Icons.Default.CalendarViewMonth,
contentDescription = null,
modifier = Modifier
.clickable(
enabled = true,
role = Role.Button,
onClick = onClicked
)
)
},
trailingIcon = {
Icon(
Icons.Default.TouchApp,
contentDescription = null,
modifier = Modifier
.clickable(
enabled = true,
role = Role.Button,
onClick = onClicked
)
)
},
readOnly = false,
modifier = Modifier // Whatever you want here
)
}
/**
* A helper method to handle formatting of visual elements that are leveraged
* internally in logic
*
* @param calendar [Calendar] An ICU compatible calendar instance
* @param date [String] A formatted date string (i.e. 01/01/2001)
*
* @returns [Array] An array of integers that represent the day, month, and
* year of a particular calendar date.
*/
fun dateStringParser( calendar: Calendar, date: String ): Array<Int> {
// A default
var day = calendar[Calendar.DAY_OF_MONTH]
var month = calendar[Calendar.MONTH]
var year = calendar[Calendar.YEAR]
// Leverage picked date to open our calendar where we last picked
// but don't attempt to split on a character that doesnt exist
if( date.isNotEmpty() && date.isNotBlank() && date.contains( "/" ) {
val breakdown = date.split( "/" )
// Then ensure each part has a value (isn't null or blank)
if( breakdown.count() == 3 &&
breakdown[0].isNotEmpty() && breakdown[0].isNotBlank() &&
breakdown[1].isNotEmpty() && breakdown[1].isNotBlank() &&
breakdown[2].isNotEmpty() && breakdown[2].isNotBlank()
) {
// Make sure we handle the month properly. We must add
// a 1 to display as text properly, but must restore it
// for the picker dialog to interpret it properly, since
// the months are indices, and not their actual value.
day = breakdown[1].toInt()
month = breakdown[0].toInt() - 1
year = breakdown[2].toInt()
}
}
return arrayOf( year, month, day )
}
// ...
and that's the basic gist of it.
You'll get the following control:
Upon clicking the control (in the text input space), you'll get:
Upon clicking on either icon:
After selecting a date (or if prepopulated) you'll get:
With this approach you can obviously just type a quick date, but if you choose to then click on the icons to view the picker it'll reflect what you've typed. It gives a well-rounded implementation.
This is also very easy to make reusable. Mind you, I'm sure there's an even more efficient way, but for the sake of a quick example:
SelectFactory.kt
// ...
/**
* A DatePicker implementation
*
* @param label [String] The label for the DatePicker
* @param selection [String] The currently seleted date, or a blank string
* @param onSelectionChanged [DatePickerDIalog.OnDateSetListener]
* @param onTextChanged [Unit]
* @param modifier [Modifier] A modifier for the Control/Field
*/
@Composable
fun DatePickerSelect(
label: String,
selection: String = "",
onSelectionChanged: DatePickerDialog.OnDateSetListener = DatePickerDialog.OnDateSetListener{ datePicker, i, i2, i3 -> },
onTextChanged: ( String ) -> Unit = {},
modifier: Modifier = Modifier.fillMaxWidth()
) {
val calendar = Calendar.getInstance()
val pickedDate = dateStringParser( calendar = calendar, date = selection )
val dialog = DatePickerDialog(
LocalContext.current,
onSelectionChanged,
pickedDate[0],
pickedDate[1],
pickedDate[2]
)
val onClicked = {
dialog.show()
}
OutlinedTextField(
value = selection,
onValueChange = onTextChanged,
label = { Text( label ) },
leadingIcon = {
Icon(
Icons.Default.CalendarViewMonth,
contentDescription = null,
modifier = Modifier
.clickable(
enabled = true,
role = Role.Button,
onClick = onClicked
)
)
},
trailingIcon = {
Icon(
Icons.Default.TouchApp,
//when( isExpanded ){ // To use arrows instead
// true -> Icons.Default.ArrowUpward
// false -> Icons.Default.ArrowDownward
//},
contentDescription = null,
modifier = Modifier
.clickable(
enabled = true,
role = Role.Button,
onClick = onClicked
)
)
},
readOnly = false,
modifier = modifier
)
}
/* NOTE: Regarding commented expanded parameters
*
* Uncomment and use all commented "expanded"-related parameters
* to track and implement arrow indicators, etc. I prefer the
* the touch icon since you wouldn't see the arrows under the
* the dialog anyways.
*
* You'd need to add a parameter to the DatePickerSelect
* Composable for both tracking the Boolean and an
* onExpandedChanged Unit, make use of those variables where you
* instantiate it and within the method, with remembered mutable
* state...and then you can easily track a toggleable icon set.
*/
// ...
fun dateStringParser( calendar: Calendar, date: String ): Array<Int> {
// The same method as reflected in above sample, copy it here...
}
// ...
/* NOTE: Regarding this next method
*
* The following method is just candy, to provide a complete
* solution if you borrow this implementation, that will give
* everything needed to fetch a proper epoch to run queries
* with based on the potentially text-input date string
*
*/
/**
* A helper method to handle converting a formatted date string to
* its respective epoch
*
* @param date [String] A formatted date string (i.e. 01/01/2001)
*
* @returns [Long] The epoch represented by the provided formatted
* date string
*/
fun dateStringConverter( date: String ): Long {
val calendar = Calendar.getInstance()
if( date.isNotEmpty() && date.isNotBlank() && date.contains( "/" ) ) {
val breakdown = date.split( "/" )
if( breakdown.count() == 3 &&
breakdown[0].isNotEmpty() && breakdown[0].isNotBlank() &&
breakdown[1].isNotEmpty() && breakdown[1].isNotBlank() &&
breakdown[2].isNotEmpty() && breakdown[2].isNotBlank()
) {
// Again we need to subtract 1 from the month, so that
// our calendar instance can provide us with a proper
// epoch, considering the months are indices, and not
// actual values
calendar[Calendar.DAY_OF_MONTH] = breakdown[1].toInt()
calendar[Calendar.MONTH] = breakdown[0].toInt() - 1
calendar[Calendar.YEAR] = breakdown[2].toInt()
}
}
return calendar.timeInMillis
}
Then, wherever you want to use it, you just need the minimal setup:
Implementation.kt
// ...
// The prep
val calendar = Calendar.getInstance()
var selected by rememberSaveable{ mutableStateOf( "" ) }
val listener = DatePickerDialog.OnDateSetListener {
_, year, month, dayOfMonth ->
calendar[Calendar.DAY_OF_MONTH] = dayOfMonth
calendar[Calendar.MONTH] = month
calendar[Calendar.YEAR] = year
// Add 1 to the month, because it's indexed, not actual
selected = "${calendar[Calendar.MONTH] + 1}/${calendar[Calendar.DAY_OF_MONTH]}/${calendar[Calendar.YEAR]}"
}
val onTextChanged: ( String ) -> Unit = { newValue -> selected = newValue }
// Then spit it out:
DatePickerSelect(
label = "Start Date",
selection = selectedDate,
onSelectionChanged = listener,
onTextChanged = onTextChanged,
modifier = Modifier // fillMaxWidth( 0.5f ) for first one
) // fillMaxWidth() (is the default) for the second
// ...
Lastly, the time picker dialog functions in the exact same way - so you can combine it with this, or make another control separate for it. You'd just split on colons rather than slashes, etc.
Solution 12:[12]
Used @Chandler's code to implement this:
@Preview()
@Composable
fun MyDatePickerPreview() {
val dp = rememberMyDatePicker(initialDate = Date(), onDateChange = {})
dp.show()
}
@Composable
fun rememberMyDatePicker(
initialDate: Date = Date(),
onDateChange : (newDate: Date) -> Unit
) : DatePickerDialog {
val context = LocalContext.current
val initialLocalDate = localDateFor(initialDate)
val initialYear = initialLocalDate.year
val initialMonth = initialLocalDate.monthValue - 1 // month 5 is june in java calendar
val initialDayOfMonth = initialLocalDate.dayOfMonth
println("Initial year $initialYear month $initialMonth day $initialDayOfMonth")
val datePickerDialog = DatePickerDialog(
context,
{ _, year: Int, month: Int, dayOfMonth: Int ->
val newLocalDate = LocalDate.of(year, month + 1, dayOfMonth)
println("Year $year Month $month Day $dayOfMonth")
onDateChange(dateFor(newLocalDate))
},
initialYear, initialMonth, initialDayOfMonth
)
return remember { datePickerDialog }
}
fun dateFor(ld: LocalDate): Date {
return Date.from(
ld.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant()
)
}
fun localDateFor(date: Date): LocalDate {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow








