react-native-date-picker icon indicating copy to clipboard operation
react-native-date-picker copied to clipboard

[Bug]: 🐛 Crash on Samsung Devices When Rotating Screen with `react-native-date-picker`

Open saif-techversant opened this issue 8 months ago • 2 comments

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


saif-techversant avatar Apr 21 '25 11:04 saif-techversant

Same issue

HariomJGohel avatar May 07 '25 10:05 HariomJGohel

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;
         }
     }
-
-
 }

zhenkaGo avatar Sep 07 '25 11:09 zhenkaGo