Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes fragment commit to synchronous #1066

Merged
merged 16 commits into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import Test887 from './src/Test887';
import Test898 from './src/Test898';
import Test913 from './src/Test913';
import Test999 from './src/Test999';
import Test1017 from './src/Test1017';
import Test1031 from './src/Test1031';
import Test1032 from './src/Test1032';
import Test1036 from './src/Test1036';
Expand Down
146 changes: 146 additions & 0 deletions TestsExample/src/Test1017.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {NavigationContainer, RouteProp} from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
TransitionPresets,
} from '@react-navigation/stack';
import React from 'react';
import {Alert, LogBox} from 'react-native';
import {StyleSheet, Text, View, FlatList, Pressable} from 'react-native';
import {ScrollView} from 'react-native-gesture-handler';

import {
Header,
Colors,
DebugInstructions,
} from 'react-native/Libraries/NewAppScreen';

type DataItem = {
title: string;
value: number;
};

enum Route {
FirstScreen = 'FirstScreen',
SecondScreen = 'SecondScreen',
}

type RootStackParamList = {
[Route.FirstScreen]: undefined;
[Route.SecondScreen]: undefined;
};

LogBox.ignoreLogs([
'VirtualizedLists should never be nested inside plain ScrollViews',
]);

const Stack = createStackNavigator();

const dataset = Array.from<unknown, DataItem>({length: 100}, (_, i) => ({
title: `Title ${i}`,
value: i,
}));

const Screen: React.FC<{
route: RouteProp<RootStackParamList, any>;
navigation: StackNavigationProp<RootStackParamList, any>;
}> = ({route, navigation}) => {
for (let i = 0; i < 10 ** 3; i++) {}
const isSecondScreen = route.name === Route.SecondScreen;

const handleItemPress = (item: DataItem) => () => {
if (isSecondScreen) {
Alert.alert(item.title, item.value.toString());
return;
}
navigation.navigate(Route.SecondScreen);
};

const renderItem = (item: DataItem) => (
<Pressable
android_ripple={{color: Colors.light}}
style={styles.listItemContainer}
onPress={handleItemPress(item)}>
<View style={styles.listItemContentWrapper}>
<Text style={styles.listItemTitle}>{item.title}</Text>
<Text>{`This is item number ${item.value}`}</Text>
</View>
</Pressable>
);

return (
// So in a much more complex scenario, we might have a ScrollView that contains multiple horizontal list
// I know it's bad to have a FlatList nested in ScrollView, ideally I should use something better
// However, it seems like having a nested VirtualizeList in ScrollView will cause the transition lag issue
<ScrollView>
<FlatList
data={dataset}
keyExtractor={(item) => item.value.toString()}
renderItem={({item}) => renderItem(item)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListHeaderComponent={!isSecondScreen && Header}
ListHeaderComponentStyle={styles.headerWrapper}
ListFooterComponent={!isSecondScreen && DebugInstructions}
ListFooterComponentStyle={styles.footerWrapper}
scrollEnabled={false}
/>
</ScrollView>
);
};

const App: React.FC = () => {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{...TransitionPresets.SlideFromRightIOS}}>
<Stack.Screen
name={Route.FirstScreen}
component={Screen}
options={{title: 'Sample List'}}
/>
<Stack.Screen
name={Route.SecondScreen}
component={Screen}
options={{title: 'Sample List 2'}}
/>
</Stack.Navigator>
</NavigationContainer>
);
};

const styles = StyleSheet.create({
headerWrapper: {
overflow: 'hidden',
},
footerWrapper: {
paddingVertical: 50 / 3,
paddingHorizontal: 40 / 3,
},
listItemContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 50 / 3,
paddingVertical: 40 / 3,
},
image: {
width: 200 / 3,
aspectRatio: 1,
borderRadius: 25 / 3,
backgroundColor: Colors.primary,
},
listItemContentWrapper: {
flexDirection: 'column',
paddingHorizontal: 30 / 3,
},
listItemTitle: {
fontWeight: 'bold',
fontSize: 50 / 3,
},
separator: {
height: 1 / 3,
flex: 1,
marginHorizontal: 50 / 3,
backgroundColor: '#000',
},
});

