Date picker state management and API changes

1. Have the DatePickerState and the DateRangePicker state as interfaces
   with default internal implementations that can be obtained through
   their "remember" functions.
2. Removed the internal StateData and move its functionality to the
   state implementations.
3. Modify internal functions at the DatePicker and DateRangePicker to
   use  values that can were read from a state on an upper level.
4. Make the DatePickerFormatter an interface with a default
   implementationt that can be obtained from the DatePickerDefaults.
5. Make the formatWithSkeleton functions public to allow using them
   without a CalendarModel instance. This allows the DatePickerFormatter
   to be implemented without requiring it.
6. Move the internal CalendarModel from the StateData to seperate the
   model from the state.
7. Added a CalendarModel companion object for retrieving a model with a
   getDefault() function.

Bug: 271912022
Bug: 264589698
Bug: 245821979
Test: Updated the tests to reflect the changes
Relnote: "DatePickerState and the DateRangePickerState are now public
interfaces with default implementations that can be retrieved by the
rememberDatePickerState and rememberDateRangePickerState."

Change-Id: I71c523826e8295772291dd5e3459c50037ac14a7
diff --git a/compose/material3/material3/api/api_lint.ignore b/compose/material3/material3/api/api_lint.ignore
new file mode 100644
index 0000000..21a0335
--- /dev/null
+++ b/compose/material3/material3/api/api_lint.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+AutoBoxing: androidx.compose.material3.DatePickerState#setSelectedDateMillis(Long) parameter #0:
+    Must avoid boxed primitives (`java.lang.Long`)
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index cef6703..9d0249b 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -163,6 +163,14 @@
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
+  public final class CalendarModelKt {
+    method @androidx.compose.material3.ExperimentalMaterial3Api public static String formatWithSkeleton(long utcTimeMillis, String skeleton, optional java.util.Locale locale);
+  }
+
+  public final class CalendarModel_androidKt {
+    method @androidx.compose.material3.ExperimentalMaterial3Api public static String formatWithSkeleton(long utcTimeMillis, String skeleton, java.util.Locale locale);
+  }
+
   @androidx.compose.runtime.Immutable public final class CardColors {
   }
 
@@ -307,9 +315,10 @@
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DatePickerDefaults {
-    method @androidx.compose.runtime.Composable public void DatePickerHeadline(androidx.compose.material3.DatePickerState state, androidx.compose.material3.DatePickerFormatter dateFormatter, optional androidx.compose.ui.Modifier modifier);
-    method @androidx.compose.runtime.Composable public void DatePickerTitle(androidx.compose.material3.DatePickerState state, optional androidx.compose.ui.Modifier modifier);
+    method @androidx.compose.runtime.Composable public void DatePickerHeadline(Long? selectedDateMillis, int displayMode, androidx.compose.material3.DatePickerFormatter dateFormatter, optional androidx.compose.ui.Modifier modifier);
+    method @androidx.compose.runtime.Composable public void DatePickerTitle(int displayMode, optional androidx.compose.ui.Modifier modifier);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.DatePickerColors colors(optional long containerColor, optional long titleContentColor, optional long headlineContentColor, optional long weekdayContentColor, optional long subheadContentColor, optional long yearContentColor, optional long disabledYearContentColor, optional long currentYearContentColor, optional long selectedYearContentColor, optional long disabledSelectedYearContentColor, optional long selectedYearContainerColor, optional long disabledSelectedYearContainerColor, optional long dayContentColor, optional long disabledDayContentColor, optional long selectedDayContentColor, optional long disabledSelectedDayContentColor, optional long selectedDayContainerColor, optional long disabledSelectedDayContainerColor, optional long todayContentColor, optional long todayDateBorderColor, optional long dayInSelectionRangeContentColor, optional long dayInSelectionRangeContainerColor);
+    method public androidx.compose.material3.DatePickerFormatter dateFormatter(optional String yearSelectionSkeleton, optional String selectedDateSkeleton, optional String selectedDateDescriptionSkeleton);
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
     method public float getTonalElevation();
     method public kotlin.ranges.IntRange getYearRange();
@@ -326,8 +335,9 @@
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DatePickerDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional androidx.compose.ui.graphics.Shape shape, optional float tonalElevation, optional androidx.compose.material3.DatePickerColors colors, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class DatePickerFormatter {
-    ctor public DatePickerFormatter(optional String yearSelectionSkeleton, optional String selectedDateSkeleton, optional String selectedDateDescriptionSkeleton);
+  @androidx.compose.material3.ExperimentalMaterial3Api public interface DatePickerFormatter {
+    method public String? formatDate(Long? dateMillis, java.util.Locale locale, optional boolean forContentDescription);
+    method public String? formatMonthYear(Long? monthMillis, java.util.Locale locale);
   }
 
   public final class DatePickerKt {
@@ -335,24 +345,25 @@
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DatePickerState rememberDatePickerState(optional Long? initialSelectedDateMillis, optional Long? initialDisplayedMonthMillis, optional kotlin.ranges.IntRange yearRange, optional int initialDisplayMode, optional androidx.compose.material3.SelectableDates selectableDates);
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DatePickerState {
-    ctor public DatePickerState(Long? initialSelectedDateMillis, Long? initialDisplayedMonthMillis, kotlin.ranges.IntRange yearRange, int initialDisplayMode, androidx.compose.material3.SelectableDates selectableDates);
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface DatePickerState {
     method public int getDisplayMode();
+    method public long getDisplayedMonthMillis();
+    method public androidx.compose.material3.SelectableDates getSelectableDates();
     method public Long? getSelectedDateMillis();
+    method public kotlin.ranges.IntRange getYearRange();
     method public void setDisplayMode(int);
-    method public void setSelection(Long? dateMillis);
-    property public final int displayMode;
-    property public final Long? selectedDateMillis;
-    field public static final androidx.compose.material3.DatePickerState.Companion Companion;
-  }
-
-  public static final class DatePickerState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DatePickerState,?> Saver(androidx.compose.material3.SelectableDates selectableDates);
+    method public void setDisplayedMonthMillis(long);
+    method public void setSelectedDateMillis(Long?);
+    property public abstract int displayMode;
+    property public abstract long displayedMonthMillis;
+    property public abstract androidx.compose.material3.SelectableDates selectableDates;
+    property public abstract Long? selectedDateMillis;
+    property public abstract kotlin.ranges.IntRange yearRange;
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DateRangePickerDefaults {
-    method @androidx.compose.runtime.Composable public void DateRangePickerHeadline(androidx.compose.material3.DateRangePickerState state, androidx.compose.material3.DatePickerFormatter dateFormatter, optional androidx.compose.ui.Modifier modifier);
-    method @androidx.compose.runtime.Composable public void DateRangePickerTitle(androidx.compose.material3.DateRangePickerState state, optional androidx.compose.ui.Modifier modifier);
+    method @androidx.compose.runtime.Composable public void DateRangePickerHeadline(Long? selectedStartDateMillis, Long? selectedEndDateMillis, int displayMode, androidx.compose.material3.DatePickerFormatter dateFormatter, optional androidx.compose.ui.Modifier modifier);
+    method @androidx.compose.runtime.Composable public void DateRangePickerTitle(int displayMode, optional androidx.compose.ui.Modifier modifier);
     field public static final androidx.compose.material3.DateRangePickerDefaults INSTANCE;
   }
 
@@ -361,21 +372,22 @@
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.DateRangePickerState rememberDateRangePickerState(optional Long? initialSelectedStartDateMillis, optional Long? initialSelectedEndDateMillis, optional Long? initialDisplayedMonthMillis, optional kotlin.ranges.IntRange yearRange, optional int initialDisplayMode, optional androidx.compose.material3.SelectableDates selectableDates);
   }
 
-  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class DateRangePickerState {
-    ctor public DateRangePickerState(Long? initialSelectedStartDateMillis, Long? initialSelectedEndDateMillis, Long? initialDisplayedMonthMillis, kotlin.ranges.IntRange yearRange, int initialDisplayMode, androidx.compose.material3.SelectableDates selectableDates);
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface DateRangePickerState {
     method public int getDisplayMode();
+    method public long getDisplayedMonthMillis();
+    method public androidx.compose.material3.SelectableDates getSelectableDates();
     method public Long? getSelectedEndDateMillis();
     method public Long? getSelectedStartDateMillis();
+    method public kotlin.ranges.IntRange getYearRange();
     method public void setDisplayMode(int);
+    method public void setDisplayedMonthMillis(long);
     method public void setSelection(Long? startDateMillis, Long? endDateMillis);
-    property public final int displayMode;
-    property public final Long? selectedEndDateMillis;
-    property public final Long? selectedStartDateMillis;
-    field public static final androidx.compose.material3.DateRangePickerState.Companion Companion;
-  }
-
-  public static final class DateRangePickerState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.DateRangePickerState,?> Saver(androidx.compose.material3.SelectableDates selectableDates);
+    property public abstract int displayMode;
+    property public abstract long displayedMonthMillis;
+    property public abstract androidx.compose.material3.SelectableDates selectableDates;
+    property public abstract Long? selectedEndDateMillis;
+    property public abstract Long? selectedStartDateMillis;
+    property public abstract kotlin.ranges.IntRange yearRange;
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api public enum DismissDirection {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt
index fdfb9d4..7e7fe30 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateInputTest.kt
@@ -155,8 +155,8 @@
                 expectValue(
                     SemanticsProperties.Error,
                     errorMessage.format(
-                        state.stateData.yearRange.first,
-                        state.stateData.yearRange.last
+                        state.yearRange.first,
+                        state.yearRange.last
                     )
                 )
             )
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
index 181d6f5..0cb837b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
@@ -312,8 +312,8 @@
         }
         with(datePickerState) {
             assertThat(selectedDateMillis).isEqualTo(1649721600000L)
-            assertThat(stateData.displayedMonth).isEqualTo(
-                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            assertThat(displayedMonthMillis).isEqualTo(
+                CalendarModel.Default.getMonth(year = 2022, month = 4).startUtcTimeMillis
             )
         }
     }
@@ -330,8 +330,8 @@
             // Assert that the actual selectedDateMillis was rounded down to the start of day
             // timestamp
             assertThat(selectedDateMillis).isEqualTo(1649721600000L)
-            assertThat(stateData.displayedMonth).isEqualTo(
-                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            assertThat(displayedMonthMillis).isEqualTo(
+                CalendarModel.Default.getMonth(year = 2022, month = 4).startUtcTimeMillis
             )
         }
     }
@@ -350,8 +350,9 @@
         with(datePickerState) {
             assertThat(selectedDateMillis).isEqualTo(1649721600000L)
             // Assert that the displayed month is the current month as of today.
-            assertThat(stateData.displayedMonth).isEqualTo(
-                stateData.calendarModel.getMonth(stateData.calendarModel.today.utcTimeMillis)
+            val calendarModel = CalendarModel.Default
+            assertThat(displayedMonthMillis).isEqualTo(
+                calendarModel.getMonth(calendarModel.today.utcTimeMillis).startUtcTimeMillis
             )
         }
     }
@@ -370,8 +371,9 @@
         with(datePickerState) {
             assertThat(selectedDateMillis).isNull()
             // Assert that the displayed month is the current month as of today.
-            assertThat(stateData.displayedMonth).isEqualTo(
-                stateData.calendarModel.getMonth(stateData.calendarModel.today.utcTimeMillis)
+            val calendarModel = CalendarModel.Default
+            assertThat(displayedMonthMillis).isEqualTo(
+                calendarModel.getMonth(calendarModel.today.utcTimeMillis).startUtcTimeMillis
             )
         }
     }
@@ -389,11 +391,11 @@
         rule.onNodeWithText("Apr 12, 2022").assertExists()
         with(datePickerState) {
             assertThat(selectedDateMillis).isEqualTo(1649721600000L)
-            assertThat(stateData.displayedMonth).isEqualTo(
-                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            assertThat(displayedMonthMillis).isEqualTo(
+                CalendarModel.Default.getMonth(year = 2022, month = 4).startUtcTimeMillis
             )
             // Reset the selection
-            datePickerState.setSelection(null)
+            datePickerState.selectedDateMillis = null
             assertThat(selectedDateMillis).isNull()
             rule.onNodeWithText("Apr 12, 2022").assertDoesNotExist()
             rule.onNodeWithText(defaultHeadline).assertExists()
@@ -408,13 +410,13 @@
             datePickerState = rememberDatePickerState()
         }
 
+        val calendarModel = CalendarModel.Default
         with(datePickerState!!) {
-            val date =
-                stateData.calendarModel.getCanonicalDate(1649721600000L) // 04/12/2022
-            val displayedMonth = stateData.calendarModel.getMonth(date)
+            val date = calendarModel.getCanonicalDate(1649721600000L) // 04/12/2022
+            val displayedMonth = calendarModel.getMonth(date)
             rule.runOnIdle {
-                stateData.selectedStartDate.value = date
-                stateData.displayedMonth = displayedMonth
+                selectedDateMillis = date.utcTimeMillis
+                displayedMonthMillis = displayedMonth.startUtcTimeMillis
             }
 
             datePickerState = null
@@ -422,8 +424,8 @@
             restorationTester.emulateSavedInstanceStateRestore()
 
             rule.runOnIdle {
-                assertThat(stateData.selectedStartDate.value).isEqualTo(date)
-                assertThat(stateData.displayedMonth).isEqualTo(displayedMonth)
+                assertThat(selectedDateMillis).isEqualTo(date.utcTimeMillis)
+                assertThat(displayedMonthMillis).isEqualTo(displayedMonth.startUtcTimeMillis)
                 assertThat(datePickerState!!.selectedDateMillis).isEqualTo(1649721600000L)
             }
         }
@@ -437,12 +439,10 @@
         }
 
         // Setting the selection to a year that is out of range.
-        datePickerState.setSelection(
-            dayInUtcMilliseconds(
-                year = 1999,
-                month = 5,
-                dayOfMonth = 11
-            )
+        datePickerState.selectedDateMillis = dayInUtcMilliseconds(
+            year = 1999,
+            month = 5,
+            dayOfMonth = 11
         )
     }
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt
index 25c6b40..17c0028 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerScreenshotTest.kt
@@ -183,7 +183,7 @@
     }
 
     // Returns the given date's day as milliseconds from epoch. The returned value is for the day's
-// start on midnight.
+    // start on midnight.
     private fun dayInUtcMilliseconds(year: Int, month: Int, dayOfMonth: Int): Long =
         LocalDate.of(year, month, dayOfMonth)
             .atTime(LocalTime.MIDNIGHT)
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
index 086476e..0e4d7a1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DateRangePickerTest.kt
@@ -57,8 +57,8 @@
         with(dateRangePickerState) {
             assertThat(selectedStartDateMillis).isEqualTo(1649721600000L)
             assertThat(selectedEndDateMillis).isEqualTo(1649721600000L + MillisecondsIn24Hours)
-            assertThat(stateData.displayedMonth).isEqualTo(
-                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            assertThat(displayedMonthMillis).isEqualTo(
+                CalendarModel.Default.getMonth(year = 2022, month = 4).startUtcTimeMillis
             )
         }
     }
@@ -80,8 +80,8 @@
             // timestamp
             assertThat(selectedStartDateMillis).isEqualTo(1649721600000L)
             assertThat(selectedEndDateMillis).isEqualTo(1649721600000L + MillisecondsIn24Hours)
-            assertThat(stateData.displayedMonth).isEqualTo(
-                stateData.calendarModel.getMonth(year = 2022, month = 4)
+            assertThat(displayedMonthMillis).isEqualTo(
+                CalendarModel.Default.getMonth(year = 2022, month = 4).startUtcTimeMillis
             )
         }
     }
