Skip to content

Commit 93cee67

Browse files
Skalakidbojanlozanovski77kkafar
authored
fix(Android): add custom fragment factory & modify library installation steps to prevent on-restoration crashes (#3089)
## Description <!-- Description and motivation for this PR. Include Fixes #<number> if this is fixing some issue. Fixes # . --> This PR replaces the current workaround for crashes connected to the configuration changes on Android. The previous fix required disabling passing saved instance state in Activity onCreate - `onCreate(null)`. Such a solution generates a problem with state persistence on the native side of brownfield applications. Because of that, we decided to replace it with a custom fragment factory that will limit the scope of the solution and fix the problem directly only on the React Native side. All fragments connected to the RN Screens package will be replaced with self-destructing ones, which will prevent Android from recreating the Fragment after the configuration change. We will leave this responsibility for React Native. Thanks to that, there won't be any conflict between the Android system and React Native, and we will prevent the app from crashing or any other app appearance-related issues. Credits to @bojanlozanovski77 for bringing up the idea in #2917 Fixes: #17 ## Changes <!-- Please describe things you've changed here, make a **high level** overview, if change is simple you can omit this section. For example: - Updated `about.md` docs --> - Created a custom fragment factory (RNScreensFragmentFactory.kt) that won't instantiate RN Screens fragments - Created self-destructing fragments (NoOpFragment.kt) that will be instanciated by the RNScreensFragmentFactory, to prevent the conflict that crashes the app - Updated installation steps in library README - Applied the fix in `MainActivity` files of `FabricExample` and `Example` apps <!-- ## Screenshots / GIFs Here you can add screenshots / GIFs documenting your change. You can add before / after section if you're changing some behavior. ### Before ### After --> ## Test code and steps to reproduce <!-- Please include code that can be used to test this change and short description how this example should work. This snippet should be as minimal as possible and ready to be pasted into editor (don't exclude exports or remove "not important" parts of reproduction example) --> 1. Remove ignoring config changes in `AndroidManifest.xml` file ```diff <activity android:name=".MainActivity" android:label="@string/app_name" - android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:launchMode="singleTask" ... ``` 2. Launch the example app 3. Verify if the app isn't crashing when causing different types of configuration changes on Android. For example: - Orientation change on devices ≥600dp with portrait lock - Opening/closing foldables or flip phones - Split/Multi-Window mode - Desktop windowing on tablets - Launching external activities (e.g. system Photo Picker) - Changing accessibility settings, permissions, system fonts, wallpapers, or language ## Checklist - [X] Included code example that can be used to test this change - [X] Updated TS types - [X] Updated documentation: <!-- For adding new props to native-stack --> - [X] https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md - [X] https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md - [X] https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx - [X] https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx - [X] Ensured that CI passes --------- Co-authored-by: bojanlozanovski77 <[email protected]> Co-authored-by: Kacper Kafara <[email protected]>
1 parent ed11286 commit 93cee67

File tree

5 files changed

+66
-2
lines changed

5 files changed

+66
-2
lines changed

Example/android/app/src/main/java/com/swmansion/rnscreens/example/MainActivity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.facebook.react.ReactActivity
55
import com.facebook.react.ReactActivityDelegate
66
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
77
import com.facebook.react.defaults.DefaultReactActivityDelegate
8+
import com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory
89

910
class MainActivity : ReactActivity() {
1011

@@ -22,6 +23,7 @@ class MainActivity : ReactActivity() {
2223
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
2324

2425
override fun onCreate(savedInstanceState: Bundle?) {
25-
super.onCreate(null)
26+
supportFragmentManager.fragmentFactory = RNScreensFragmentFactory()
27+
super.onCreate(savedInstanceState)
2628
}
2729
}

FabricExample/android/app/src/main/java/com/fabricexample/MainActivity.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.fabricexample
22

3+
import android.os.Bundle
34
import com.facebook.react.ReactActivity
45
import com.facebook.react.ReactActivityDelegate
56
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
67
import com.facebook.react.defaults.DefaultReactActivityDelegate
8+
import com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory
79

810
class MainActivity : ReactActivity() {
911

@@ -19,4 +21,9 @@ class MainActivity : ReactActivity() {
1921
*/
2022
override fun createReactActivityDelegate(): ReactActivityDelegate =
2123
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
24+
25+
override fun onCreate(savedInstanceState: Bundle?) {
26+
supportFragmentManager.fragmentFactory = RNScreensFragmentFactory()
27+
super.onCreate(savedInstanceState)
28+
}
2229
}

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Please note that the override code should not be placed inside `MainActivityDele
3535

3636
```java
3737
import android.os.Bundle;
38+
import com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory;
3839

3940
public class MainActivity extends ReactActivity {
4041

@@ -43,7 +44,8 @@ public class MainActivity extends ReactActivity {
4344
//react-native-screens override
4445
@Override
4546
protected void onCreate(Bundle savedInstanceState) {
46-
super.onCreate(null);
47+
getSupportFragmentManager().setFragmentFactory(new RNScreensFragmentFactory());
48+
super.onCreate(savedInstanceState);
4749
}
4850

4951
public static class MainActivityDelegate extends ReactActivityDelegate {
@@ -59,13 +61,15 @@ public class MainActivity extends ReactActivity {
5961

6062
```kotlin
6163
import android.os.Bundle;
64+
import com.swmansion.rnscreens.fragment.restoration.RNScreensFragmentFactory;
6265

6366
class MainActivity: ReactActivity() {
6467

6568
//...code
6669

6770
//react-native-screens override
6871
override fun onCreate(savedInstanceState: Bundle?) {
72+
supportFragmentManager.fragmentFactory = RNScreensFragmentFactory()
6973
super.onCreate(null);
7074
}
7175
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.swmansion.rnscreens.fragment.restoration
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.fragment.app.Fragment
8+
9+
/**
10+
* This class serves as a workaround to https://github.com/software-mansion/react-native-screens/issues/17.
11+
*
12+
* This fragment, when attached to the fragment manager & its state is progressed
13+
* to `ON_CREATED`, attempts to detach itself from the parent fragment manager
14+
* as soon as possible.
15+
*
16+
* Instances of this type should be created in place of regular screen fragments
17+
* when Android restores fragments after activity / application restart.
18+
* If done so, it's behaviour can prevent duplicated fragment instances,
19+
* as React will render new ones on activity restart.
20+
*/
21+
class AutoRemovingFragment : Fragment() {
22+
override fun onCreate(savedInstanceState: Bundle?) {
23+
super.onCreate(savedInstanceState)
24+
25+
// This is the first moment where we have access to non-null parent fragment manager,
26+
// so that we can remove the fragment from the hierarchy.
27+
parentFragmentManager
28+
.beginTransaction()
29+
.remove(this)
30+
.commitAllowingStateLoss()
31+
}
32+
33+
override fun onCreateView(
34+
inflater: LayoutInflater,
35+
container: ViewGroup?,
36+
savedInstanceState: Bundle?,
37+
): View? = null
38+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.swmansion.rnscreens.fragment.restoration
2+
3+
class RNScreensFragmentFactory : androidx.fragment.app.FragmentFactory() {
4+
override fun instantiate(
5+
classLoader: ClassLoader,
6+
className: String,
7+
): androidx.fragment.app.Fragment =
8+
if (className.startsWith(com.swmansion.rnscreens.BuildConfig.LIBRARY_PACKAGE_NAME)) {
9+
AutoRemovingFragment()
10+
} else {
11+
super.instantiate(classLoader, className)
12+
}
13+
}

0 commit comments

Comments
 (0)