From 6048cc590ccf6144cddfa0227b8d00665bb8561b Mon Sep 17 00:00:00 2001 From: yevhencheshchevyi Date: Sun, 7 Sep 2025 13:48:57 +0300 Subject: [PATCH] refactor: migrate Android date picker from AlertDialog to DialogFragment - Replace AlertDialog-based implementation with DialogFragment for better lifecycle management - Add proper fragment handling for activity configuration changes - Improve dialog dismissal logic with fragment manager cleanup - Maintain existing picker functionality and event handling --- .../date_picker/DatePickerModuleImpl.java | 273 ++++++++++-------- 1 file changed, 160 insertions(+), 113 deletions(-) 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 b8a2a800..cebf191a 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,188 @@ 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); } - 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) { + 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(); - } + closePicker(); - public void closePicker(){ - dialog.dismiss(); + 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 { + 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; + 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(); + 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) {} + } + } + 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; + } } 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 +230,4 @@ public AlertDialogBuilder setColoredTitle(ReadableMap props){ return this; } } - - -} \ No newline at end of file +}