@@ -414,19 +414,18 @@
         restorationTester.setContent {
             dateRangePickerState = rememberDateRangePickerState()
         }
-
+        val calendarModel = CalendarModel.Default
         with(dateRangePickerState!!) {
             // 04/12/2022
             val startDate =
-                stateData.calendarModel.getCanonicalDate(1649721600000L)
+                calendarModel.getCanonicalDate(1649721600000L)
             // 04/13/2022
             val endDate =
-                stateData.calendarModel.getCanonicalDate(1649721600000L + MillisecondsIn24Hours)
-            val displayedMonth = stateData.calendarModel.getMonth(startDate)
+                calendarModel.getCanonicalDate(1649721600000L + MillisecondsIn24Hours)
+            val displayedMonth = calendarModel.getMonth(startDate)
             rule.runOnIdle {
-                stateData.selectedStartDate.value = startDate
-                stateData.selectedEndDate.value = endDate
-                stateData.displayedMonth = displayedMonth
+                setSelection(startDate.utcTimeMillis, endDate.utcTimeMillis)
+                displayedMonthMillis = displayedMonth.startUtcTimeMillis
             }
 
             dateRangePickerState = null
@@ -434,9 +433,9 @@
             restorationTester.emulateSavedInstanceStateRestore()
 
             rule.runOnIdle {
-                assertThat(stateData.selectedStartDate.value).isEqualTo(startDate)
-                assertThat(stateData.selectedEndDate.value).isEqualTo(endDate)
-                assertThat(stateData.displayedMonth).isEqualTo(displayedMonth)
+                assertThat(selectedStartDateMillis).isEqualTo(startDate.utcTimeMillis)
+                assertThat(selectedEndDateMillis).isEqualTo(endDate.utcTimeMillis)
+                assertThat(displayedMonthMillis).isEqualTo(displayedMonth.startUtcTimeMillis)
                 assertThat(dateRangePickerState!!.selectedStartDateMillis)
                     .isEqualTo(1649721600000L)
                 assertThat(dateRangePickerState!!.selectedEndDateMillis)
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
index 1c3aae9..e4fe3ed 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/CalendarModel.android.kt
@@ -50,7 +50,7 @@
  * @param locale the [Locale] to use when formatting the given timestamp
  */
 @ExperimentalMaterial3Api
-internal actual fun formatWithSkeleton(
+actual fun formatWithSkeleton(
     utcTimeMillis: Long,
     skeleton: String,
     locale: Locale
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
index 9291197..9991e2d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
@@ -41,7 +41,7 @@
  * @param locale the [Locale] to use when formatting the given timestamp
  */
 @ExperimentalMaterial3Api
-internal expect fun formatWithSkeleton(
+expect fun formatWithSkeleton(
     utcTimeMillis: Long,
     skeleton: String,
     locale: Locale = Locale.getDefault()
@@ -205,6 +205,14 @@
      * @return a [CalendarDate], or a `null` in case the parsing failed
      */
     fun parse(date: String, pattern: String): CalendarDate?
+
+    companion object {
+
+        /**
+         * Returns a default [CalendarModel] for this environment.
+         */
+        val Default by lazy { CalendarModel() }
+    }
 }
 
 /**
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt
index 40dad31..8a98947 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateInput.kt
@@ -47,20 +47,25 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 internal fun DateInputContent(
-    stateData: StateData,
-    dateFormatter: DatePickerFormatter
+    selectedDateMillis: Long?,
+    onDateSelectionChange: (dateInMillis: Long?) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
+    dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates
 ) {
     // Obtain the DateInputFormat for the default Locale.
     val defaultLocale = defaultLocale()
     val dateInputFormat = remember(defaultLocale) {
-        stateData.calendarModel.getDateInputFormat(defaultLocale)
+        calendarModel.getDateInputFormat(defaultLocale)
     }
     val errorDatePattern = getString(Strings.DateInputInvalidForPattern)
     val errorDateOutOfYearRange = getString(Strings.DateInputInvalidYearRange)
     val errorInvalidNotAllowed = getString(Strings.DateInputInvalidNotAllowed)
     val dateInputValidator = remember(dateInputFormat, dateFormatter) {
         DateInputValidator(
-            stateData = stateData,
+            yearRange = yearRange,
+            selectableDates = selectableDates,
             dateInputFormat = dateInputFormat,
             dateFormatter = dateFormatter,
             errorDatePattern = errorDatePattern,
@@ -75,17 +80,20 @@
         modifier = Modifier
             .fillMaxWidth()
             .padding(InputTextFieldPadding),
+        calendarModel = calendarModel,
         label = {
             Text(
                 labelText,
                 modifier = Modifier.semantics { contentDescription = "$labelText, $pattern" })
         },
         placeholder = { Text(pattern, modifier = Modifier.clearAndSetSemantics { }) },
-        stateData = stateData,
-        initialDate = stateData.selectedStartDate.value,
-        onDateChanged = { date -> stateData.selectedStartDate.value = date },
+        initialDateMillis = selectedDateMillis,
+        onDateSelectionChange = onDateSelectionChange,
         inputIdentifier = InputIdentifier.SingleDateInput,
-        dateInputValidator = dateInputValidator,
+        dateInputValidator = dateInputValidator.apply {
+            // Only need to apply the start date, as this is for a single date input.
+            currentStartDateMillis = selectedDateMillis
+        },
         dateInputFormat = dateInputFormat,
         locale = defaultLocale
     )
@@ -95,11 +103,11 @@
 @Composable
 internal fun DateInputTextField(
     modifier: Modifier,
+    initialDateMillis: Long?,
+    onDateSelectionChange: (Long?) -> Unit,
+    calendarModel: CalendarModel,
     label: @Composable (() -> Unit)?,
     placeholder: @Composable (() -> Unit)?,
-    stateData: StateData,
-    initialDate: CalendarDate?,
-    onDateChanged: (CalendarDate?) -> Unit,
     inputIdentifier: InputIdentifier,
     dateInputValidator: DateInputValidator,
     dateInputFormat: DateInputFormat,
@@ -109,15 +117,13 @@
     var text by rememberSaveable(stateSaver = TextFieldValue.Saver) {
         mutableStateOf(
             TextFieldValue(
-                text = with(stateData) {
-                    initialDate?.let {
-                        calendarModel.formatWithPattern(
-                            it.utcTimeMillis,
-                            dateInputFormat.patternWithoutDelimiters,
-                            locale
-                        )
-                    } ?: ""
-                },
+                text = initialDateMillis?.let {
+                    calendarModel.formatWithPattern(
+                        it,
+                        dateInputFormat.patternWithoutDelimiters,
+                        locale
+                    )
+                } ?: "",
                 TextRange(0, 0)
             )
         )
@@ -135,20 +141,26 @@
                     trimmedText.length < dateInputFormat.patternWithoutDelimiters.length
                 ) {
                     errorText.value = ""
-                    onDateChanged(null)
+                    onDateSelectionChange(null)
                 } else {
-                    val parsedDate = stateData.calendarModel.parse(
+                    val parsedDate = calendarModel.parse(
                         trimmedText,
                         dateInputFormat.patternWithoutDelimiters
                     )
                     errorText.value = dateInputValidator.validate(
-                        calendarDate = parsedDate,
+                        dateToValidate = parsedDate,
                         inputIdentifier = inputIdentifier,
                         locale = locale
                     )
                     // Set the parsed date only if the error validation returned an empty string.
                     // Otherwise, set it to null, as the validation failed.
-                    onDateChanged(if (errorText.value.isEmpty()) parsedDate else null)
+                    onDateSelectionChange(
+                        if (errorText.value.isEmpty()) {
+                            parsedDate?.utcTimeMillis
+                        } else {
+                            null
+                        }
+                    )
                 }
             }
         },
@@ -182,7 +194,8 @@
 /**
  * A date input validator class.
  *
- * @param stateData the [StateData] that holds the selected dates info
+ * @param yearRange an [IntRange] that holds the year range that the date picker is be limited to
+ * @param selectableDates a [SelectableDates] that is consulted to check if a date is allowed
  * @param dateInputFormat a [DateInputFormat] that holds date patterns information
  * @param dateFormatter a [DatePickerFormatter]
  * @param errorDatePattern a string for displaying an error message when an input does not match the
@@ -197,52 +210,58 @@
  * @param errorInvalidRangeInput a string for displaying an error message when in a range input mode
  * and one of the input dates is out of order (i.e. the user inputs a start date that is after the
  * end date, or an end date that is before the start date)
+ * @param currentStartDateMillis the currently selected start date in milliseconds. Only checked
+ * against when the [InputIdentifier] is [InputIdentifier.EndDateInput].
+ * @param currentEndDateMillis the currently selected end date in milliseconds. Only checked
+ * against when the [InputIdentifier] is [InputIdentifier.StartDateInput].
  */
 @OptIn(ExperimentalMaterial3Api::class)
 @Stable
 internal class DateInputValidator(
-    private val stateData: StateData,
+    private val yearRange: IntRange,
+    private val selectableDates: SelectableDates,
     private val dateInputFormat: DateInputFormat,
     private val dateFormatter: DatePickerFormatter,
     private val errorDatePattern: String,
     private val errorDateOutOfYearRange: String,
     private val errorInvalidNotAllowed: String,
-    private val errorInvalidRangeInput: String
+    private val errorInvalidRangeInput: String,
+    internal var currentStartDateMillis: Long? = null,
+    internal var currentEndDateMillis: Long? = null,
 ) {
 
     /**
      * Validates a [CalendarDate] input and returns an error string in case an issue with the given
      * date is detected, or an empty string in case there are no issues.
      *
-     * @param calendarDate a [CalendarDate] input
+     * @param dateToValidate a [CalendarDate] input to validate
      * @param inputIdentifier an [InputIdentifier] that provides information about the input field
      * that is supposed to hold the date.
      * @param locale the current [Locale]
      */
     fun validate(
-        calendarDate: CalendarDate?,
+        dateToValidate: CalendarDate?,
         inputIdentifier: InputIdentifier,
         locale: Locale
     ): String {
-        if (calendarDate == null) {
+        if (dateToValidate == null) {
             return errorDatePattern.format(dateInputFormat.patternWithDelimiters.uppercase())
         }
         // Check that the date is within the valid range of years.
-        if (!stateData.yearRange.contains(calendarDate.year)) {
+        if (!yearRange.contains(dateToValidate.year)) {
             return errorDateOutOfYearRange.format(
-                stateData.yearRange.first.toLocalString(),
-                stateData.yearRange.last.toLocalString()
+                yearRange.first.toLocalString(),
+                yearRange.last.toLocalString()
             )
         }
         // Check that the provided SelectableDates allows this date to be selected.
-        with(stateData.selectableDates) {
-            if (!isSelectableYear(calendarDate.year) ||
-                !isSelectableDate(calendarDate.utcTimeMillis)
+        with(selectableDates) {
+            if (!isSelectableYear(dateToValidate.year) ||
+                !isSelectableDate(dateToValidate.utcTimeMillis)
             ) {
                 return errorInvalidNotAllowed.format(
                     dateFormatter.formatDate(
-                        date = calendarDate,
-                        calendarModel = stateData.calendarModel,
+                        dateMillis = dateToValidate.utcTimeMillis,
                         locale = locale
                     )
                 )
@@ -251,11 +270,9 @@
 
         // Additional validation when the InputIdentifier is for start of end dates in a range input
         if ((inputIdentifier == InputIdentifier.StartDateInput &&
-                calendarDate.utcTimeMillis >= (stateData.selectedEndDate.value?.utcTimeMillis
-                ?: Long.MAX_VALUE)) ||
+                dateToValidate.utcTimeMillis >= (currentEndDateMillis ?: Long.MAX_VALUE)) ||
             (inputIdentifier == InputIdentifier.EndDateInput &&
-                calendarDate.utcTimeMillis <= (stateData.selectedStartDate.value?.utcTimeMillis
-                ?: Long.MIN_VALUE))
+                dateToValidate.utcTimeMillis < (currentStartDateMillis ?: Long.MIN_VALUE))
         ) {
             // The input start date is after the end date, or the end date is before the start date.
             return errorInvalidRangeInput
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
index 71c2852..29fcdd6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
@@ -149,23 +149,25 @@
 fun DatePicker(
     state: DatePickerState,
     modifier: Modifier = Modifier,
-    dateFormatter: DatePickerFormatter = remember { DatePickerFormatter() },
+    dateFormatter: DatePickerFormatter = remember { DatePickerDefaults.dateFormatter() },
     title: (@Composable () -> Unit)? = {
         DatePickerDefaults.DatePickerTitle(
-            state,
+            displayMode = state.displayMode,
             modifier = Modifier.padding(DatePickerTitlePadding)
         )
     },
     headline: (@Composable () -> Unit)? = {
         DatePickerDefaults.DatePickerHeadline(
-            state,
-            dateFormatter,
+            selectedDateMillis = state.selectedDateMillis,
+            displayMode = state.displayMode,
+            dateFormatter = dateFormatter,
             modifier = Modifier.padding(DatePickerHeadlinePadding)
         )
     },
     showModeToggle: Boolean = true,
     colors: DatePickerColors = DatePickerDefaults.colors()
 ) {
+    val calendarModel = remember { CalendarModel() }
     DateEntryContainer(
         modifier = modifier,
         title = title,
@@ -175,11 +177,7 @@
                 DisplayModeToggleButton(
                     modifier = Modifier.padding(DatePickerModeTogglePadding),
                     displayMode = state.displayMode,
-                    onDisplayModeChange = { displayMode ->
-                        state.stateData.switchDisplayMode(
-                            displayMode
-                        )
-                    },
+                    onDisplayModeChange = { displayMode -> state.displayMode = displayMode },
                 )
             }
         } else {
@@ -192,14 +190,141 @@
         colors = colors
     ) {
         SwitchableDateEntryContent(
-            state = state,
+            selectedDateMillis = state.selectedDateMillis,
+            displayedMonthMillis = state.displayedMonthMillis,
+            displayMode = state.displayMode,
+            onDateSelectionChange = { dateInMillis -> state.selectedDateMillis = dateInMillis },
+            onDisplayedMonthChange = { monthInMillis ->
+                state.displayedMonthMillis = monthInMillis
+            },
+            calendarModel = calendarModel,
+            yearRange = state.yearRange,
             dateFormatter = dateFormatter,
+            selectableDates = state.selectableDates,
             colors = colors
         )
     }
 }
 
 /**
+ * A state object that can be hoisted to observe the date picker state. See
+ * [rememberDatePickerState].
+ */
+@ExperimentalMaterial3Api
+@Stable
+interface DatePickerState {
+
+    /**
+     * A timestamp that represents the selected date _start_ of the day in _UTC_ milliseconds
+     * from the epoch.
+     *
+     * @throws IllegalArgumentException in case the value is set with a timestamp that does not fall
+     * within the [yearRange].
+     */
+    @get:Suppress("AutoBoxing")
+    var selectedDateMillis: Long?
+
+    /**
+     * A timestamp that represents the currently displayed month _start_ date in _UTC_ milliseconds
+     * from the epoch.
+     *
+     * @throws IllegalArgumentException in case the value is set with a timestamp that does not fall
+     * within the [yearRange].
+     */
+    var displayedMonthMillis: Long
+
+    /**
+     * A [DisplayMode] that represents the current UI mode (i.e. picker or input).
+     */
+    var displayMode: DisplayMode
+
+    /**
+     * An [IntRange] that holds the year range that the date picker will be limited to.
+     */
+    val yearRange: IntRange
+
+    /**
+     * A [SelectableDates] that is consulted to check if a date is allowed.
+     *
+     * In case a date is not allowed to be selected, it will appear disabled in the UI.
+     */
+    val selectableDates: SelectableDates
+}
+
+/**
+ * An interface that controls the selectable dates and years in the date pickers UI.
+ */
+@ExperimentalMaterial3Api
+interface SelectableDates {
+
+    /**
+     * Returns true if the date item representing the [utcTimeMillis] should be enabled for
+     * selection in the UI.
+     */
+    fun isSelectableDate(utcTimeMillis: Long) = true
+
+    /**
+     * Returns true if a given [year] should be enabled for selection in the UI. When a year is
+     * defined as non selectable, all the dates in that year will also be non selectable.
+     */
+    fun isSelectableYear(year: Int) = true
+}
+
+/**
+ * A date formatter interface used by [DatePicker].
+ */
+@ExperimentalMaterial3Api
+interface DatePickerFormatter {
+
+    /**
+     * Format a given [monthMillis] to a string representation of the month and the year (i.e.
+     * January 2023).
+     *
+     * @param monthMillis timestamp in _UTC_ milliseconds from the epoch that represents the month
+     * @param locale a [Locale] to use when formatting the month and year
+     */
+    fun formatMonthYear(@Suppress("AutoBoxing") monthMillis: Long?, locale: Locale): String?
+
+    /**
+     * Format a given [dateMillis] to a string representation of the date (i.e. Mar 27, 2021).
+     *
+     * @param dateMillis timestamp in _UTC_ milliseconds from the epoch that represents the date
+     * @param locale a [Locale] to use when formatting the date
+     * @param forContentDescription indicates that the requested formatting is for content
+     * description. In these cases, the output may include a more descriptive wording that will be
+     * passed to a screen readers.
+     */
+    fun formatDate(
+        @Suppress("AutoBoxing") dateMillis: Long?,
+        locale: Locale,
+        forContentDescription: Boolean = false
+    ): String?
+}
+
+/**
+ * Represents the different modes that a date picker can be at.
+ */
+@Immutable
+@JvmInline
+@ExperimentalMaterial3Api
+value class DisplayMode internal constructor(internal val value: Int) {
+
+    companion object {
+        /** Date picker mode */
+        val Picker = DisplayMode(0)
+
+        /** Date text input mode */
+        val Input = DisplayMode(1)
+    }
+
+    override fun toString() = when (this) {
+        Picker -> "Picker"
+        Input -> "Input"
+        else -> "Unknown"
+    }
+}
+
+/**
  * Creates a [DatePickerState] for a [DatePicker] that is remembered across compositions.
  *
  * @param initialSelectedDateMillis timestamp in _UTC_ milliseconds from the epoch that represents
@@ -223,9 +348,9 @@
     initialDisplayMode: DisplayMode = DisplayMode.Picker,
     selectableDates: SelectableDates = object : SelectableDates {}
 ): DatePickerState = rememberSaveable(
-    saver = DatePickerState.Saver(selectableDates)
+    saver = DatePickerStateImpl.Saver(selectableDates)
 ) {
-    DatePickerState(
+    DatePickerStateImpl(
         initialSelectedDateMillis = initialSelectedDateMillis,
         initialDisplayedMonthMillis = initialDisplayedMonthMillis,
         yearRange = yearRange,
@@ -235,120 +360,7 @@
 }
 
 /**
- * A state object that can be hoisted to observe the date picker state. See
- * [rememberDatePickerState].
- *
- * The state's [selectedDateMillis] will provide a timestamp that represents the _start_ of the day.
- */
-@ExperimentalMaterial3Api
-@Stable
-class DatePickerState private constructor(internal val stateData: StateData) {
-
-    /**
-     * Constructs a DatePickerState.
-     *
-     * @param initialSelectedDateMillis timestamp in _UTC_ milliseconds from the epoch that
-     * represents an initial selection of a date. Provide a `null` to indicate no selection. Note
-     * that the state's
-     * [selectedDateMillis] will provide a timestamp that represents the _start_ of the day, which
-     * may be different than the provided initialSelectedDateMillis.
-     * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that
-     * represents an initial selection of a month to be displayed to the user. In case `null` is
-     * provided, the displayed month would be the current one.
-     * @param yearRange an [IntRange] that holds the year range that the date picker will be limited
-     * to
-     * @param initialDisplayMode an initial [DisplayMode] that this state will hold
-     * @param selectableDates a [SelectableDates] that is consulted to check if a date is allowed.
-     * In case a date is not allowed to be selected, it will appear disabled in the UI.
-     * @see rememberDatePickerState
-     * @throws [IllegalArgumentException] if the initial selected date or displayed month represent
-     * a year that is out of the year range.
-     */
-    constructor(
-        @Suppress("AutoBoxing") initialSelectedDateMillis: Long?,
-        @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
-        yearRange: IntRange,
-        initialDisplayMode: DisplayMode,
-        selectableDates: SelectableDates
-    ) : this(
-        stateData = StateData(
-            initialSelectedStartDateMillis = initialSelectedDateMillis,
-            initialSelectedEndDateMillis = null,
-            initialDisplayedMonthMillis = initialDisplayedMonthMillis,
-            yearRange = yearRange,
-            initialDisplayMode = initialDisplayMode,
-            selectableDates = selectableDates
-        )
-    )
-
-    /**
-     * A timestamp that represents the _start_ of the day of the selected date in _UTC_ milliseconds
-     * from the epoch.
-     *
-     * In case no date was selected or provided, the state will hold a `null` value.
-     *
-     * @see [setSelection]
-     */
-    val selectedDateMillis: Long?
-        @Suppress("AutoBoxing") get() = stateData.selectedStartDate.value?.utcTimeMillis
-
-    /**
-     * Sets the selected date.
-     *
-     * @param dateMillis timestamp in _UTC_ milliseconds from the epoch that represents the date
-     * selection, or `null` to indicate no selection.
-     *
-     * @throws IllegalArgumentException in case the given timestamps do not fall within the year
-     * range this state was created with.
-     */
-    fun setSelection(@Suppress("AutoBoxing") dateMillis: Long?) {
-        stateData.setSelection(startDateMillis = dateMillis, endDateMillis = null)
-    }
-
-    /**
-     * A mutable state of [DisplayMode] that represents the current display mode of the UI
-     * (i.e. picker or input).
-     */
-    var displayMode by stateData.displayMode
-
-    companion object {
-        /**
-         * The default [Saver] implementation for [DatePickerState].
-         *
-         * @param selectableDates a [SelectableDates] instance that is consulted to check if a date
-         * is allowed
-         */
-        fun Saver(selectableDates: SelectableDates): Saver<DatePickerState, *> = Saver(
-            save = { with(StateData.Saver(selectableDates)) { save(it.stateData) } },
-            restore = { value ->
-                DatePickerState(
-                    stateData = with(StateData.Saver(selectableDates)) { restore(value)!! })
-            }
-        )
-    }
-}
-
-/**
- * An interface that controls the selectable dates and years in the date pickers UI.
- */
-@ExperimentalMaterial3Api
-interface SelectableDates {
-
-    /**
-     * Returns true if the date item representing the [utcTimeMillis] should be enabled for
-     * selection in the UI.
-     */
-    fun isSelectableDate(utcTimeMillis: Long) = true
-
-    /**
-     * Returns true if a given [year] should be enabled for selection in the UI. When a year is
-     * defined as non selectable, all the dates in that year will also be non selectable.
-     */
-    fun isSelectableYear(year: Int) = true
-}
-
-/**
- * Contains default values used by the date pickers.
+ * Contains default values used by the [DatePicker].
  */
 @ExperimentalMaterial3Api
 @Stable
@@ -452,14 +464,41 @@
         )
 
     /**
+     * Returns a [DatePickerFormatter].
+     *
+     * The date formatter will apply the best possible localized form of the given skeleton and Locale.
+     * A skeleton is similar to, and uses the same format characters as, a Unicode
+     * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> pattern.
+     *
+     * One difference is that order is irrelevant. For example, "MMMMd" will return "MMMM d" in the
+     * `en_US` locale, but "d. MMMM" in the `de_CH` locale.
+     *
+     * @param yearSelectionSkeleton a date format skeleton used to format the date picker's year
+     * selection menu button (e.g. "March 2021").
+     * @param selectedDateSkeleton a date format skeleton used to format a selected date (e.g.
+     * "Mar 27, 2021")
+     * @param selectedDateDescriptionSkeleton a date format skeleton used to format a selected date to
+     * be used as content description for screen readers (e.g. "Saturday, March 27, 2021")
+     */
+    fun dateFormatter(
+        yearSelectionSkeleton: String = YearMonthSkeleton,
+        selectedDateSkeleton: String = YearAbbrMonthDaySkeleton,
+        selectedDateDescriptionSkeleton: String = YearMonthWeekdayDaySkeleton
+    ): DatePickerFormatter = DatePickerFormatterImpl(
+        yearSelectionSkeleton = yearSelectionSkeleton,
+        selectedDateSkeleton = selectedDateSkeleton,
+        selectedDateDescriptionSkeleton = selectedDateDescriptionSkeleton
+    )
+
+    /**
      * A default date picker title composable.
      *
-     * @param state a [DatePickerState] that will help determine the title's content
+     * @param displayMode the current [DisplayMode]
      * @param modifier a [Modifier] to be applied for the title
      */
     @Composable
-    fun DatePickerTitle(state: DatePickerState, modifier: Modifier = Modifier) {
-        when (state.displayMode) {
+    fun DatePickerTitle(displayMode: DisplayMode, modifier: Modifier = Modifier) {
+        when (displayMode) {
             DisplayMode.Picker -> Text(
                 text = getString(string = Strings.DatePickerTitle),
                 modifier = modifier
@@ -476,55 +515,54 @@
      * A default date picker headline composable that displays a default headline text when there is
      * no date selection, and an actual date string when there is.
      *
-     * @param state a [DatePickerState] that will help determine the title's headline
+     * @param selectedDateMillis a timestamp that represents the selected date _start_ of the day in
+     * _UTC_ milliseconds from the epoch
+     * @param displayMode the current [DisplayMode]
      * @param dateFormatter a [DatePickerFormatter]
      * @param modifier a [Modifier] to be applied for the headline
      */
     @Composable
     fun DatePickerHeadline(
-        state: DatePickerState,
+        @Suppress("AutoBoxing") selectedDateMillis: Long?,
+        displayMode: DisplayMode,
         dateFormatter: DatePickerFormatter,
         modifier: Modifier = Modifier
     ) {
-        with(state.stateData) {
-            val defaultLocale = defaultLocale()
-            val formattedDate = dateFormatter.formatDate(
-                date = selectedStartDate.value,
-                calendarModel = calendarModel,
-                locale = defaultLocale
-            )
-            val verboseDateDescription = dateFormatter.formatDate(
-                date = selectedStartDate.value,
-                calendarModel = calendarModel,
-                locale = defaultLocale,
-                forContentDescription = true
-            ) ?: when (displayMode.value) {
-                DisplayMode.Picker -> getString(Strings.DatePickerNoSelectionDescription)
-                DisplayMode.Input -> getString(Strings.DateInputNoInputDescription)
-                else -> ""
-            }
-
-            val headlineText = formattedDate ?: when (displayMode.value) {
-                DisplayMode.Picker -> getString(Strings.DatePickerHeadline)
-                DisplayMode.Input -> getString(Strings.DateInputHeadline)
-                else -> ""
-            }
-
-            val headlineDescription = when (displayMode.value) {
-                DisplayMode.Picker -> getString(Strings.DatePickerHeadlineDescription)
-                DisplayMode.Input -> getString(Strings.DateInputHeadlineDescription)
-                else -> ""
-            }.format(verboseDateDescription)
-
-            Text(
-                text = headlineText,
-                modifier = modifier.semantics {
-                    liveRegion = LiveRegionMode.Polite
-                    contentDescription = headlineDescription
-                },
-                maxLines = 1
-            )
+        val defaultLocale = defaultLocale()
+        val formattedDate = dateFormatter.formatDate(
+            dateMillis = selectedDateMillis,
+            locale = defaultLocale
+        )
+        val verboseDateDescription = dateFormatter.formatDate(
+            dateMillis = selectedDateMillis,
+            locale = defaultLocale,
+            forContentDescription = true
+        ) ?: when (displayMode) {
+            DisplayMode.Picker -> getString(Strings.DatePickerNoSelectionDescription)
+            DisplayMode.Input -> getString(Strings.DateInputNoInputDescription)
+            else -> ""
         }
+
+        val headlineText = formattedDate ?: when (displayMode) {
+            DisplayMode.Picker -> getString(Strings.DatePickerHeadline)
+            DisplayMode.Input -> getString(Strings.DateInputHeadline)
+            else -> ""
+        }
+
+        val headlineDescription = when (displayMode) {
+            DisplayMode.Picker -> getString(Strings.DatePickerHeadlineDescription)
+            DisplayMode.Input -> getString(Strings.DateInputHeadlineDescription)
+            else -> ""
+        }.format(verboseDateDescription)
+
+        Text(
+            text = headlineText,
+            modifier = modifier.semantics {
+                liveRegion = LiveRegionMode.Polite
+                contentDescription = headlineDescription
+            },
+            maxLines = 1
+        )
     }
 
     /**
@@ -780,6 +818,167 @@
 }
 
 /**
+ * An abstract for the date pickers states.
+ *
+ * This base class common state properties and provides a base implementation that is extended by
+ * the different state classes.
+ *
+ * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of a month to be displayed to the user. In case `null` is
+ * provided, the displayed month would be the current one.
+ * @param yearRange an [IntRange] that holds the year range that the date picker will be limited
+ * to
+ * @param selectableDates a [SelectableDates] that is consulted to check if a date is allowed.
+ * In case a date is not allowed to be selected, it will appear disabled in the UI.
+ * @see rememberDatePickerState
+ * @throws [IllegalArgumentException] if the initial selected date or displayed month represent
+ * a year that is out of the year range.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Stable
+internal abstract class BaseDatePickerStateImpl(
+    @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
+    val yearRange: IntRange,
+    val selectableDates: SelectableDates
+) {
+
+    val calendarModel = CalendarModel.Default
+
+    private var _displayedMonth =
+        mutableStateOf(if (initialDisplayedMonthMillis != null) {
+            val month = calendarModel.getMonth(initialDisplayedMonthMillis)
+            require(yearRange.contains(month.year)) {
+                "The initial display month's year (${month.year}) is out of the years range of " +
+                    "$yearRange."
+            }
+            month
+        } else {
+            // Set the displayed month to the current one.
+            calendarModel.getMonth(calendarModel.today)
+        })
+
+    var displayedMonthMillis: Long
+        get() = _displayedMonth.value.startUtcTimeMillis
+        set(monthMillis) {
+            val month = calendarModel.getMonth(monthMillis)
+            require(yearRange.contains(month.year)) {
+                "The display month's year (${month.year}) is out of the years range of $yearRange."
+            }
+            _displayedMonth.value = month
+        }
+}
+
+/**
+ * A default implementation of the [DatePickerState]. See [rememberDatePickerState].
+ *
+ * @param initialSelectedDateMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of a date. Provide a `null` to indicate no selection. Note
+ * that the state's
+ * [selectedDateMillis] will provide a timestamp that represents the _start_ of the day, which
+ * may be different than the provided initialSelectedDateMillis.
+ * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of a month to be displayed to the user. In case `null` is
+ * provided, the displayed month would be the current one.
+ * @param yearRange an [IntRange] that holds the year range that the date picker will be limited
+ * to
+ * @param initialDisplayMode an initial [DisplayMode] that this state will hold
+ * @param selectableDates a [SelectableDates] that is consulted to check if a date is allowed.
+ * In case a date is not allowed to be selected, it will appear disabled in the UI.
+ * @see rememberDatePickerState
+ * @throws [IllegalArgumentException] if the initial selected date or displayed month represent
+ * a year that is out of the year range.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Stable
+private class DatePickerStateImpl(
+    @Suppress("AutoBoxing") initialSelectedDateMillis: Long?,
+    @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
+    yearRange: IntRange,
+    initialDisplayMode: DisplayMode,
+    selectableDates: SelectableDates
+) : BaseDatePickerStateImpl(
+    initialDisplayedMonthMillis,
+    yearRange,
+    selectableDates
+), DatePickerState {
+
+    /**
+     * A mutable state of [CalendarDate] that represents a selected date.
+     */
+    private var _selectedDate =
+        mutableStateOf(if (initialSelectedDateMillis != null) {
+            val date = calendarModel.getCanonicalDate(initialSelectedDateMillis)
+            require(yearRange.contains(date.year)) {
+                "The provided initial date's year (${date.year}) is out of the years range " +
+                    "of $yearRange."
+            }
+            date
+        } else {
+            null
+        })
+
+    override var selectedDateMillis: Long?
+        @Suppress("AutoBoxing") get() = _selectedDate.value?.utcTimeMillis
+        set(@Suppress("AutoBoxing") dateMillis) {
+            if (dateMillis != null) {
+                val date = calendarModel.getCanonicalDate(dateMillis)
+                // Validate that the give date is within the valid years range.
+                require(yearRange.contains(date.year)) {
+                    "The provided date's year (${date.year}) is out of the years range of " +
+                        "$yearRange."
+                }
+                _selectedDate.value = date
+            } else {
+                _selectedDate.value = null
+            }
+        }
+
+    /**
+     * A mutable state of [DisplayMode] that represents the current display mode of the UI
+     * (i.e. picker or input).
+     */
+    private var _displayMode = mutableStateOf(initialDisplayMode)
+
+    override var displayMode
+        get() = _displayMode.value
+        set(displayMode) {
+            selectedDateMillis?.let {
+                displayedMonthMillis = calendarModel.getMonth(it).startUtcTimeMillis
+            }
+            _displayMode.value = displayMode
+        }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [DatePickerStateImpl].
+         *
+         * @param selectableDates a [SelectableDates] instance that is consulted to check if a date
+         * is allowed
+         */
+        fun Saver(selectableDates: SelectableDates): Saver<DatePickerStateImpl, Any> = listSaver(
+            save = {
+                listOf(
+                    it.selectedDateMillis,
+                    it.displayedMonthMillis,
+                    it.yearRange.first,
+                    it.yearRange.last,
+                    it.displayMode.value
+                )
+            },
+            restore = { value ->
+                DatePickerStateImpl(
+                    initialSelectedDateMillis = value[0] as Long?,
+                    initialDisplayedMonthMillis = value[1] as Long?,
+                    yearRange = IntRange(value[2] as Int, value[3] as Int),
+                    initialDisplayMode = DisplayMode(value[4] as Int),
+                    selectableDates = selectableDates
+                )
+            }
+        )
+    }
+}
+
+/**
  * A date formatter used by [DatePicker].
  *
  * The date formatter will apply the best possible localized form of the given skeleton and Locale.
@@ -796,33 +995,30 @@
  * @param selectedDateDescriptionSkeleton a date format skeleton used to format a selected date to
  * be used as content description for screen readers (e.g. "Saturday, March 27, 2021")
  */
-@ExperimentalMaterial3Api
+@OptIn(ExperimentalMaterial3Api::class)
 @Immutable
-class DatePickerFormatter constructor(
-    internal val yearSelectionSkeleton: String = DatePickerDefaults.YearMonthSkeleton,
-    internal val selectedDateSkeleton: String = DatePickerDefaults.YearAbbrMonthDaySkeleton,
-    internal val selectedDateDescriptionSkeleton: String =
-        DatePickerDefaults.YearMonthWeekdayDaySkeleton
-) {
+private class DatePickerFormatterImpl constructor(
+    val yearSelectionSkeleton: String,
+    val selectedDateSkeleton: String,
+    val selectedDateDescriptionSkeleton: String
+) : DatePickerFormatter {
 
-    internal fun formatMonthYear(
-        month: CalendarMonth?,
-        calendarModel: CalendarModel,
+    override fun formatMonthYear(
+        monthMillis: Long?,
         locale: Locale
     ): String? {
-        if (month == null) return null
-        return calendarModel.formatWithSkeleton(month, yearSelectionSkeleton, locale)
+        if (monthMillis == null) return null
+        return formatWithSkeleton(monthMillis, yearSelectionSkeleton, locale)
     }
 
-    internal fun formatDate(
-        date: CalendarDate?,
-        calendarModel: CalendarModel,
+    override fun formatDate(
+        dateMillis: Long?,
         locale: Locale,
-        forContentDescription: Boolean = false
+        forContentDescription: Boolean
     ): String? {
-        if (date == null) return null
-        return calendarModel.formatWithSkeleton(
-            date, if (forContentDescription) {
+        if (dateMillis == null) return null
+        return formatWithSkeleton(
+            dateMillis, if (forContentDescription) {
                 selectedDateDescriptionSkeleton
             } else {
                 selectedDateSkeleton
@@ -832,7 +1028,7 @@
     }
 
     override fun equals(other: Any?): Boolean {
-        if (other !is DatePickerFormatter) return false
+        if (other !is DatePickerFormatterImpl) return false
 
         if (yearSelectionSkeleton != other.yearSelectionSkeleton) return false
         if (selectedDateSkeleton != other.selectedDateSkeleton) return false
@@ -850,226 +1046,6 @@
 }
 
 /**
- * Represents the different modes that a date picker can be at.
- */
-@Immutable
-@JvmInline
-@ExperimentalMaterial3Api
-value class DisplayMode internal constructor(internal val value: Int) {
-
-    companion object {
-        /** Date picker mode */
-        val Picker = DisplayMode(0)
-
-        /** Date text input mode */
-        val Input = DisplayMode(1)
-    }
-
-    override fun toString() = when (this) {
-        Picker -> "Picker"
-        Input -> "Input"
-        else -> "Unknown"
-    }
-}
-
-/**
- * Holds the state's data for the date picker.
- *
- * Note that the internal representation is capable of holding a start and end date. However, the
- * the [DatePickerState] and the [DateRangePickerState] that use this class will only expose
- * publicly the relevant functionality for their purpose.
- *
- * @param initialSelectedStartDateMillis timestamp in _UTC_ milliseconds from the epoch that
- * represents an initial selection of a start date. Provide a `null` to indicate no selection.
- * @param initialSelectedEndDateMillis timestamp in _UTC_ milliseconds from the epoch that
- * represents an initial selection of an end date. Provide a `null` to indicate no selection. This
- * value will be ignored in case it's smaller or equals to the initial start value.
- * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that represents
- * an initial selection of a month to be displayed to the user. In case `null` is provided, the
- * displayed month would be the current one.
- * @param yearRange an [IntRange] that holds the year range that the date picker will be limited to
- * @param initialDisplayMode an initial [DisplayMode] that this state will hold
- * @see rememberDatePickerState
- */
-@OptIn(ExperimentalMaterial3Api::class)
-@Stable
-internal class StateData constructor(
-    initialSelectedStartDateMillis: Long?,
-    initialSelectedEndDateMillis: Long?,
-    initialDisplayedMonthMillis: Long?,
-    val yearRange: IntRange,
-    initialDisplayMode: DisplayMode,
-    val selectableDates: SelectableDates,
-) {
-
-    val calendarModel: CalendarModel = CalendarModel()
-
-    /**
-     * A mutable state of [CalendarDate] that represents the start date for a selection.
-     */
-    var selectedStartDate = mutableStateOf<CalendarDate?>(null)
-
-    /**
-     * A mutable state of [CalendarDate] that represents the end date for a selection.
-     *
-     * Single date selection states that use this [StateData] should always have this as `null`.
-     */
-    var selectedEndDate = mutableStateOf<CalendarDate?>(null)
-
-    /**
-     * Initialize the state with the provided initial selections.
-     */
-    init {
-        setSelection(
-            startDateMillis = initialSelectedStartDateMillis,
-            endDateMillis = initialSelectedEndDateMillis
-        )
-    }
-
-    /**
-     * A mutable state for the month that is displayed to the user. In case an initial month was not
-     * provided, the current month will be the one to be displayed.
-     */
-    var displayedMonth by mutableStateOf(
-        if (initialDisplayedMonthMillis != null) {
-            val month = calendarModel.getMonth(initialDisplayedMonthMillis)
-            require(yearRange.contains(month.year)) {
-                "The initial display month's year (${month.year}) is out of the years range of " +
-                    "$yearRange."
-            }
-            month
-        } else {
-            currentMonth
-        }
-    )
-
-    /**
-     * The current [CalendarMonth] that represents the present's day month.
-     */
-    val currentMonth: CalendarMonth
-        get() = calendarModel.getMonth(calendarModel.today)
-
-    /**
-     * A mutable state of [DisplayMode] that represents the current display mode of the UI
-     * (i.e. picker or input).
-     */
-    var displayMode = mutableStateOf(initialDisplayMode)
-
-    /**
-     * The displayed month index within the total months at the defined years range.
-     *
-     * @see [displayedMonth]
-     * @see [yearRange]
-     */
-    val displayedMonthIndex: Int
-        get() = displayedMonth.indexIn(yearRange)
-
-    /**
-     * The total month count for the defined years range.
-     *
-     * @see [yearRange]
-     */
-    val totalMonthsInRange: Int
-        get() = (yearRange.last - yearRange.first + 1) * 12
-
-    /**
-     * Sets a start and end selection dates.
-     *
-     * The function expects the dates to be within the state's year-range, and for the start date to
-     * appear before, or be equal, the end date. Also, if an end date is provided (e.g. not `null`),
-     * a start date is also expected to be provided. In any other case, an
-     * [IllegalArgumentException] is thrown.
-     *
-     * @param startDateMillis timestamp in _UTC_ milliseconds from the epoch that represents the
-     * start date selection. Provide a `null` to indicate no selection.
-     * @param endDateMillis timestamp in _UTC_ milliseconds from the epoch that represents the
-     * end date selection. Provide a `null` to indicate no selection.
-     * @throws IllegalArgumentException in case the given timestamps do not comply with the expected
-     * values specified above.
-     */
-    fun setSelection(startDateMillis: Long?, endDateMillis: Long?) {
-        val startDate = if (startDateMillis != null) {
-            calendarModel.getCanonicalDate(startDateMillis)
-        } else {
-            null
-        }
-        val endDate = if (endDateMillis != null) {
-            calendarModel.getCanonicalDate(endDateMillis)
-        } else {
-            null
-        }
-        // Validate that both dates are within the valid years range.
-        startDate?.let {
-            require(yearRange.contains(it.year)) {
-                "The provided start date year (${it.year}) is out of the years range of $yearRange."
-            }
-        }
-        endDate?.let {
-            require(yearRange.contains(it.year)) {
-                "The provided end date year (${it.year}) is out of the years range of $yearRange."
-            }
-        }
-        // Validate that an end date cannot be set without a start date.
-        if (endDate != null) {
-            requireNotNull(startDate) {
-                "An end date was provided without a start date."
-            }
-            // Validate that the end date appears on or after the start date.
-            require(startDate.utcTimeMillis <= endDate.utcTimeMillis) {
-                "The provided end date appears before the start date."
-            }
-        }
-        selectedStartDate.value = startDate
-        selectedEndDate.value = endDate
-    }
-
-    fun switchDisplayMode(displayMode: DisplayMode) {
-        // Update the displayed month, if needed, and change the mode to a  date-picker.
-        selectedStartDate.value?.let {
-            displayedMonth = calendarModel.getMonth(it)
-        }
-        // When toggling back from an input mode, it's possible that the user input an invalid
-        // start date and a valid end date. If this is the case, and the start date is null, ensure
-        // that the end date is also null.
-        if (selectedStartDate.value == null && selectedEndDate.value != null) {
-            selectedEndDate.value = null
-        }
-        this.displayMode.value = displayMode
-    }
-
-    companion object {
-        /**
-         * A [Saver] implementation for [StateData].
-         *
-         * @param selectableDates a [SelectableDates] instance that is consulted to check if a date
-         * is allowed
-         */
-        fun Saver(selectableDates: SelectableDates): Saver<StateData, Any> = listSaver(
-            save = {
-                listOf(
-                    it.selectedStartDate.value?.utcTimeMillis,
-                    it.selectedEndDate.value?.utcTimeMillis,
-                    it.displayedMonth.startUtcTimeMillis,
-                    it.yearRange.first,
-                    it.yearRange.last,
-                    it.displayMode.value.value
-                )
-            },
-            restore = { value ->
-                StateData(
-                    initialSelectedStartDateMillis = value[0] as Long?,
-                    initialSelectedEndDateMillis = value[1] as Long?,
-                    initialDisplayedMonthMillis = value[2] as Long?,
-                    yearRange = IntRange(value[3] as Int, value[4] as Int),
-                    initialDisplayMode = DisplayMode(value[5] as Int),
-                    selectableDates = selectableDates
-                )
-            }
-        )
-    }
-}
-
-/**
  * A base container for the date picker and the date input. This container composes the top common
  * area of the UI, and accepts [content] for the actual calendar picker or text field input.
  */
@@ -1158,26 +1134,43 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SwitchableDateEntryContent(
-    state: DatePickerState,
+    selectedDateMillis: Long?,
+    displayedMonthMillis: Long,
+    displayMode: DisplayMode,
+    onDateSelectionChange: (dateInMillis: Long?) -> Unit,
+    onDisplayedMonthChange: (monthInMillis: Long) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
     dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates,
     colors: DatePickerColors
 ) {
     // TODO(b/266480386): Apply the motion spec for this once we have it. Consider replacing this
     //  with AnimatedContent when it's out of experimental.
     Crossfade(
-        targetState = state.displayMode,
+        targetState = displayMode,
         animationSpec = spring(),
         modifier = Modifier.semantics { isContainer = true }) { mode ->
         when (mode) {
             DisplayMode.Picker -> DatePickerContent(
-                stateData = state.stateData,
+                selectedDateMillis = selectedDateMillis,
+                displayedMonthMillis = displayedMonthMillis,
+                onDateSelectionChange = onDateSelectionChange,
+                onDisplayedMonthChange = onDisplayedMonthChange,
+                calendarModel = calendarModel,
+                yearRange = yearRange,
                 dateFormatter = dateFormatter,
+                selectableDates = selectableDates,
                 colors = colors
             )
 
             DisplayMode.Input -> DateInputContent(
-                stateData = state.stateData,
-                dateFormatter = dateFormatter
+                selectedDateMillis = selectedDateMillis,
+                onDateSelectionChange = onDateSelectionChange,
+                calendarModel = calendarModel,
+                yearRange = yearRange,
+                dateFormatter = dateFormatter,
+                selectableDates = selectableDates
             )
         }
     }
@@ -1186,19 +1179,20 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun DatePickerContent(
-    stateData: StateData,
+    selectedDateMillis: Long?,
+    displayedMonthMillis: Long,
+    onDateSelectionChange: (dateInMillis: Long) -> Unit,
+    onDisplayedMonthChange: (monthInMillis: Long) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
     dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates,
     colors: DatePickerColors
 ) {
+    val displayedMonth = calendarModel.getMonth(displayedMonthMillis)
     val monthsListState =
-        rememberLazyListState(initialFirstVisibleItemIndex = stateData.displayedMonthIndex)
+        rememberLazyListState(initialFirstVisibleItemIndex = displayedMonth.indexIn(yearRange))
     val coroutineScope = rememberCoroutineScope()
-
-    val onDateSelected = { dateInMillis: Long ->
-        stateData.selectedStartDate.value =
-            stateData.calendarModel.getCanonicalDate(dateInMillis)
-    }
-
     var yearPickerVisible by rememberSaveable { mutableStateOf(false) }
     val defaultLocale = defaultLocale()
     Column {
@@ -1208,8 +1202,7 @@
             previousAvailable = monthsListState.canScrollBackward,
             yearPickerVisible = yearPickerVisible,
             yearPickerText = dateFormatter.formatMonthYear(
-                month = stateData.displayedMonth,
-                calendarModel = stateData.calendarModel,
+                monthMillis = displayedMonthMillis,
                 locale = defaultLocale
             ) ?: "-",
             onNextClicked = {
@@ -1231,12 +1224,16 @@
 
         Box {
             Column(modifier = Modifier.padding(horizontal = DatePickerHorizontalPadding)) {
-                WeekDays(colors, stateData.calendarModel)
+                WeekDays(colors, calendarModel)
                 HorizontalMonthsList(
-                    onDateSelected = onDateSelected,
-                    stateData = stateData,
                     lazyListState = monthsListState,
+                    selectedDateMillis = selectedDateMillis,
+                    onDateSelectionChange = onDateSelectionChange,
+                    onDisplayedMonthChange = onDisplayedMonthChange,
+                    calendarModel = calendarModel,
+                    yearRange = yearRange,
                     dateFormatter = dateFormatter,
+                    selectableDates = selectableDates,
                     colors = colors
                 )
             }
@@ -1262,6 +1259,7 @@
                                     DividerDefaults.Thickness
                             )
                             .padding(horizontal = DatePickerHorizontalPadding),
+                        displayedMonthMillis = displayedMonthMillis,
                         onYearSelected = { year ->
                             // Switch back to the monthly calendar and scroll to the selected year.
                             yearPickerVisible = !yearPickerVisible
@@ -1269,15 +1267,15 @@
                                 // Scroll to the selected year (maintaining the month of year).
                                 // A LaunchEffect at the MonthsList will take care of rest and will
                                 // update the state's displayedMonth to the month we scrolled to.
-                                with(stateData) {
-                                    monthsListState.scrollToItem(
-                                        (year - yearRange.first) * 12 + displayedMonth.month - 1
-                                    )
-                                }
+                                monthsListState.scrollToItem(
+                                    (year - yearRange.first) * 12 + displayedMonth.month - 1
+                                )
                             }
                         },
-                        colors = colors,
-                        stateData = stateData
+                        selectableDates = selectableDates,
+                        calendarModel = calendarModel,
+                        yearRange = yearRange,
+                        colors = colors
                     )
                     Divider()
                 }
@@ -1333,16 +1331,20 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun HorizontalMonthsList(
-    onDateSelected: (dateInMillis: Long) -> Unit,
-    stateData: StateData,
     lazyListState: LazyListState,
+    selectedDateMillis: Long?,
+    onDateSelectionChange: (dateInMillis: Long) -> Unit,
+    onDisplayedMonthChange: (monthInMillis: Long) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
     dateFormatter: DatePickerFormatter,
-    colors: DatePickerColors,
+    selectableDates: SelectableDates,
+    colors: DatePickerColors
 ) {
-    val today = stateData.calendarModel.today
-    val firstMonth = remember(stateData.yearRange) {
-        stateData.calendarModel.getMonth(
-            year = stateData.yearRange.first,
+    val today = calendarModel.today
+    val firstMonth = remember(yearRange) {
+        calendarModel.getMonth(
+            year = yearRange.first,
             month = 1 // January
         )
     }
@@ -1358,22 +1360,23 @@
         //  when promoted to stable
         flingBehavior = DatePickerDefaults.rememberSnapFlingBehavior(lazyListState)
     ) {
-        items(stateData.totalMonthsInRange) {
-            val month =
-                stateData.calendarModel.plusMonths(
-                    from = firstMonth,
-                    addedMonthsCount = it
-                )
+        items(numberOfMonthsInRange(yearRange)) {
+            val month = calendarModel.plusMonths(
+                from = firstMonth,
+                addedMonthsCount = it
+            )
             Box(
                 modifier = Modifier.fillParentMaxWidth()
             ) {
                 Month(
                     month = month,
-                    onDateSelected = onDateSelected,
-                    today = today,
-                    stateData = stateData,
-                    rangeSelectionEnabled = false,
+                    onDateSelectionChange = onDateSelectionChange,
+                    todayMillis = today.utcTimeMillis,
+                    startDateMillis = selectedDateMillis,
+                    endDateMillis = null,
+                    rangeSelectionInfo = null,
                     dateFormatter = dateFormatter,
+                    selectableDates = selectableDates,
                     colors = colors
                 )
             }
@@ -1381,28 +1384,31 @@
     }
 
     LaunchedEffect(lazyListState) {
-        updateDisplayedMonth(lazyListState, stateData)
+        updateDisplayedMonth(
+            lazyListState = lazyListState,
+            onDisplayedMonthChange = onDisplayedMonthChange,
+            calendarModel = calendarModel,
+            yearRange = yearRange
+        )
     }
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
 internal suspend fun updateDisplayedMonth(
     lazyListState: LazyListState,
-    stateData: StateData
+    onDisplayedMonthChange: (monthInMillis: Long) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange
 ) {
     snapshotFlow { lazyListState.firstVisibleItemIndex }.collect {
         val yearOffset = lazyListState.firstVisibleItemIndex / 12
         val month = lazyListState.firstVisibleItemIndex % 12 + 1
-        with(stateData) {
-            if (displayedMonth.month != month ||
-                displayedMonth.year != yearRange.first + yearOffset
-            ) {
-                displayedMonth = calendarModel.getMonth(
-                    year = yearRange.first + yearOffset,
-                    month = month
-                )
-            }
-        }
+        onDisplayedMonthChange(
+            calendarModel.getMonth(
+                year = yearRange.first + yearOffset,
+                month = month
+            ).startUtcTimeMillis
+        )
     }
 }
 
@@ -1463,32 +1469,18 @@
 @Composable
 internal fun Month(
     month: CalendarMonth,
-    onDateSelected: (dateInMillis: Long) -> Unit,
-    today: CalendarDate,
-    stateData: StateData,
-    rangeSelectionEnabled: Boolean,
+    onDateSelectionChange: (dateInMillis: Long) -> Unit,
+    todayMillis: Long,
+    startDateMillis: Long?,
+    endDateMillis: Long?,
+    rangeSelectionInfo: SelectedRangeInfo?,
     dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates,
     colors: DatePickerColors
 ) {
-    val rangeSelectionInfo: State<SelectedRangeInfo?> = remember(rangeSelectionEnabled) {
-        derivedStateOf {
-            if (rangeSelectionEnabled) {
-                SelectedRangeInfo.calculateRangeInfo(
-                    month,
-                    stateData.selectedStartDate.value,
-                    stateData.selectedEndDate.value
-                )
-            } else {
-                null
-            }
-        }
-    }
-
-    val rangeSelectionDrawModifier = if (rangeSelectionEnabled) {
+    val rangeSelectionDrawModifier = if (rangeSelectionInfo != null) {
         Modifier.drawWithContent {
-            rangeSelectionInfo.value?.let {
-                drawRangeBackground(it, colors.dayInSelectionRangeContainerColor)
-            }
+            drawRangeBackground(rangeSelectionInfo, colors.dayInSelectionRangeContainerColor)
             drawContent()
         }
     } else {
@@ -1496,8 +1488,6 @@
     }
 
     val defaultLocale = defaultLocale()
-    val startSelection = stateData.selectedStartDate
-    val endSelection = stateData.selectedEndDate
     ProvideTextStyle(
         MaterialTheme.typography.fromToken(DatePickerModalTokens.DateLabelTextFont)
     ) {
@@ -1530,37 +1520,33 @@
                             val dayNumber = cellIndex - month.daysFromStartOfWeekToFirstOfMonth
                             val dateInMillis = month.startUtcTimeMillis +
                                 (dayNumber * MillisecondsIn24Hours)
-                            val isToday = dateInMillis == today.utcTimeMillis
-                            val startDateSelected =
-                                dateInMillis == startSelection.value?.utcTimeMillis
-                            val endDateSelected = dateInMillis == endSelection.value?.utcTimeMillis
-                            val inRange = remember(rangeSelectionEnabled, dateInMillis) {
+                            val isToday = dateInMillis == todayMillis
+                            val startDateSelected = dateInMillis == startDateMillis
+                            val endDateSelected = dateInMillis == endDateMillis
+                            val inRange = remember(rangeSelectionInfo, dateInMillis) {
                                 derivedStateOf {
-                                    with(stateData) {
-                                        rangeSelectionEnabled &&
-                                            dateInMillis >= (selectedStartDate.value?.utcTimeMillis
-                                            ?: Long.MAX_VALUE) &&
-                                            dateInMillis <= (selectedEndDate.value?.utcTimeMillis
-                                            ?: Long.MIN_VALUE)
-                                    }
+                                    rangeSelectionInfo != null &&
+                                        dateInMillis >= (startDateMillis
+                                        ?: Long.Companion.MAX_VALUE) &&
+                                        dateInMillis <= (endDateMillis ?: Long.MIN_VALUE)
                                 }
                             }
                             val dayContentDescription = dayContentDescription(
-                                rangeSelectionEnabled = rangeSelectionEnabled,
+                                rangeSelectionEnabled = rangeSelectionInfo != null,
                                 isToday = isToday,
                                 isStartDate = startDateSelected,
                                 isEndDate = endDateSelected,
                                 isInRange = inRange.value
                             )
-                            val formattedDateDescription = formatWithSkeleton(
+                            val formattedDateDescription = dateFormatter.formatDate(
                                 dateInMillis,
-                                dateFormatter.selectedDateDescriptionSkeleton,
-                                defaultLocale
-                            )
+                                defaultLocale,
+                                forContentDescription = true
+                            ) ?: ""
                             Day(
                                 modifier = Modifier,
                                 selected = startDateSelected || endDateSelected,
-                                onClick = { onDateSelected(dateInMillis) },
+                                onClick = { onDateSelectionChange(dateInMillis) },
                                 // Only animate on the first selected day. This is important to
                                 // disable when drawing a range marker behind the days on an
                                 // end-date selection.
@@ -1569,7 +1555,7 @@
                                     // Disabled a day in case its year is not selectable, or the
                                     // date itself is specifically not allowed by the state's
                                     // SelectableDates.
-                                    with(stateData.selectableDates) {
+                                    with(selectableDates) {
                                         isSelectableYear(month.year) &&
                                             isSelectableDate(dateInMillis)
                                     }
@@ -1599,6 +1585,12 @@
     }
 }
 
+/**
+ * Returns the number of months within the given year range.
+ */
+internal fun numberOfMonthsInRange(yearRange: IntRange) =
+    (yearRange.last - yearRange.first + 1) * 12
+
 @Composable
 private fun dayContentDescription(
     rangeSelectionEnabled: Boolean,
@@ -1692,21 +1684,24 @@
 @Composable
 private fun YearPicker(
     modifier: Modifier,
+    displayedMonthMillis: Long,
     onYearSelected: (year: Int) -> Unit,
-    colors: DatePickerColors,
-    stateData: StateData
+    selectableDates: SelectableDates,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
+    colors: DatePickerColors
 ) {
     ProvideTextStyle(
         value = MaterialTheme.typography.fromToken(DatePickerModalTokens.SelectionYearLabelTextFont)
     ) {
-        val currentYear = stateData.currentMonth.year
-        val displayedYear = stateData.displayedMonth.year
+        val currentYear = calendarModel.getMonth(calendarModel.today).year
+        val displayedYear = calendarModel.getMonth(displayedMonthMillis).year
         val lazyGridState =
             rememberLazyGridState(
                 // Set the initial index to a few years before the current year to allow quicker
                 // selection of previous years.
                 initialFirstVisibleItemIndex = max(
-                    0, displayedYear - stateData.yearRange.first - YearsInRow
+                    0, displayedYear - yearRange.first - YearsInRow
                 )
             )
         // Match the years container color to any elevated surface color that is composed under it.
@@ -1731,8 +1726,8 @@
             horizontalArrangement = Arrangement.SpaceEvenly,
             verticalArrangement = Arrangement.spacedBy(YearsVerticalPadding)
         ) {
-            items(stateData.yearRange.count()) {
-                val selectedYear = it + stateData.yearRange.first
+            items(yearRange.count()) {
+                val selectedYear = it + yearRange.first
                 val localizedYear = selectedYear.toLocalString()
                 Year(
                     modifier = Modifier
@@ -1760,7 +1755,7 @@
                     selected = selectedYear == displayedYear,
                     currentYear = selectedYear == currentYear,
                     onClick = { onYearSelected(selectedYear) },
-                    enabled = stateData.selectableDates.isSelectableYear(selectedYear),
+                    enabled = selectableDates.isSelectableYear(selectedYear),
                     description = getString(Strings.DatePickerNavigateToYearDescription)
                         .format(localizedYear),
                     colors = colors
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangeInput.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangeInput.kt
index 1f9670f..7bfa365 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangeInput.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangeInput.kt
@@ -30,13 +30,18 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 internal fun DateRangeInputContent(
-    stateData: StateData,
-    dateFormatter: DatePickerFormatter
+    selectedStartDateMillis: Long?,
+    selectedEndDateMillis: Long?,
+    onDatesSelectionChange: (startDateMillis: Long?, endDateMillis: Long?) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
+    dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates
 ) {
     // Obtain the DateInputFormat for the default Locale.
     val defaultLocale = defaultLocale()
     val dateInputFormat = remember(defaultLocale) {
-        stateData.calendarModel.getDateInputFormat(defaultLocale)
+        calendarModel.getDateInputFormat(defaultLocale)
     }
     val errorDatePattern = getString(Strings.DateInputInvalidForPattern)
     val errorDateOutOfYearRange = getString(Strings.DateInputInvalidYearRange)
@@ -44,7 +49,8 @@
     val errorInvalidRange = getString(Strings.DateRangeInputInvalidRangeInput)
     val dateInputValidator = remember(dateInputFormat, dateFormatter) {
         DateInputValidator(
-            stateData = stateData,
+            yearRange = yearRange,
+            selectableDates = selectableDates,
             dateInputFormat = dateInputFormat,
             dateFormatter = dateFormatter,
             errorDatePattern = errorDatePattern,
@@ -53,6 +59,11 @@
             errorInvalidRangeInput = errorInvalidRange
         )
     }
+    // Apply both start and end dates for proper validation.
+    dateInputValidator.apply {
+        currentStartDateMillis = selectedStartDateMillis
+        currentEndDateMillis = selectedEndDateMillis
+    }
     Row(
         modifier = Modifier.padding(paddingValues = InputTextFieldPadding),
         horizontalArrangement = Arrangement.spacedBy(TextFieldSpacing)
@@ -61,6 +72,7 @@
         val startRangeText = getString(string = Strings.DateRangePickerStartHeadline)
         DateInputTextField(
             modifier = Modifier.weight(0.5f),
+            calendarModel = calendarModel,
             label = {
                 Text(startRangeText,
                     modifier = Modifier.semantics {
@@ -68,9 +80,11 @@
                     })
             },
             placeholder = { Text(pattern, modifier = Modifier.clearAndSetSemantics { }) },
-            stateData = stateData,
-            initialDate = stateData.selectedStartDate.value,
-            onDateChanged = { date -> stateData.selectedStartDate.value = date },
+            initialDateMillis = selectedStartDateMillis,
+            onDateSelectionChange = { startDateMillis ->
+                // Delegate to the onDatesSelectionChange and change just the start date.
+                onDatesSelectionChange(startDateMillis, selectedEndDateMillis)
+            },
             inputIdentifier = InputIdentifier.StartDateInput,
             dateInputValidator = dateInputValidator,
             dateInputFormat = dateInputFormat,
@@ -79,6 +93,7 @@
         val endRangeText = getString(string = Strings.DateRangePickerEndHeadline)
         DateInputTextField(
             modifier = Modifier.weight(0.5f),
+            calendarModel = calendarModel,
             label = {
                 Text(endRangeText,
                     modifier = Modifier.semantics {
@@ -86,9 +101,11 @@
                     })
             },
             placeholder = { Text(pattern, modifier = Modifier.clearAndSetSemantics { }) },
-            stateData = stateData,
-            initialDate = stateData.selectedEndDate.value,
-            onDateChanged = { date -> stateData.selectedEndDate.value = date },
+            initialDateMillis = selectedEndDateMillis,
+            onDateSelectionChange = { endDateMillis ->
+                // Delegate to the onDatesSelectionChange and change just the end date.
+                onDatesSelectionChange(selectedStartDateMillis, endDateMillis)
+            },
             inputIdentifier = InputIdentifier.EndDateInput,
             dateInputValidator = dateInputValidator,
             dateInputFormat = dateInputFormat,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt
index 9205c2d..abf3272 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DateRangePicker.kt
@@ -31,12 +31,14 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithContent
@@ -83,16 +85,18 @@
 fun DateRangePicker(
     state: DateRangePickerState,
     modifier: Modifier = Modifier,
-    dateFormatter: DatePickerFormatter = remember { DatePickerFormatter() },
+    dateFormatter: DatePickerFormatter = remember { DatePickerDefaults.dateFormatter() },
     title: (@Composable () -> Unit)? = {
         DateRangePickerDefaults.DateRangePickerTitle(
-            state = state,
+            displayMode = state.displayMode,
             modifier = Modifier.padding(DateRangePickerTitlePadding)
         )
     },
     headline: (@Composable () -> Unit)? = {
         DateRangePickerDefaults.DateRangePickerHeadline(
-            state,
+            selectedStartDateMillis = state.selectedStartDateMillis,
+            selectedEndDateMillis = state.selectedEndDateMillis,
+            displayMode = state.displayMode,
             dateFormatter,
             modifier = Modifier.padding(DateRangePickerHeadlinePadding)
         )
@@ -100,6 +104,7 @@
     showModeToggle: Boolean = true,
     colors: DatePickerColors = DatePickerDefaults.colors()
 ) {
+    val calendarModel = remember { CalendarModel() }
     DateEntryContainer(
         modifier = modifier,
         title = title,
@@ -109,11 +114,7 @@
                 DisplayModeToggleButton(
                     modifier = Modifier.padding(DatePickerModeTogglePadding),
                     displayMode = state.displayMode,
-                    onDisplayModeChange = { displayMode ->
-                        state.stateData.switchDisplayMode(
-                            displayMode
-                        )
-                    }
+                    onDisplayModeChange = { displayMode -> state.displayMode = displayMode },
                 )
             }
         } else {
@@ -127,14 +128,107 @@
         colors = colors
     ) {
         SwitchableDateEntryContent(
-            state = state,
+            selectedStartDateMillis = state.selectedStartDateMillis,
+            selectedEndDateMillis = state.selectedEndDateMillis,
+            displayedMonthMillis = state.displayedMonthMillis,
+            displayMode = state.displayMode,
+            onDatesSelectionChange = { startDateMillis, endDateMillis ->
+                try {
+                    state.setSelection(
+                        startDateMillis = startDateMillis,
+                        endDateMillis = endDateMillis
+                    )
+                } catch (iae: IllegalArgumentException) {
+                    // By default, ignore exceptions that setSelection throws.
+                    // Custom implementation may act differently.
+                }
+            },
+            onDisplayedMonthChange = { monthInMillis ->
+                state.displayedMonthMillis = monthInMillis
+            },
+            calendarModel = calendarModel,
+            yearRange = state.yearRange,
             dateFormatter = dateFormatter,
+            selectableDates = state.selectableDates,
             colors = colors
         )
     }
 }
 
 /**
+ * A state object that can be hoisted to observe the date range picker state. See
+ * [rememberDateRangePickerState].
+ */
+@ExperimentalMaterial3Api
+@Stable
+interface DateRangePickerState {
+
+    /**
+     * A timestamp that represents the selected start date _start_ of the day in _UTC_ milliseconds
+     * from the epoch.
+     *
+     * @see [setSelection] for setting this value along with the [selectedEndDateMillis].
+     */
+    @get:Suppress("AutoBoxing")
+    val selectedStartDateMillis: Long?
+
+    /**
+     * A timestamp that represents the selected end date _start_ of the day in _UTC_ milliseconds
+     * from the epoch.
+     *
+     * @see [setSelection] for setting this value along with the [selectedStartDateMillis].
+     */
+    @get:Suppress("AutoBoxing")
+    val selectedEndDateMillis: Long?
+
+    /**
+     * A timestamp that represents the currently displayed month _start_ date in _UTC_ milliseconds
+     * from the epoch.
+     *
+     * @throws IllegalArgumentException in case the value is set with a timestamp that does not fall
+     * within the [yearRange].
+     */
+    var displayedMonthMillis: Long
+
+    /**
+     * A [DisplayMode] that represents the current UI mode (i.e. picker or input).
+     */
+    var displayMode: DisplayMode
+
+    /**
+     * An [IntRange] that holds the year range that the date picker will be limited to.
+     */
+    val yearRange: IntRange
+
+    /**
+     * A [SelectableDates] that is consulted to check if a date is allowed.
+     *
+     * In case a date is not allowed to be selected, it will appear disabled in the UI.
+     */
+    val selectableDates: SelectableDates
+
+    /**
+     * Sets a start and end selection dates.
+     *
+     * The function expects the dates to be within the state's year-range, and for the start date to
+     * appear before, or be equal, the end date. Also, if an end date is provided (e.g. not `null`),
+     * a start date is also expected to be provided. In any other case, an
+     * [IllegalArgumentException] is thrown.
+     *
+     * @param startDateMillis timestamp in _UTC_ milliseconds from the epoch that represents the
+     * start date selection. Provide a `null` to indicate no selection.
+     * @param endDateMillis timestamp in _UTC_ milliseconds from the epoch that represents the
+     * end date selection. Provide a `null` to indicate no selection.
+     * @throws IllegalArgumentException in case the given timestamps do not comply with the expected
+     * values specified above.
+     */
+    fun setSelection(
+        @Suppress("AutoBoxing") startDateMillis: Long?,
+        @Suppress("AutoBoxing") endDateMillis: Long?
+    )
+}
+
+/**
  * Creates a [DateRangePickerState] for a [DateRangePicker] that is remembered across compositions.
  *
  * @param initialSelectedStartDateMillis timestamp in _UTC_ milliseconds from the epoch that
@@ -146,7 +240,8 @@
  * `initialSelectedStartDateMillis` is provided, the initial displayed month would be the month of
  * the selected date. Otherwise, in case `null` is provided, the displayed month would be the
  * current one.
- * @param yearRange an [IntRange] that holds the year range that the date picker will be limited to
+ * @param yearRange an [IntRange] that holds the year range that the date range picker will be
+ * limited to
  * @param initialDisplayMode an initial [DisplayMode] that this state will hold
  * @param selectableDates a [SelectableDates] that is consulted to check if a date is allowed.
  * In case a date is not allowed to be selected, it will appear disabled in the UI.
@@ -162,9 +257,9 @@
     initialDisplayMode: DisplayMode = DisplayMode.Picker,
     selectableDates: SelectableDates = object : SelectableDates {}
 ): DateRangePickerState = rememberSaveable(
-    saver = DateRangePickerState.Saver(selectableDates)
+    saver = DateRangePickerStateImpl.Saver(selectableDates)
 ) {
-    DateRangePickerState(
+    DateRangePickerStateImpl(
         initialSelectedStartDateMillis = initialSelectedStartDateMillis,
         initialSelectedEndDateMillis = initialSelectedEndDateMillis,
         initialDisplayedMonthMillis = initialDisplayedMonthMillis,
@@ -175,147 +270,21 @@
 }
 
 /**
- * A state object that can be hoisted to observe the date picker state. See
- * [rememberDateRangePickerState].
- *
- * The state's [selectedStartDateMillis] and [selectedEndDateMillis] will provide timestamps for the
- * _beginning_ of the selected days (i.e. midnight in _UTC_ milliseconds from the epoch).
- */
-@ExperimentalMaterial3Api
-@Stable
-class DateRangePickerState private constructor(internal val stateData: StateData) {
-
-    /**
-     * Constructs a DateRangePickerState.
-     *
-     * @param initialSelectedStartDateMillis timestamp in _UTC_ milliseconds from the epoch that
-     * represents an initial selection of a start date. Provide a `null` to indicate no selection.
-     * @param initialSelectedEndDateMillis timestamp in _UTC_ milliseconds from the epoch that
-     * represents an initial selection of an end date. Provide a `null` to indicate no selection.
-     * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that
-     * represents an initial selection of a month to be displayed to the user. By default, in case
-     * an `initialSelectedStartDateMillis` is provided, the initial displayed month would be the
-     * month of the selected date. Otherwise, in case `null` is provided, the displayed month would
-     * be the current one.
-     * @param yearRange an [IntRange] that holds the year range that the date picker will be limited
-     * to
-     * @param initialDisplayMode an initial [DisplayMode] that this state will hold
-     * @param selectableDates a [SelectableDates] that is consulted to check if a date is allowed.
-     * In case a date is not allowed to be selected, it will appear disabled in the UI.
-     * @see rememberDatePickerState
-     * @throws IllegalArgumentException if the initial timestamps do not fall within the year range
-     * this state is created with, or the end date precedes the start date, or when an end date is
-     * provided without a start date (e.g. the start date was null, while the end date was not).
-     */
-    constructor(
-        @Suppress("AutoBoxing") initialSelectedStartDateMillis: Long?,
-        @Suppress("AutoBoxing") initialSelectedEndDateMillis: Long?,
-        @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
-        yearRange: IntRange,
-        initialDisplayMode: DisplayMode,
-        selectableDates: SelectableDates
-    ) : this(
-        stateData = StateData(
-            initialSelectedStartDateMillis = initialSelectedStartDateMillis,
-            initialSelectedEndDateMillis = initialSelectedEndDateMillis,
-            initialDisplayedMonthMillis = initialDisplayedMonthMillis,
-            yearRange = yearRange,
-            initialDisplayMode = initialDisplayMode,
-            selectableDates = selectableDates
-        )
-    )
-
-    /**
-     * A timestamp that represents the selected range start date.
-     *
-     * The timestamp would hold a value for the _start_ of the day in _UTC_ milliseconds from the
-     * epoch.
-     *
-     * In case a start date was not selected or provided, the state will hold a `null` value.
-     *
-     * @see [setSelection]
-     */
-
-    val selectedStartDateMillis: Long?
-        @Suppress("AutoBoxing") get() = stateData.selectedStartDate.value?.utcTimeMillis
-
-    /**
-     * A timestamp that represents the selected range end date.
-     *
-     * The timestamp would hold a value for the _start_ of the day in _UTC_ milliseconds from the
-     * epoch.
-     *
-     * In case an end date was not selected or provided, the state will hold a `null` value.
-     *
-     * @see [setSelection]
-     */
-    val selectedEndDateMillis: Long?
-        @Suppress("AutoBoxing") get() = stateData.selectedEndDate.value?.utcTimeMillis
-
-    /**
-     * Sets the selected date.
-     *
-     * @param startDateMillis timestamp in _UTC_ milliseconds from the epoch that represents the
-     * start date selection, or `null` to indicate no selection.
-     * @param endDateMillis timestamp in _UTC_ milliseconds from the epoch that represents the end
-     * date selection, or `null` to indicate no selection.
-     *
-     * @throws IllegalArgumentException if the given timestamps do not fall within the year range
-     * this state was created with, or the end date precedes the start date, or when an end date is
-     * provided without a start date (e.g. the start date was null, while the end date was not).
-     */
-    fun setSelection(
-        @Suppress("AutoBoxing") startDateMillis: Long?,
-        @Suppress("AutoBoxing") endDateMillis: Long?
-    ) {
-        stateData.setSelection(startDateMillis = startDateMillis, endDateMillis = endDateMillis)
-    }
-
-    /**
-     * A mutable state of [DisplayMode] that represents the current display mode of the UI
-     * (i.e. picker or input).
-     */
-    var displayMode by stateData.displayMode
-
-    companion object {
-        /**
-         * The default [Saver] implementation for [DateRangePickerState].
-         *
-         * @param selectableDates a [SelectableDates] instance that is consulted to check if a date
-         * is allowed
-         */
-        fun Saver(selectableDates: SelectableDates): Saver<DateRangePickerState, *> = Saver(
-            save = { with(StateData.Saver(selectableDates)) { save(it.stateData) } },
-            restore = { value ->
-                DateRangePickerState(stateData = with(StateData.Saver(selectableDates)) {
-                    restore(
-                        value
-                    )!!
-                })
-            }
-        )
-    }
-}
-
-/**
- * Contains default values used by the date range pickers.
+ * Contains default values used by the [DateRangePicker].
  */
 @ExperimentalMaterial3Api
 @Stable
 object DateRangePickerDefaults {
 
     /**
-     * A default date picker title composable.
+     * A default date range picker title composable.
      *
-     * @param state a [DatePickerState] that will help determine the title's content
+     * @param displayMode the current [DisplayMode]
      * @param modifier a [Modifier] to be applied for the title
      */
     @Composable
-    fun DateRangePickerTitle(
-        state: DateRangePickerState,
-        modifier: Modifier = Modifier
-    ) {
-        when (state.displayMode) {
+    fun DateRangePickerTitle(displayMode: DisplayMode, modifier: Modifier = Modifier) {
+        when (displayMode) {
             DisplayMode.Picker -> Text(
                 getString(string = Strings.DateRangePickerTitle),
                 modifier = modifier
@@ -332,20 +301,28 @@
      * A default date picker headline composable lambda that displays a default headline text when
      * there is no date selection, and an actual date string when there is.
      *
-     * @param state a [DateRangePickerState] that will help determine the headline
+     * @param selectedStartDateMillis a timestamp that represents the selected start date _start_
+     * of the day in _UTC_ milliseconds from the epoch
+     * @param selectedEndDateMillis a timestamp that represents the selected end date _start_ of the
+     * day in _UTC_ milliseconds from the epoch
+     * @param displayMode the current [DisplayMode]
      * @param dateFormatter a [DatePickerFormatter]
      * @param modifier a [Modifier] to be applied for the headline
      */
     @Composable
     fun DateRangePickerHeadline(
-        state: DateRangePickerState,
+        @Suppress("AutoBoxing") selectedStartDateMillis: Long?,
+        @Suppress("AutoBoxing") selectedEndDateMillis: Long?,
+        displayMode: DisplayMode,
         dateFormatter: DatePickerFormatter,
         modifier: Modifier = Modifier
     ) {
         val startDateText = getString(Strings.DateRangePickerStartHeadline)
         val endDateText = getString(Strings.DateRangePickerEndHeadline)
         DateRangePickerHeadline(
-            state = state,
+            selectedStartDateMillis = selectedStartDateMillis,
+            selectedEndDateMillis = selectedEndDateMillis,
+            displayMode = displayMode,
             dateFormatter = dateFormatter,
             modifier = modifier,
             startDateText = startDateText,
@@ -360,7 +337,11 @@
      * A date picker headline composable lambda that displays a default headline text when
      * there is no date selection, and an actual date string when there is.
      *
-     * @param state a [DateRangePickerState] that will help determine the headline
+     * @param selectedStartDateMillis a timestamp that represents the selected start date _start_
+     * of the day in _UTC_ milliseconds from the epoch
+     * @param selectedEndDateMillis a timestamp that represents the selected end date _start_ of the
+     * day in _UTC_ milliseconds from the epoch
+     * @param displayMode the current [DisplayMode]
      * @param dateFormatter a [DatePickerFormatter]
      * @param modifier a [Modifier] to be applied for the headline
      * @param startDateText a string that, by default, be used as the text content for the
@@ -378,7 +359,9 @@
      */
     @Composable
     private fun DateRangePickerHeadline(
-        state: DateRangePickerState,
+        selectedStartDateMillis: Long?,
+        selectedEndDateMillis: Long?,
+        displayMode: DisplayMode,
         dateFormatter: DatePickerFormatter,
         modifier: Modifier,
         startDateText: String,
@@ -387,96 +370,281 @@
         endDatePlaceholder: @Composable () -> Unit,
         datesDelimiter: @Composable () -> Unit,
     ) {
-        with(state.stateData) {
-            val defaultLocale = defaultLocale()
-            val formatterStartDate = dateFormatter.formatDate(
-                date = selectedStartDate.value,
-                calendarModel = calendarModel,
-                locale = defaultLocale
-            )
+        val defaultLocale = defaultLocale()
+        val formatterStartDate = dateFormatter.formatDate(
+            dateMillis = selectedStartDateMillis,
+            locale = defaultLocale
+        )
 
-            val formatterEndDate = dateFormatter.formatDate(
-                date = selectedEndDate.value,
-                calendarModel = calendarModel,
-                locale = defaultLocale
-            )
+        val formatterEndDate = dateFormatter.formatDate(
+            dateMillis = selectedEndDateMillis,
+            locale = defaultLocale
+        )
 
-            val verboseStartDateDescription = dateFormatter.formatDate(
-                date = selectedStartDate.value,
-                calendarModel = calendarModel,
-                locale = defaultLocale,
-                forContentDescription = true
-            ) ?: when (displayMode.value) {
-                DisplayMode.Picker -> getString(Strings.DatePickerNoSelectionDescription)
-                DisplayMode.Input -> getString(Strings.DateInputNoInputDescription)
-                else -> ""
+        val verboseStartDateDescription = dateFormatter.formatDate(
+            dateMillis = selectedStartDateMillis,
+            locale = defaultLocale,
+            forContentDescription = true
+        ) ?: when (displayMode) {
+            DisplayMode.Picker -> getString(Strings.DatePickerNoSelectionDescription)
+            DisplayMode.Input -> getString(Strings.DateInputNoInputDescription)
+            else -> ""
+        }
+
+        val verboseEndDateDescription = dateFormatter.formatDate(
+            dateMillis = selectedEndDateMillis,
+            locale = defaultLocale,
+            forContentDescription = true
+        ) ?: when (displayMode) {
+            DisplayMode.Picker -> getString(Strings.DatePickerNoSelectionDescription)
+            DisplayMode.Input -> getString(Strings.DateInputNoInputDescription)
+            else -> ""
+        }
+
+        val startHeadlineDescription = "$startDateText: $verboseStartDateDescription"
+        val endHeadlineDescription = "$endDateText: $verboseEndDateDescription"
+
+        Row(
+            modifier = modifier.clearAndSetSemantics {
+                liveRegion = LiveRegionMode.Polite
+                contentDescription = "$startHeadlineDescription, $endHeadlineDescription"
+            },
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = Arrangement.spacedBy(4.dp),
+        ) {
+            if (formatterStartDate != null) {
+                Text(text = formatterStartDate)
+            } else {
+                startDatePlaceholder()
             }
-
-            val verboseEndDateDescription = dateFormatter.formatDate(
-                date = selectedEndDate.value,
-                calendarModel = calendarModel,
-                locale = defaultLocale,
-                forContentDescription = true
-            ) ?: when (displayMode.value) {
-                DisplayMode.Picker -> getString(Strings.DatePickerNoSelectionDescription)
-                DisplayMode.Input -> getString(Strings.DateInputNoInputDescription)
-                else -> ""
-            }
-
-            val startHeadlineDescription = "$startDateText: $verboseStartDateDescription"
-            val endHeadlineDescription = "$endDateText: $verboseEndDateDescription"
-
-            Row(
-                modifier = modifier.clearAndSetSemantics {
-                    liveRegion = LiveRegionMode.Polite
-                    contentDescription = "$startHeadlineDescription, $endHeadlineDescription"
-                },
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = Arrangement.spacedBy(4.dp),
-            ) {
-                if (formatterStartDate != null) {
-                    Text(text = formatterStartDate)
-                } else {
-                    startDatePlaceholder()
-                }
-                datesDelimiter()
-                if (formatterEndDate != null) {
-                    Text(text = formatterEndDate)
-                } else {
-                    endDatePlaceholder()
-                }
+            datesDelimiter()
+            if (formatterEndDate != null) {
+                Text(text = formatterEndDate)
+            } else {
+                endDatePlaceholder()
             }
         }
     }
 }
 
 /**
+ * A default implementation of the [DateRangePickerState]. See [rememberDateRangePickerState].
+ *
+ * The state's [selectedStartDateMillis] and [selectedEndDateMillis] will provide timestamps for the
+ * _beginning_ of the selected days (i.e. midnight in _UTC_ milliseconds from the epoch).
+ *
+ * @param initialSelectedStartDateMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of a start date. Provide a `null` to indicate no selection.
+ * @param initialSelectedEndDateMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of an end date. Provide a `null` to indicate no selection.
+ * @param initialDisplayedMonthMillis timestamp in _UTC_ milliseconds from the epoch that
+ * represents an initial selection of a month to be displayed to the user. By default, in case
+ * an `initialSelectedStartDateMillis` is provided, the initial displayed month would be the
+ * month of the selected date. Otherwise, in case `null` is provided, the displayed month would
+ * be the current one.
+ * @param yearRange an [IntRange] that holds the year range that the date picker will be limited
+ * to
+ * @param initialDisplayMode an initial [DisplayMode] that this state will hold
+ * @param selectableDates a [SelectableDates] that is consulted to check if a date is allowed.
+ * In case a date is not allowed to be selected, it will appear disabled in the UI.
+ * @see rememberDatePickerState
+ * @throws IllegalArgumentException if the initial timestamps do not fall within the year range
+ * this state is created with, or the end date precedes the start date, or when an end date is
+ * provided without a start date (e.g. the start date was null, while the end date was not).
+ */
+@ExperimentalMaterial3Api
+@Stable
+private class DateRangePickerStateImpl(
+    @Suppress("AutoBoxing") initialSelectedStartDateMillis: Long?,
+    @Suppress("AutoBoxing") initialSelectedEndDateMillis: Long?,
+    @Suppress("AutoBoxing") initialDisplayedMonthMillis: Long?,
+    yearRange: IntRange,
+    initialDisplayMode: DisplayMode,
+    selectableDates: SelectableDates
+) : BaseDatePickerStateImpl(
+    initialDisplayedMonthMillis,
+    yearRange,
+    selectableDates
+), DateRangePickerState {
+
+    /**
+     * A mutable state of [CalendarDate] that represents a selected start date.
+     */
+    private var _selectedStartDate = mutableStateOf<CalendarDate?>(null)
+
+    /**
+     * A mutable state of [CalendarDate] that represents a selected end date.
+     */
+    private var _selectedEndDate = mutableStateOf<CalendarDate?>(null)
+
+    /**
+     * Initialize the state with the provided initial selections.
+     */
+    init {
+        setSelection(
+            startDateMillis = initialSelectedStartDateMillis,
+            endDateMillis = initialSelectedEndDateMillis
+        )
+    }
+
+    /**
+     * A timestamp that represents the _start_ of the day of the selected start date in _UTC_
+     * milliseconds from the epoch.
+     *
+     * In case no date was selected or provided, the state will hold a `null` value.
+     *
+     * @throws IllegalArgumentException in case a set timestamp does not fall within the year range
+     * this state was created with.
+     */
+    override val selectedStartDateMillis: Long?
+        @Suppress("AutoBoxing") get() = _selectedStartDate.value?.utcTimeMillis
+
+    /**
+     * A timestamp that represents the _start_ of the day of the selected end date in _UTC_
+     * milliseconds from the epoch.
+     *
+     * In case no date was selected or provided, the state will hold a `null` value.
+     *
+     * @throws IllegalArgumentException in case a set timestamp does not fall within the year range
+     * this state was created with.
+     */
+    override val selectedEndDateMillis: Long?
+        @Suppress("AutoBoxing") get() = _selectedEndDate.value?.utcTimeMillis
+
+    /**
+     * A mutable state of [DisplayMode] that represents the current display mode of the UI
+     * (i.e. picker or input).
+     */
+    private var _displayMode = mutableStateOf(initialDisplayMode)
+
+    override var displayMode
+        get() = _displayMode.value
+        set(displayMode) {
+            selectedStartDateMillis?.let {
+                displayedMonthMillis = calendarModel.getMonth(it).startUtcTimeMillis
+            }
+            _displayMode.value = displayMode
+        }
+
+    override fun setSelection(
+        @Suppress("AutoBoxing") startDateMillis: Long?,
+        @Suppress("AutoBoxing") endDateMillis: Long?
+    ) {
+        val startDate = if (startDateMillis != null) {
+            calendarModel.getCanonicalDate(startDateMillis)
+        } else {
+            null
+        }
+        val endDate = if (endDateMillis != null) {
+            calendarModel.getCanonicalDate(endDateMillis)
+        } else {
+            null
+        }
+        // Validate that both dates are within the valid years range.
+        startDate?.let {
+            require(yearRange.contains(it.year)) {
+                "The provided start date year (${it.year}) is out of the years range of $yearRange."
+            }
+        }
+        endDate?.let {
+            require(yearRange.contains(it.year)) {
+                "The provided end date year (${it.year}) is out of the years range of $yearRange."
+            }
+        }
+        // Validate that an end date cannot be set without a start date.
+        if (endDate != null) {
+            requireNotNull(startDate) {
+                "An end date was provided without a start date."
+            }
+            // Validate that the end date appears on or after the start date.
+            require(startDate.utcTimeMillis <= endDate.utcTimeMillis) {
+                "The provided end date appears before the start date."
+            }
+        }
+        _selectedStartDate.value = startDate
+        _selectedEndDate.value = endDate
+    }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [DateRangePickerStateImpl].
+         *
+         * @param selectableDates a [SelectableDates] instance that is consulted to check if a date
+         * is allowed
+         */
+        fun Saver(selectableDates: SelectableDates): Saver<DateRangePickerStateImpl, Any> =
+            listSaver(
+                save = {
+                    listOf(
+                        it.selectedStartDateMillis,
+                        it.selectedEndDateMillis,
+                        it.displayedMonthMillis,
+                        it.yearRange.first,
+                        it.yearRange.last,
+                        it.displayMode.value
+                    )
+                },
+                restore = { value ->
+                    DateRangePickerStateImpl(
+                        initialSelectedStartDateMillis = value[0] as Long?,
+                        initialSelectedEndDateMillis = value[1] as Long?,
+                        initialDisplayedMonthMillis = value[2] as Long?,
+                        yearRange = IntRange(value[3] as Int, value[4] as Int),
+                        initialDisplayMode = DisplayMode(value[5] as Int),
+                        selectableDates = selectableDates
+                    )
+                }
+            )
+    }
+}
+
+/**
  * Date entry content that displays a [DateRangePickerContent] or a [DateRangeInputContent]
  * according to the state's display mode.
  */
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SwitchableDateEntryContent(
-    state: DateRangePickerState,
+    selectedStartDateMillis: Long?,
+    selectedEndDateMillis: Long?,
+    displayedMonthMillis: Long,
+    displayMode: DisplayMode,
+    onDatesSelectionChange: (startDateMillis: Long?, endDateMillis: Long?) -> Unit,
+    onDisplayedMonthChange: (monthInMillis: Long) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
     dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates,
     colors: DatePickerColors
 ) {
     // TODO(b/266480386): Apply the motion spec for this once we have it. Consider replacing this
     //  with AnimatedContent when it's out of experimental.
     Crossfade(
-        targetState = state.displayMode,
+        targetState = displayMode,
         animationSpec = spring(),
         modifier = Modifier.semantics { isContainer = true }) { mode ->
         when (mode) {
             DisplayMode.Picker -> DateRangePickerContent(
-                stateData = state.stateData,
+                selectedStartDateMillis = selectedStartDateMillis,
+                selectedEndDateMillis = selectedEndDateMillis,
+                displayedMonthMillis = displayedMonthMillis,
+                onDatesSelectionChange = onDatesSelectionChange,
+                onDisplayedMonthChange = onDisplayedMonthChange,
+                calendarModel = calendarModel,
+                yearRange = yearRange,
                 dateFormatter = dateFormatter,
+                selectableDates = selectableDates,
                 colors = colors
             )
 
             DisplayMode.Input -> DateRangeInputContent(
-                stateData = state.stateData,
-                dateFormatter = dateFormatter
+                selectedStartDateMillis = selectedStartDateMillis,
+                selectedEndDateMillis = selectedEndDateMillis,
+                onDatesSelectionChange = onDatesSelectionChange,
+                calendarModel = calendarModel,
+                yearRange = yearRange,
+                dateFormatter = dateFormatter,
+                selectableDates = selectableDates
             )
         }
     }
@@ -485,25 +653,32 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun DateRangePickerContent(
-    stateData: StateData,
+    selectedStartDateMillis: Long?,
+    selectedEndDateMillis: Long?,
+    displayedMonthMillis: Long,
+    onDatesSelectionChange: (startDateMillis: Long?, endDateMillis: Long?) -> Unit,
+    onDisplayedMonthChange: (monthInMillis: Long) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
     dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates,
     colors: DatePickerColors
 ) {
+    val displayedMonth = calendarModel.getMonth(displayedMonthMillis)
     val monthsListState =
-        rememberLazyListState(
-            initialFirstVisibleItemIndex = stateData.displayedMonthIndex
-        )
-
-    val onDateSelected = { dateInMillis: Long ->
-        updateDateSelection(stateData, dateInMillis)
-    }
+        rememberLazyListState(initialFirstVisibleItemIndex = displayedMonth.indexIn(yearRange))
     Column(modifier = Modifier.padding(horizontal = DatePickerHorizontalPadding)) {
-        WeekDays(colors, stateData.calendarModel)
+        WeekDays(colors, calendarModel)
         VerticalMonthsList(
-            onDateSelected = onDateSelected,
-            stateData = stateData,
             lazyListState = monthsListState,
+            selectedStartDateMillis = selectedStartDateMillis,
+            selectedEndDateMillis = selectedEndDateMillis,
+            onDatesSelectionChange = onDatesSelectionChange,
+            onDisplayedMonthChange = onDisplayedMonthChange,
+            calendarModel = calendarModel,
+            yearRange = yearRange,
             dateFormatter = dateFormatter,
+            selectableDates = selectableDates,
             colors = colors
         )
     }
@@ -516,16 +691,21 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun VerticalMonthsList(
-    onDateSelected: (dateInMillis: Long) -> Unit,
-    stateData: StateData,
     lazyListState: LazyListState,
+    selectedStartDateMillis: Long?,
+    selectedEndDateMillis: Long?,
+    onDatesSelectionChange: (startDateMillis: Long?, endDateMillis: Long?) -> Unit,
+    onDisplayedMonthChange: (monthInMillis: Long) -> Unit,
+    calendarModel: CalendarModel,
+    yearRange: IntRange,
     dateFormatter: DatePickerFormatter,
+    selectableDates: SelectableDates,
     colors: DatePickerColors
 ) {
-    val today = stateData.calendarModel.today
-    val firstMonth = remember(stateData.yearRange) {
-        stateData.calendarModel.getMonth(
-            year = stateData.yearRange.first,
+    val today = calendarModel.today
+    val firstMonth = remember(yearRange) {
+        calendarModel.getMonth(
+            year = yearRange.first,
             month = 1 // January
         )
     }
@@ -545,9 +725,9 @@
             },
             state = lazyListState
         ) {
-            items(stateData.totalMonthsInRange) {
+            items(numberOfMonthsInRange(yearRange)) {
                 val month =
-                    stateData.calendarModel.plusMonths(
+                    calendarModel.plusMonths(
                         from = firstMonth,
                         addedMonthsCount = it
                     )
@@ -556,8 +736,7 @@
                 ) {
                     Text(
                         text = dateFormatter.formatMonthYear(
-                            month,
-                            stateData.calendarModel,
+                            month.startUtcTimeMillis,
                             defaultLocale()
                         ) ?: "-",
                         modifier = Modifier
@@ -573,13 +752,43 @@
                             },
                         color = colors.subheadContentColor
                     )
+                    val rangeSelectionInfo: State<SelectedRangeInfo?> =
+                        remember(selectedStartDateMillis, selectedEndDateMillis) {
+                            derivedStateOf {
+                                SelectedRangeInfo.calculateRangeInfo(
+                                    month = month,
+                                    startDate = selectedStartDateMillis?.let { date ->
+                                        calendarModel.getCanonicalDate(
+                                            date
+                                        )
+                                    },
+                                    endDate = selectedEndDateMillis?.let { date ->
+                                        calendarModel.getCanonicalDate(
+                                            date
+                                        )
+                                    }
+                                )
+                            }
+                        }
+                    // The updateDateSelection will invoke the onDatesSelectionChange with the proper
+                    // selection according to the current state.
+                    val onDateSelectionChange = { dateInMillis: Long ->
+                        updateDateSelection(
+                            dateInMillis = dateInMillis,
+                            currentStartDateMillis = selectedStartDateMillis,
+                            currentEndDateMillis = selectedEndDateMillis,
+                            onDatesSelectionChange = onDatesSelectionChange
+                        )
+                    }
                     Month(
                         month = month,
-                        onDateSelected = onDateSelected,
-                        today = today,
-                        stateData = stateData,
-                        rangeSelectionEnabled = true,
+                        onDateSelectionChange = onDateSelectionChange,
+                        todayMillis = today.utcTimeMillis,
+                        startDateMillis = selectedStartDateMillis,
+                        endDateMillis = selectedEndDateMillis,
+                        rangeSelectionInfo = rangeSelectionInfo.value,
                         dateFormatter = dateFormatter,
+                        selectableDates = selectableDates,
                         colors = colors
                     )
                 }
@@ -587,29 +796,33 @@
         }
     }
     LaunchedEffect(lazyListState) {
-        updateDisplayedMonth(lazyListState, stateData)
+        updateDisplayedMonth(
+            lazyListState = lazyListState,
+            onDisplayedMonthChange = onDisplayedMonthChange,
+            calendarModel = calendarModel,
+            yearRange = yearRange
+        )
     }
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
 private fun updateDateSelection(
-    stateData: StateData,
-    dateInMillis: Long
+    dateInMillis: Long,
+    currentStartDateMillis: Long?,
+    currentEndDateMillis: Long?,
+    onDatesSelectionChange: (startDateMillis: Long?, endDateMillis: Long?) -> Unit
 ) {
-    with(stateData) {
-        val date = calendarModel.getCanonicalDate(dateInMillis)
-        val currentStart = selectedStartDate.value
-        val currentEnd = selectedEndDate.value
-        if ((currentStart == null && currentEnd == null) ||
-            (currentStart != null && currentEnd != null) ||
-            (currentStart != null && date < currentStart)
-        ) {
-            // Reset the selection to "start" only.
-            selectedStartDate.value = date
-            selectedEndDate.value = null
-        } else if (currentStart != null && date >= currentStart) {
-            selectedEndDate.value = date
-        }
+    if ((currentStartDateMillis == null && currentEndDateMillis == null) ||
+        (currentStartDateMillis != null && currentEndDateMillis != null)
+    ) {
+        // Set the selection to "start" only.
+        onDatesSelectionChange(dateInMillis, null)
+    } else if (currentStartDateMillis != null && dateInMillis >= currentStartDateMillis) {
+        // Set the end date.
+        onDatesSelectionChange(currentStartDateMillis, dateInMillis)
+    } else {
+        // The user selected an earlier date than the start date, so reset the start.
+        onDatesSelectionChange(dateInMillis, null)
     }
 }
 
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/CalendarModel.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/CalendarModel.desktop.kt
index 5d4556e7..d7859f5 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/CalendarModel.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/CalendarModel.desktop.kt
@@ -34,7 +34,7 @@
  * @param locale the [Locale] to use when formatting the given timestamp
  */
 @ExperimentalMaterial3Api
-internal actual fun formatWithSkeleton(
+actual fun formatWithSkeleton(
     utcTimeMillis: Long,
     skeleton: String,
     locale: Locale