react-native-date-picker
react-native-date-picker copied to clipboard
[Bug]: 🐛 Crash on Samsung Devices When Rotating Screen with `react-native-date-picker`
Describe the bug
Bug Description
When using react-native-date-picker on Samsung devices, the app crashes upon screen rotation (changing device orientation) while the date picker is open. This issue persists even when the date picker is used inside a modal (using the modal prop).
The crash seems to be specific to Samsung devices and occurs regardless of whether a modal or portal is used. All combinations tested result in a crash only on Samsung; the behavior is stable on other OEMs.
✅ Tested Setup
- Device: Samsung (multiple models)
- React Native:
0.76.9 - react-native-date-picker:
^5.0.12 - react-native-modal:
^13.0.1
📋 Code Snippet
{showDatePicker && (
<DatePicker
modal
open={showDatePicker}
date={expiryDate || new Date()}
onConfirm={handleDateChange}
onCancel={() => setShowDatePicker(false)}
mode="date"
minimumDate={new Date()}
/>
)}
📌 Notes
- The issue was not observed on other device brands.
- I also tested with and without portals and modals – the issue still persists on Samsung.
Expected behavior
- The DatePicker should remain stable and functional when the device orientation changes.
- Rotating the screen should not cause a crash, regardless of the device manufacturer.
- The modal should either adjust layout or re-render gracefully during orientation changes on all Android devices, including Samsung.
To Reproduce
{showDatePicker && (
<DatePicker
modal
open={showDatePicker}
date={expiryDate || new Date()}
onConfirm={handleDateChange}
onCancel={() => setShowDatePicker(false)}
mode="date"
minimumDate={new Date()}
/>
)}
Operating System
- [x] Android
- [ ] iOS
React Native Version
0.76.9
Expo Version (if applicable)
No response
react-native-date-picker version
5.0.12
React Native Architecture
- [x] Old Architecture (Paper)
- [ ] New Architecture (Fabric)
Relevant log output
Same issue
This patch should resolve the problem. Also, I made a PR
diff --git a/android/src/main/java/com/henninghall/date_picker/DatePickerModuleImpl.java b/android/src/main/java/com/henninghall/date_picker/DatePickerModuleImpl.java
index b8a2a800da92e0b67996eb6f2fea68ae6b57f0aa..d66b45870abecba5a022caabfd16a9467c54289c 100644
--- a/android/src/main/java/com/henninghall/date_picker/DatePickerModuleImpl.java
+++ b/android/src/main/java/com/henninghall/date_picker/DatePickerModuleImpl.java
@@ -1,17 +1,24 @@
package com.henninghall.date_picker;
-
import android.app.AlertDialog;
+import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Color;
+import android.os.Bundle;
import android.util.TypedValue;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableMap;
@@ -24,146 +31,197 @@ import net.time4j.android.ApplicationStarter;
public class DatePickerModuleImpl {
public static final String NAME = "RNDatePicker";
- private AlertDialog dialog;
+ private static final String DIALOG_TAG = "DatePickerDialog";
+ private DatePickerDialogFragment dialogFragment;
DatePickerModuleImpl(Context context) {
- ApplicationStarter.initialize(context, false); // false = no need to prefetch on time data background tread
+ ApplicationStarter.initialize(context, false); // false = no need to prefetch on time data background thread
}
- public void openPicker(ReadableMap props){
- final PickerView picker = createPicker(props);
- Callback onConfirm = new Callback() {
- @Override
- public void invoke(Object... objects) {
- Emitter.onConfirm(picker.getDate(), picker.getPickerId());
- }
- };
+ public void openPicker(ReadableMap props) {
+ // Get the current activity as FragmentActivity
+ FragmentActivity activity = (FragmentActivity) DatePickerPackage.context.getCurrentActivity();
+ if (activity == null) {
+ return;
+ }
- Callback onCancel = new Callback() {
- @Override
- public void invoke(Object... objects) {
- Emitter.onCancel(picker.getPickerId());
- }
- };
+ FragmentManager fragmentManager = activity.getSupportFragmentManager();
- dialog = createDialog(props, picker, onConfirm, onCancel);
- dialog.show();
- }
+ // Close any existing dialog
+ closePicker();
- public void closePicker(){
- dialog.dismiss();
+ // Create and show new dialog fragment
+ dialogFragment = DatePickerDialogFragment.newInstance(props);
+ dialogFragment.setProps(props);
+ dialogFragment.show(fragmentManager, DIALOG_TAG);
}
- private AlertDialog createDialog(
- ReadableMap props, final PickerView picker, final Callback onConfirm, final Callback onCancel) {
- String confirmText = props.getString("confirmText");
- final String cancelText = props.getString("cancelText");
- final String buttonColor = props.getString("buttonColor");
- final View pickerWithMargin = withTopMargin(picker);
-
- AlertDialog dialog = new AlertDialogBuilder(DatePickerPackage.context.getCurrentActivity(), getTheme(props))
- .setColoredTitle(props)
- .setCancelable(true)
- .setView(pickerWithMargin)
- .setPositiveButton(confirmText, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- onConfirm.invoke(picker.getDate());
- dialog.dismiss();
- }
- })
- .setNegativeButton(cancelText, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- onCancel.invoke();
- dialog.dismiss();
- }
- })
- .setOnCancelListener(new DialogInterface.OnCancelListener() {
- @Override
- public void onCancel(DialogInterface dialogInterface) {
- onCancel.invoke();
- }
- })
- .create();
-
- dialog.setOnShowListener(new DialogInterface.OnShowListener() {
- @Override
- public void onShow(DialogInterface dialoga) {
- if(buttonColor != null){
- int color = Color.parseColor(buttonColor);
- dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(color);
- dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(color);
+ public void closePicker() {
+ if (dialogFragment != null) {
+ dialogFragment.dismiss();
+ dialogFragment = null;
+ } else {
+ // Try to find and dismiss any existing dialog fragment
+ FragmentActivity activity = (FragmentActivity) DatePickerPackage.context.getCurrentActivity();
+ if (activity != null) {
+ FragmentManager fragmentManager = activity.getSupportFragmentManager();
+ DatePickerDialogFragment existingDialog =
+ (DatePickerDialogFragment) fragmentManager.findFragmentByTag(DIALOG_TAG);
+ if (existingDialog != null) {
+ existingDialog.dismiss();
}
-
}
- });
-
- return dialog;
+ }
}
- private int getTheme(ReadableMap props) {
- int defaultTheme = 0;
- String theme = props.getString("theme");
- if(theme == null) return defaultTheme;
- switch (theme){
- case "light": return AlertDialog.THEME_DEVICE_DEFAULT_LIGHT;
- case "dark": return AlertDialog.THEME_DEVICE_DEFAULT_DARK;
- default: return defaultTheme;
+ // Inner DialogFragment class
+ public static class DatePickerDialogFragment extends DialogFragment {
+
+ private static final String ARG_PROPS = "props";
+ private PickerView picker;
+ private ReadableMap props;
+ private AlertDialog alertDialog;
+
+ public static DatePickerDialogFragment newInstance(ReadableMap props) {
+ DatePickerDialogFragment fragment = new DatePickerDialogFragment();
+ Bundle args = new Bundle();
+ // Note: For full state restoration, you might need to serialize ReadableMap to Bundle
+ fragment.setArguments(args);
+ return fragment;
}
- }
- private PickerView createPicker(ReadableMap props){
- int height = 180;
- LinearLayout.LayoutParams rootLayoutParams = new LinearLayout.LayoutParams(
- RelativeLayout.LayoutParams.MATCH_PARENT,
- Utils.toDp(height));
- PickerView picker = new PickerView(rootLayoutParams);
- ReadableMapKeySetIterator iterator = props.keySetIterator();
- while(iterator.hasNextKey()){
- String key = iterator.nextKey();
- Dynamic value = props.getDynamic(key);
- if(!key.equals("style")){
- try{
- picker.updateProp(key, value);
- } catch (Exception e){
- // ignore invalid prop
+ public void setProps(ReadableMap props) {
+ this.props = props;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ if (props == null) {
+ dismiss();
+ return super.onCreateDialog(savedInstanceState);
+ }
+
+ picker = createPicker(props);
+
+ String confirmText = props.getString("confirmText");
+ String cancelText = props.getString("cancelText");
+ String buttonColor = props.getString("buttonColor");
+ View pickerWithMargin = withTopMargin(picker);
+
+ AlertDialog.Builder builder = new AlertDialogBuilder(requireContext(), getTheme(props))
+ .setColoredTitle(props)
+ .setCancelable(true)
+ .setView(pickerWithMargin)
+ .setPositiveButton(confirmText, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ Emitter.onConfirm(picker.getDate(), picker.getPickerId());
+ dismiss();
+ }
+ })
+ .setNegativeButton(cancelText, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ Emitter.onCancel(picker.getPickerId());
+ dismiss();
+ }
+ });
+
+ alertDialog = builder.create();
+
+ alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
+ @Override
+ public void onShow(DialogInterface dialog) {
+ if (buttonColor != null) {
+ int color = Color.parseColor(buttonColor);
+ alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(color);
+ alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(color);
+ }
}
+ });
+
+ return alertDialog;
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ if (picker != null) {
+ Emitter.onCancel(picker.getPickerId());
}
}
- picker.update();
- picker.addSpinnerStateListener(new SpinnerStateListener() {
- @Override
- public void onChange(SpinnerState state) {
- setEnabledConfirmButton(state == SpinnerState.idle);
+ private int getTheme(ReadableMap props) {
+ int defaultTheme = 0;
+ String theme = props.getString("theme");
+ if (theme == null) return defaultTheme;
+ switch (theme) {
+ case "light":
+ return AlertDialog.THEME_DEVICE_DEFAULT_LIGHT;
+ case "dark":
+ return AlertDialog.THEME_DEVICE_DEFAULT_DARK;
+ default:
+ return defaultTheme;
}
- });
+ }
- return picker;
- }
+ private PickerView createPicker(ReadableMap props) {
+ int height = 180;
+ LinearLayout.LayoutParams rootLayoutParams = new LinearLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT,
+ Utils.toDp(height));
+ PickerView picker = new PickerView(rootLayoutParams);
+ ReadableMapKeySetIterator iterator = props.keySetIterator();
+ while (iterator.hasNextKey()) {
+ String key = iterator.nextKey();
+ Dynamic value = props.getDynamic(key);
+ if (!key.equals("style")) {
+ try {
+ picker.updateProp(key, value);
+ } catch (Exception e) {
+ // ignore invalid prop
+ }
+ }
+ }
+ picker.update();
- private void setEnabledConfirmButton(boolean enabled) {
- dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
- }
+ picker.addSpinnerStateListener(new SpinnerStateListener() {
+ @Override
+ public void onChange(SpinnerState state) {
+ setEnabledConfirmButton(state == SpinnerState.idle);
+ }
+ });
+
+ return picker;
+ }
+
+ private void setEnabledConfirmButton(boolean enabled) {
+ if (alertDialog != null) {
+ alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
+ }
+ }
- private View withTopMargin(PickerView view) {
- LinearLayout linearLayout = new LinearLayout(DatePickerPackage.context);
- linearLayout.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- ));
- linearLayout.addView(view);
- linearLayout.setPadding(0, Utils.toDp(20),0,0);
- return linearLayout;
+ private View withTopMargin(PickerView view) {
+ LinearLayout linearLayout = new LinearLayout(requireContext());
+ linearLayout.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ ));
+ linearLayout.addView(view);
+ linearLayout.setPadding(0, Utils.toDp(20), 0, 0);
+ return linearLayout;
+ }
}
+ // Inner AlertDialogBuilder class
static class AlertDialogBuilder extends AlertDialog.Builder {
public AlertDialogBuilder(Context context, int themeResId) {
super(context, themeResId);
}
- public AlertDialogBuilder setColoredTitle(ReadableMap props){
+
+ public AlertDialog.Builder setColoredTitle(ReadableMap props) {
String textColor = props.getString("textColor");
String title = props.getString("title");
- if(textColor == null){
+ if (textColor == null) {
this.setTitle(title);
return this;
}
@@ -181,6 +239,4 @@ public class DatePickerModuleImpl {
return this;
}
}
-
-
}