Skip to content

Commit 5d9a947

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Migration RN Alert Dialog to androidx (#44495)
Summary: ## Summary Migrates the `AlertFragment` from `android.app.AlertDialog` to `androidx.appcompat.app.AlertDialog`. This backports tons of fixes that have gone into the AlertDialog component over the years, including proper line wrapping of button text, dark mode support, alignment of buttons, etc. This change provides a fallback to the original `android.app.AlertDialog` if the current activity is not an AppCompat descendant. ## For consideration - Alert dialog themes may no longer need the `android` namespace, meaning themes can now be specified as `alertDialogTheme` rather than `android:alertDialogTheme`. ## Changelog: [Android] [Changed] - Migrated `AlertFragment` dialog builder to use `androidx.appcompat` Differential Revision: D57113950
1 parent 73cbe46 commit 5d9a947

File tree

2 files changed

+95
-22
lines changed

2 files changed

+95
-22
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.app.Dialog;
1212
import android.content.Context;
1313
import android.content.DialogInterface;
14+
import android.content.res.TypedArray;
1415
import android.os.Bundle;
1516
import androidx.annotation.Nullable;
1617
import androidx.appcompat.app.AlertDialog;
@@ -40,6 +41,33 @@ public AlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bund
4041

4142
public static Dialog createDialog(
4243
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
44+
if (isAppCompatTheme(activityContext)) {
45+
return createAppCompatDialog(activityContext, arguments, fragment);
46+
} else {
47+
return createAppDialog(activityContext, arguments, fragment);
48+
}
49+
}
50+
51+
/**
52+
* Checks if the current activity is a descendant of an AppCompat theme.
53+
*
54+
* @returns true if the current activity is a descendant of an AppCompat theme.
55+
*/
56+
private static boolean isAppCompatTheme(Context activityContext) {
57+
TypedArray attributes =
58+
activityContext.obtainStyledAttributes(androidx.appcompat.R.styleable.AppCompatTheme);
59+
boolean isAppCompat =
60+
attributes.hasValue(androidx.appcompat.R.styleable.AppCompatTheme_windowActionBar);
61+
attributes.recycle();
62+
return isAppCompat;
63+
}
64+
65+
/**
66+
* Creates a dialog compatible only with AppCompat activities. This function should be kept in
67+
* sync with {@link createAppDialog}.
68+
*/
69+
private static Dialog createAppCompatDialog(
70+
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
4371
AlertDialog.Builder builder =
4472
new AlertDialog.Builder(activityContext).setTitle(arguments.getString(ARG_TITLE));
4573

@@ -64,6 +92,39 @@ public static Dialog createDialog(
6492
return builder.create();
6593
}
6694

95+
/**
96+
* Creates a dialog compatible with non-AppCompat activities. This function should be kept in sync
97+
* with {@link createAppCompatDialog}.
98+
*
99+
* @deprecated non-AppCompat dialogs are deprecated and will be removed in a future version.
100+
*/
101+
private static Dialog createAppDialog(
102+
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
103+
android.app.AlertDialog.Builder builder =
104+
new android.app.AlertDialog.Builder(activityContext)
105+
.setTitle(arguments.getString(ARG_TITLE));
106+
107+
if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
108+
builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment);
109+
}
110+
if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
111+
builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment);
112+
}
113+
if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
114+
builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment);
115+
}
116+
// if both message and items are set, Android will only show the message
117+
// and ignore the items argument entirely
118+
if (arguments.containsKey(ARG_MESSAGE)) {
119+
builder.setMessage(arguments.getString(ARG_MESSAGE));
120+
}
121+
if (arguments.containsKey(ARG_ITEMS)) {
122+
builder.setItems(arguments.getCharSequenceArray(ARG_ITEMS), fragment);
123+
}
124+
125+
return builder.create();
126+
}
127+
67128
@Override
68129
public Dialog onCreateDialog(Bundle savedInstanceState) {
69130
return createDialog(getActivity(), getArguments(), this);

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/dialog/DialogModuleTest.kt

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,14 @@ class DialogModuleTest {
4747

4848
@Before
4949
fun setUp() {
50-
activityController = Robolectric.buildActivity(FragmentActivity::class.java)
51-
activity = activityController.create().start().resume().get()
52-
// We must set the theme to a descendant of AppCompat for the AlertDialog to show without
53-
// raising an exception
54-
activity.setTheme(APP_COMPAT_THEME)
55-
56-
val context: ReactApplicationContext = mock(ReactApplicationContext::class.java)
57-
whenever(context.hasActiveReactInstance()).thenReturn(true)
58-
whenever(context.currentActivity).thenReturn(activity)
59-
60-
dialogModule = DialogModule(context)
61-
dialogModule.onHostResume()
50+
setupActivity()
6251
}
6352

6453
@After
6554
fun tearDown() {
6655
activityController.pause().stop().destroy()
6756
}
6857

69-
@Test
70-
fun testIllegalActivityTheme() {
71-
val options = JavaOnlyMap()
72-
activity.setTheme(NON_APP_COMPAT_THEME)
73-
74-
assertThrows(NullPointerException::class.java) { dialogModule.showAlert(options, null, null) }
75-
76-
activity.setTheme(APP_COMPAT_THEME)
77-
}
78-
7958
@Test
8059
fun testAllOptions() {
8160
val options =
@@ -168,6 +147,39 @@ class DialogModuleTest {
168147
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args!![0])
169148
}
170149

150+
@Test
151+
fun testNonAppCompatActivityTheme() {
152+
setupActivity(NON_APP_COMPAT_THEME)
153+
154+
val options = JavaOnlyMap()
155+
156+
val actionCallback = SimpleCallback()
157+
dialogModule.showAlert(options, null, actionCallback)
158+
shadowOf(getMainLooper()).idle()
159+
160+
getFragment()!!.dialog!!.dismiss()
161+
shadowOf(getMainLooper()).idle()
162+
163+
assertEquals(1, actionCallback.calls)
164+
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args!![0])
165+
}
166+
167+
private fun setupActivity(theme: Int = APP_COMPAT_THEME) {
168+
activityController = Robolectric.buildActivity(FragmentActivity::class.java)
169+
activity = activityController.create().start().resume().get()
170+
171+
// We must set the theme to a descendant of AppCompat for the AlertDialog to show without
172+
// raising an exception
173+
activity.setTheme(theme)
174+
175+
val context: ReactApplicationContext = mock(ReactApplicationContext::class.java)
176+
whenever(context.hasActiveReactInstance()).thenReturn(true)
177+
whenever(context.currentActivity).thenReturn(activity)
178+
179+
dialogModule = DialogModule(context)
180+
dialogModule.onHostResume()
181+
}
182+
171183
private fun getFragment(): AlertFragment? {
172184
return activity.supportFragmentManager.findFragmentByTag(DialogModule.FRAGMENT_TAG)
173185
as? AlertFragment

0 commit comments

Comments
 (0)