DragonFly - Maintaining state on Android Brownfield
DragonFly - Maintaining state on Android Brownfield¶
import useBaseUrl from ‘@docusaurus/useBaseUrl’
DragonFly - Maintaining state on Android Brownfield¶
The new DragonFly integration made the choice to make use of the 3rd Party React Navigation library, which in turn has a dependency on another 3rd Party library, React Native Screens. While testing the Android Brownfield setup of DragonFly on Android, we discovered crashes when swapping between Portrait and Landscape mode on any DragonFly experience. As it turned out, React Native Screens has an issue where it will crash if restarting a React Activity, or an Activity that is hosting a React Fragment. The solution to this issue, per their documentation, is to not save state using the primary Android state saving methodology between Activity restarts. While following this guidance removes the crashes and allows these experiences to swap between Portrait and Landscape, any experience with any form of state will lose it between rotations, which is a regression from current Android experiences. This document is to serve as a central point for listing what work arounds have been tried, and what options we still have for fixing this issue.
Terminology¶
Activity:
In Android, an Activity is an application component that provides a screen with which users can interact in order to do something. See Resources section below for Activity documentation for more details.
React Activity:
A React Activity is simply an extension of an Android Activity that is setup to host a React Native screen. We have created a custom GenericReactActivity to be used for all DragonFly React Native experiences on Android that need to be hosted in an Activity instead of a Fragment (anything that does not want the bottom nav bar, for example)
Fragment:
A Fragment represents a reusable portion of your app’s UI. A fragment defines and manages its own layout, has its own lifecycle, and can handle its own input events. Fragments can’t live on their own. They must be hosted by an activity or another fragment. See Resources section below for Fragment documentation for more details.
React Fragment:
Similar to a React Activity, a React Fragment is an extension on the base Android Fragment, meant to host a React Native View. We’ve implemented a custom GenericReactFragment to be used for the majority of DragonFly React Native experiences.
Save Instance Bundle:
An Android Bundle class object that is used to store the View State of an activity before an Activity is destroyed and recreated when triggered by something like an Orientation change (Portrait to Landscape, or vice versa). As mentioned above, our use of React Native Screens makes it so we cannot use this object and need to pass it as null.
ViewModel:
The ViewModel class is a business logic or screen level state holder. It exposes state to the UI and encapsulates related business logic. Its principal advantage is that it caches state and persists it through configuration changes
Resources¶
Relevant Crash Logs¶
Caused by: java.lang.IllegalStateException: Screen fragments should never be restored.
Follow instructions from
https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067 to properly configure your main activity.
at com.swmansion.rnscreens.ScreenFragment.<init>(ScreenFragment.kt:54)
at com.swmansion.rnscreens.ScreenStackFragment.<init>(ScreenStackFragment.kt:35)
... 31 more
Example¶
What has been tried?¶
Locking Screen Rotation¶
This works as a temporary fix: Locking screen rotation allows us to use the base Android State saving logic, and avoids crashes. However it has its own weird problems and edge case bugs to fix, and is overall a regression compared to current Android experiences. There is also an overall expectation that all Android pages be able to swap between Portrait and Landscape.
Override configuration change to do nothing¶
There is an onConfigurationChange method that can be overriden for Fragments and Activities. I attempted to have this function just return and do nothing when the configuration change was a swap from Portrait to Landscape or vice versa, however this led to the same original React Native Screens crash.
override fun onConfigurationChange(config) {
if (config.orientation == ActivityInfo.LANDSCAPE || config == ActivityInfo.PORTRAIT) {
return
}
super.onConfigurationChange(config)
}
For Fragments: Set React Fragments to be Retained Fragments¶
There is an option that can be set for Fragments to turn them into retained fragments in their onCreate Method. Retained Fragments are fragments that do not get destroyed when their activity restarts:
override fun onCreate(saveInstanceState: Bundle?) {
...
retainInstance = true
}
Setting this for ReactFragments however has the same outcome as the saveInstanceBundle: React Native Screens crash.
For Activities: Remove ScreenStackFragment object from saveInstanceState¶
Here, I attempted to remove the ScreenStackFragment for the saved state of the Activity. The goal was to see if I could get the system to recreate the ScreenStackFragment and ScreenFragment, the source of the crash (attempting to create a new one when one already exists) without losing the actual ReactActivity. This was a dead end however and attempts to remove this fragment from the saved state lead to the same RNScreens crash.
Store the View in a ViewModel¶
As suggested by the last comment at https://stackoverflow.com/questions/33591734/react-native-android-how-to-save-state-on-screen-rotation-or-orientation
We can cache a reference to the React Native view in the ViewModel, and reuse that when the view is recreated after runtime changes. This is not recommended by Android and leads to a few other issues around the view having stale references, but those can be worked around. Alternatively, we can store the screen object expected by RN Screens ScreenFragment constructor, to try and force the original constructor instead of the empty constructor that leads to the crash:
https://github.com/software-mansion/react-native-screens/blob/85093d6bc9374449da6c64efdfd40143513f9e59/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt#L60
After attempting this solution, I was only able to reach two different outcomes:
A crash occurs from trying to add a view to a parent object that already contains a view. This occurred if I simply stored the ReactRootView in the ViewModel and tried to set it as the ContentView.
A blank screen is shown. This occurred if I made sure to remove the original view from the parent before trying to set the ReactRootView I stored as the new ContentView.
Path Forward¶
Because numerous attempts to solve this on the Android side have lead to dead ends, the real path forward to a solution seems to lie on the React Native side of things. However, there are some other considerations to make as well. Given our goal of trying to unify iOS and Android experiences and development, should Android continue to have landscape views while iOS does not (besides Now Playing)? This is a conversation we likely should have with the design team. Perhaps the outcome of that conversation is we should have both platforms mimic iOS, which would remove the need for us to solve this rotation issue at all. Or perhaps the opposite decision is made, and the solution we come up with here will also need to cover iOS. Or perhaps nothing will change at all.
Assuming that the results of that conversation, if it occurs, are that we still need to solve this on Android, here are the options I’ve come up with for a solution on the React Native side:
Option 1: Store State in Dragonfly and reload it on page recreation (Recommended)¶
Instead of resolving this on the Android side, we could store the relevant state on the Dragonfly side and check if theres anything stored on page load. If there isn’t, load as normal. If there is, use the stored state. This would be more complicated than an Android solution most likely, but is something we are interested in long term for Dragonfly in order to pass state around between RN experiences.
For user input, there is some argument this state storage should be handled by the Bauhaus team/the Bauhaus components, rather than on the dragonfly side, as they have the necessary scope. They can persist state based on testId and can call the chosen storage solution without any extra context that Dragonfly would need. Then, experience teams could simply call for state to be stored and retrieved for a component, rather than have to go through each component and store the state themselves.
This is the recommended choice, as this state storage setup can be used for other use cases than rotation, such as passing state between experiences. Pros:
Long term gain
Has uses outside of solving for Android rotation like passing state between experiences
Likely a necessary add for other experiences that want to migrate to DF
Helps set a standard for how to handle state in a DF experience that needs offline/disk storage
Cons:
Risk of causing issues on iOS, since we only need this state storage for Android rotations
Largest scope also means hardest to gate and properly monitor usage of/set standards for
Option 2: Handle Rotation in DragonFly¶
Instead of storing state in DragonFly, we could instead leave rotation locked on Android and then handle actually rotating the view in the DragonFly view code. This would avoid the issue of the Page being recreated on rotation, so no state would be lost. However, rotation locking is not a usually recommended Adnroid design pattern and it can have interesting interactions when we’re doing something like going in and out of the locked Fragment or Activity from unlocked screens that we will need to solve. Another consideration with this option is whether or not we want to just always lock all DragonFly experiences on Android and handle their rotation in DF, or do we only want to lock experiences with state, and allow other experiences like Activity Feed and Find Landing Page to have their rotation handled by Android, as they do not need any extra state saved (Apollo cache will handle making the reload fast for us on their queries).
Pros:
Don’t need to handle state persistence
(Optional) Can implement a good looking, dedicated landscape view
We currently get the portrait view but stretched on Android in Landscape
No risk of affecting iOS, which has no issues currently
Cons:
(Optional) Need a design for what landscape should look like and implement it
Rotation locked Fragments can behave strangely when the Activity that they load on isn’t also rotation locked
Will need to thoroughly test this
Option 3: Don’t handle state on rotation¶
The last option would be simply to not handle the saving of state for a rotation change. This is actually the current behavior of the legacy RN experiences in Prod, and I have not been able to find any tickets about the issue. This would require the least amount of work (really no work besides removing rotation locks on Android).
Pros:
No work to be done: is the current state of Prod and Legacy RN experiences on Android
iOS does not rotate at all, so is not a concern there either
Cons:
Not Customer Obsessed
Could lead to customer frustration if state is lost due to an accidental rotation
[Extra] Possible State Storage Options¶
Should we choose to go the state management route, we will likely need a separate design review for deciding which State Management tool to use for this. However, here are a few good options with a short description and some pros and cons about them so you have an idea of what we have available to us.
1. AsyncStorage¶
https://reactnative.dev/docs/asyncstorage
AsyncStorage was a built in React Native library, and is now maintained as a community library. It serves as an unencrypted, asynchronous, persistent, key-value storage system that is global to the app. On iOS, AsyncStorage is backed by native code that stores small values in a serialized dictionary and larger values in separate files. On Android, AsyncStorage will use either RocksDB or SQLite based on what is available.
The general recommendation with AsyncStorage is to not use it directly, and instead use some kind of abstraction around it. As such, we could choose to setup our own abstraction around it, or if we want to go the simplest route possible, we could choose to use it directly despite their recommendation. See option 2 for using an already existing abstraction around it.
Pros
Very simple
Highly supported as a community library
Cons
Abstraction recommended over using directly
Better used in concert with other existing solutions than on its own
Limited in Scope on its own
2. MobX¶
We’ve already done a fairly deep dive on MobX in our State Management Systems doc under the Device Specific State Management section, so I recommend taking a look over there for a good rundown of MobX and what it can do.
A quick summary however: “MobX is a reactive state management library that makes it easy to manage and update application state using observable properties, actions, and computed values. It focuses on simplicity and ease of use.”
Another important note with MobX is that it does not persist its State Stores as a base feature, instead, the use of other libraries that work with it is necessary. A common choice is the mobx-persist library, which behind the hood makes use of ReactNative’s AsyncStorage, essentially serving as the recommended abstraction over AsyncStorage mentioned above.
Pros
Well tested library
Powerful
Simple to implement for simple use cases
Enforces clean separation of concerns and ownership
Persistence libraries like mobx-persist are readily available
Cons
Built before TypeScript was mainstream, meaning types can be slightly confusing and conflict with TypeScript
Can be difficult to Mock
Does not persist as a base feature: Need to use another library
3. Ignite useStorage¶
InfiniteRed is currently working on an alternative useState hook called useStorage , which is essentially a wrapper around useState and AsyncStorage, persisting the stored state to disk with AsyncStorage. This is not merged currently, but we could pull down and copy their implementation for our own use.
Pros:
Very simple to use
Can replace usages of useState throughout DF where needed
Lowest dev time
Limited scope → less framework to setup and standards to set
Cons:
Experimental
Would either be using a beta branch or copying the Ignite setup code for this locally ourselves, which means
Potentially higher maintenance as a new/untested feature
Higher chance of bugs as a new/untested feature
Potential need for rapid changes as the implementation of this WIP feature changes
Notes¶
Find way to figure out when orientation changes as a metric, use to check how much of an issue this is
Action items:
Look into ways to find metrics on rotations
Look for SongSearch and Reporting page type, check orientation if in metrics for UiPageView metric
Does rotation trigger a new UIPageView on the legacy experiences (it should)
Aggregate by deviceId, count the number that have some landscape and some portrait.
Limit query to a single day
Rework doc to move state storage recs out into a different section to reduce confusion and ensure the focus of the doc is tighter
Start working on a doc for Option 1 store

Comment out the part of RN Screens that throws the error on Activity recreate¶
https://github.com/software-mansion/react-native-screens/blob/85093d6bc9374449da6c64efdfd40143513f9e59/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt#L54
By making use of a patch file, we can comment out the lines of RN Screens causing a crash on activity recreate. However attempting this leads to a different crash: What seems to occur is that the incorrect RN Screens constructor is not called, and no screen object provided to the constructor, leading to this crash (copying only the most relevant part of the crash logs)