export default App;
95 changes: 17 additions & 78 deletions android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,13 @@ import com.facebook.react.ReactRootView
import com.facebook.react.modules.core.ChoreographerCompat
import com.facebook.react.modules.core.ReactChoreographer
import com.swmansion.rnscreens.Screen.ActivityState
import java.lang.IllegalStateException

open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(context) {
@JvmField
protected val mScreenFragments = ArrayList<T>()
@JvmField
protected var mFragmentManager: FragmentManager? = null
private var mCurrentTransaction: FragmentTransaction? = null
private var mProcessingTransaction: FragmentTransaction? = null
private var mNeedUpdate = false
private var mIsAttached = false
private val mFrameCallback: ChoreographerCompat.FrameCallback = object : ChoreographerCompat.FrameCallback() {
override fun doFrame(frameTimeNanos: Long) {
updateIfNeeded()
}
}
private var mLayoutEnqueued = false
private val mLayoutCallback: ChoreographerCompat.FrameCallback = object : ChoreographerCompat.FrameCallback() {
override fun doFrame(frameTimeNanos: Long) {
Expand Down Expand Up @@ -89,20 +80,8 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
val isNested: Boolean
get() = mParentScreenFragment != null

protected fun markUpdated() {
if (!mNeedUpdate) {
mNeedUpdate = true
// enqueue callback of NATIVE_ANIMATED_MODULE type as all view operations are executed in
// DISPATCH_UI type and we want the callback to be called right after in the same frame.
ReactChoreographer.getInstance()
.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, mFrameCallback
)
}
}

fun notifyChildUpdate() {
markUpdated()
performUpdate()
}

protected open fun adapt(screen: Screen): T {
Expand All @@ -115,21 +94,21 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
screen.fragment = fragment
mScreenFragments.add(index, fragment)
screen.container = this
markUpdated()
performUpdate()
}

open fun removeScreenAt(index: Int) {
mScreenFragments[index].screen.container = null
mScreenFragments.removeAt(index)
markUpdated()
performUpdate()
}

open fun removeAllScreens() {
for (screenFragment in mScreenFragments) {
screenFragment.screen.container = null
}
mScreenFragments.clear()
markUpdated()
performUpdate()
}

val screenCount: Int
Expand All @@ -151,7 +130,7 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co

private fun setFragmentManager(fm: FragmentManager) {
mFragmentManager = fm
updateIfNeeded()
performUpdate()
}

private fun setupFragmentManager() {
Expand Down Expand Up @@ -189,47 +168,23 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
setFragmentManager(context.supportFragmentManager)
}

protected fun getOrCreateTransaction(): FragmentTransaction {
if (mCurrentTransaction == null) {
val fragmentManager = requireNotNull(mFragmentManager, { "mFragmentManager is null when creating transaction" })
val transaction = fragmentManager.beginTransaction()
transaction.setReorderingAllowed(true)
mCurrentTransaction = transaction
}
mCurrentTransaction?.let { return it }
throw IllegalStateException("mCurrentTransaction changed to null during creating transaction")
}

protected fun tryCommitTransaction() {
val transaction = mCurrentTransaction
if (transaction != null) {
mProcessingTransaction = transaction
mProcessingTransaction?.runOnCommit {
if (mProcessingTransaction === transaction) {
// we need to take into account that commit is initiated with some other transaction
// while the previous one is still processing. In this case mProcessingTransaction
// gets overwritten and we don't want to set it to null until the second transaction
// is finished.
mProcessingTransaction = null
}
}
transaction.commitAllowingStateLoss()
mCurrentTransaction = null
}
protected fun createTransaction(): FragmentTransaction {
val fragmentManager = requireNotNull(mFragmentManager, { "mFragmentManager is null when creating transaction" })
val transaction = fragmentManager.beginTransaction()
transaction.setReorderingAllowed(true)
return transaction
}

private fun attachScreen(screenFragment: T) {
getOrCreateTransaction().add(id, screenFragment)
private fun attachScreen(screenFragment: ScreenFragment) {
createTransaction().add(id, screenFragment).commitNowAllowingStateLoss()
}

private fun moveToFront(screenFragment: ScreenFragment) {
val transaction = getOrCreateTransaction()
transaction.remove(screenFragment)
transaction.add(id, screenFragment)
createTransaction().remove(screenFragment).add(id, screenFragment).commitNowAllowingStateLoss()
}

private fun detachScreen(screenFragment: ScreenFragment) {
getOrCreateTransaction().remove(screenFragment)
createTransaction().remove(screenFragment).commitNowAllowingStateLoss()
}

private fun getActivityState(screenFragment: ScreenFragment): ActivityState? {
Expand All @@ -243,7 +198,6 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
override fun onAttachedToWindow() {
super.onAttachedToWindow()
mIsAttached = true
mNeedUpdate = true
setupFragmentManager()
}

Expand Down Expand Up @@ -311,29 +265,15 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
}
}

private fun updateIfNeeded() {
if (!mNeedUpdate || !mIsAttached || mFragmentManager == null) {
protected fun performUpdate() {
if (!mIsAttached || mFragmentManager == null) {
return
}
mNeedUpdate = false
onUpdate()
}

private fun onUpdate() {
// We double check if fragment manager have any pending transactions to run.
// In performUpdate we often check whether some fragments are added to
// manager to avoid adding them for the second time (which result in crash).
// By design performUpdate should be called at most once per frame, so this
// should never happen, but in case there are some pending transaction we
// need to flush them here such that Fragment#isAdded checks reflect the
// reality and that we don't have enqueued fragment add commands that will
// execute shortly and cause "Fragment already added" crash.
mFragmentManager?.executePendingTransactions()
performUpdate()
notifyContainerUpdate()
}

protected open fun performUpdate() {
protected open fun onUpdate() {
// detach screens that are no longer active
val orphaned: MutableSet<Fragment> = HashSet(requireNotNull(mFragmentManager, { "mFragmentManager is null when performing update in ScreenContainer" }).fragments)
for (screenFragment in mScreenFragments) {
Expand Down Expand Up @@ -372,7 +312,6 @@ open class ScreenContainer<T : ScreenFragment>(context: Context?) : ViewGroup(co
}
screenFragment.screen.setTransitioning(transitioning)
}
tryCommitTransaction()
}

protected open fun notifyContainerUpdate() {
Expand Down
10 changes: 5 additions & 5 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
var goingForward = false
fun dismiss(screenFragment: ScreenStackFragment) {
mDismissed.add(screenFragment)
markUpdated()
performUpdate()
}

override val topScreen: Screen?
Expand Down Expand Up @@ -128,7 +128,7 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
return super.hasScreen(screenFragment) && !mDismissed.contains(screenFragment)
}

override fun performUpdate() {
override fun onUpdate() {

// When going back from a nested stack with a single screen on it, we may hit an edge case
// when all screens are dismissed and no screen is to be displayed on top. We need to gracefully
Expand Down Expand Up @@ -185,7 +185,7 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
stackAnimation = mTopScreen?.screen?.stackAnimation
}

getOrCreateTransaction().let {
createTransaction().let {
// animation logic start
if (stackAnimation != null) {
if (shouldUseOpenAnimation) {
Expand Down Expand Up @@ -278,7 +278,7 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
mTopScreen = newTop
mStack.clear()
mStack.addAll(mScreenFragments)
tryCommitTransaction()
it.commitNowAllowingStateLoss()
mTopScreen?.let { screen -> setupBackHandlerIfNeeded(screen) }
}
}
Expand Down Expand Up @@ -338,7 +338,7 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
.show(topScreen)
.addToBackStack(BACK_STACK_TAG)
.setPrimaryNavigationFragment(topScreen)
.commitAllowingStateLoss()
.commitNowAllowingStateLoss()
it.addOnBackStackChangedListener(mBackStackListener)
}
}
Expand Down