Skip to content

Commit e27fb65

Browse files
ramya-2196facebook-github-bot
authored andcommitted
Render multi-column FlatList as a grid behind a feature flag
Summary: With `numColumns > 1`, `FlatList` groups items into one `VirtualizedList` cell per row and keys each row by the keys of its items. Adding or removing an item moves every later item into a different row, so React remounts the items of every later row and they lose their state (#45895). Behind the new JS-only feature flag `enableFlatListGridLayout`, `FlatList` instead passes items straight through and lets `VirtualizedList` lay them out with `experimental_numColumns`. Each item is its own cell keyed by `keyExtractor`, so no item remounts when another item is added or removed, including items that move to another row, and `onViewableItemsChanged` keeps reporting the right items. Existing behaviour is kept: - `getItemLayout`, `initialScrollIndex`, `scrollToIndex` and `maintainVisibleContentPosition.minIndexForVisible` still take row indices; `FlatList` converts them to and from the item indices of the grid. - Lists that use `columnWrapperStyle`, `ItemSeparatorComponent`, `CellRendererComponent` or `stickyHeaderIndices` keep row cells, as these depend on one cell per row. So does Windows, whose `VirtualizedList` fork does not support grids. - One visible difference: an item alone in a partially filled last row keeps its column width instead of being stretched across the whole row by `flex: 1`. Changelog: [General][Added] - Add `enableFlatListGridLayout` feature flag to render multi-column `FlatList` as a grid of item cells, so items no longer remount when other items are added or removed Differential Revision: D124984958
1 parent bd37d26 commit e27fb65

4 files changed

Lines changed: 362 additions & 22 deletions

File tree

‎packages/react-native/Libraries/Lists/FlatList.js‎

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,14 @@ class FlatList<ItemT = any> extends React.PureComponent<FlatListProps<ItemT>> {
333333
...
334334
}) {
335335
if (this._listRef) {
336-
this._listRef.scrollToIndex(params);
336+
this._listRef.scrollToIndex(
337+
this._usesGridLayout()
338+
? {
339+
...params,
340+
index: params.index * numColumnsOrDefault(this.props.numColumns),
341+
}
342+
: params,
343+
);
337344
}
338345
}
339346

@@ -518,15 +525,78 @@ class FlatList<ItemT = any> extends React.PureComponent<FlatListProps<ItemT>> {
518525
);
519526
}
520527

528+
// With the grid layout, VirtualizedList lays out one cell per item.
529+
// Features that depend on one cell per row keep the row layout.
530+
_usesGridLayout(): boolean {
531+
const {
532+
CellRendererComponent,
533+
ItemSeparatorComponent,
534+
columnWrapperStyle,
535+
stickyHeaderIndices,
536+
} = this.props;
537+
return (
538+
numColumnsOrDefault(this.props.numColumns) > 1 &&
539+
ReactNativeFeatureFlags.enableFlatListGridLayout() &&
540+
// The Windows fork of VirtualizedList does not support grids.
541+
(Platform.OS as string) !== 'windows' &&
542+
columnWrapperStyle == null &&
543+
ItemSeparatorComponent == null &&
544+
CellRendererComponent == null &&
545+
(stickyHeaderIndices == null || stickyHeaderIndices.length === 0)
546+
);
547+
}
548+
549+
_itemsPerCell(): number {
550+
return this._usesGridLayout()
551+
? 1
552+
: numColumnsOrDefault(this.props.numColumns);
553+
}
554+
555+
// Indices passed to `getItemLayout`, `initialScrollIndex` and
556+
// `maintainVisibleContentPosition` refer to rows when `numColumns > 1`, so
557+
// they are converted to and from the item indices used by the grid.
558+
_getGridLayoutProps: (
559+
numColumns: number,
560+
getItemLayout: FlatListProps<ItemT>['getItemLayout'],
561+
initialScrollIndex: FlatListProps<ItemT>['initialScrollIndex'],
562+
maintainVisibleContentPosition: FlatListProps<ItemT>['maintainVisibleContentPosition'],
563+
) => Partial<VirtualizedListProps> = memoizeOne(
564+
(
565+
numColumns,
566+
getItemLayout,
567+
initialScrollIndex,
568+
maintainVisibleContentPosition,
569+
) => ({
570+
experimental_numColumns: numColumns,
571+
getItemLayout:
572+
getItemLayout == null
573+
? undefined
574+
: (data, itemIndex) =>
575+
getItemLayout(data, Math.floor(itemIndex / numColumns)),
576+
initialScrollIndex:
577+
initialScrollIndex == null
578+
? initialScrollIndex
579+
: initialScrollIndex * numColumns,
580+
maintainVisibleContentPosition:
581+
maintainVisibleContentPosition == null
582+
? maintainVisibleContentPosition
583+
: {
584+
...maintainVisibleContentPosition,
585+
minIndexForVisible:
586+
maintainVisibleContentPosition.minIndexForVisible * numColumns,
587+
},
588+
}),
589+
);
590+
521591
_getItem = (
522592
data: $ArrayLike<ItemT>,
523593
index: number,
524594
): ?(ItemT | ReadonlyArray<ItemT>) => {
525-
const numColumns = numColumnsOrDefault(this.props.numColumns);
526-
if (numColumns > 1) {
595+
const itemsPerCell = this._itemsPerCell();
596+
if (itemsPerCell > 1) {
527597
const ret = [];
528-
for (let kk = 0; kk < numColumns; kk++) {
529-
const itemIndex = index * numColumns + kk;
598+
for (let kk = 0; kk < itemsPerCell; kk++) {
599+
const itemIndex = index * itemsPerCell + kk;
530600
if (itemIndex < data.length) {
531601
const item = data[itemIndex];
532602
ret.push(item);
@@ -546,27 +616,29 @@ class FlatList<ItemT = any> extends React.PureComponent<FlatListProps<ItemT>> {
546616
// available to prevent it from trying to read from the invalid data
547617
// (without propagating invalidly typed data).
548618
if (data != null && isArrayLike(data)) {
549-
const numColumns = numColumnsOrDefault(this.props.numColumns);
550-
return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
619+
const itemsPerCell = this._itemsPerCell();
620+
return itemsPerCell > 1
621+
? Math.ceil(data.length / itemsPerCell)
622+
: data.length;
551623
} else {
552624
return 0;
553625
}
554626
};
555627

556628
_keyExtractor = (items: ItemT | Array<ItemT>, index: number): string => {
557-
const numColumns = numColumnsOrDefault(this.props.numColumns);
629+
const itemsPerCell = this._itemsPerCell();
558630
const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor;
559631

560-
if (numColumns > 1) {
632+
if (itemsPerCell > 1) {
561633
invariant(
562634
Array.isArray(items),
563635
'FlatList: Encountered internal consistency error, expected each item to consist of an ' +
564636
'array with 1-%s columns; instead, received a single item.',
565-
numColumns,
637+
itemsPerCell,
566638
);
567639
return items
568640
.map((item, kk) =>
569-
keyExtractor(item as $FlowFixMe as ItemT, index * numColumns + kk),
641+
keyExtractor(item as $FlowFixMe as ItemT, index * itemsPerCell + kk),
570642
)
571643
.join(':');
572644
}
@@ -576,11 +648,11 @@ class FlatList<ItemT = any> extends React.PureComponent<FlatListProps<ItemT>> {
576648
};
577649

578650
_pushMultiColumnViewable(arr: Array<ListViewToken>, v: ListViewToken): void {
579-
const numColumns = numColumnsOrDefault(this.props.numColumns);
651+
const itemsPerCell = this._itemsPerCell();
580652
const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor;
581653
v.item.forEach((item, ii) => {
582654
invariant(v.index != null, 'Missing index!');
583-
const index = v.index * numColumns + ii;
655+
const index = v.index * itemsPerCell + ii;
584656
arr.push({...v, item, key: keyExtractor(item, index), index});
585657
});
586658
}
@@ -598,9 +670,8 @@ class FlatList<ItemT = any> extends React.PureComponent<FlatListProps<ItemT>> {
598670
changed: Array<ListViewToken>,
599671
...
600672
}) => {
601-
const numColumns = numColumnsOrDefault(this.props.numColumns);
602673
if (onViewableItemsChanged) {
603-
if (numColumns > 1) {
674+
if (this._itemsPerCell() > 1) {
604675
const changed: Array<ListViewToken> = [];
605676
const viewableItems: Array<ListViewToken> = [];
606677
info.viewableItems.forEach(v =>
@@ -684,11 +755,20 @@ class FlatList<ItemT = any> extends React.PureComponent<FlatListProps<ItemT>> {
684755
} = this.props;
685756

686757
const renderer = strictMode ? this._memoizedRenderer : this._renderer;
758+
const gridLayoutProps = this._usesGridLayout()
759+
? this._getGridLayoutProps(
760+
numColumnsOrDefault(numColumns),
761+
restProps.getItemLayout,
762+
restProps.initialScrollIndex,
763+
restProps.maintainVisibleContentPosition,
764+
)
765+
: null;
687766

688767
return (
689768
// $FlowFixMe[incompatible-exact] - `restProps` (`Props`) is inexact.
690769
<VirtualizedList
691770
{...restProps}
771+
{...gridLayoutProps}
692772
getItem={this._getItem}
693773
getItemCount={this._getItemCount}
694774
keyExtractor={this._keyExtractor}
@@ -701,7 +781,7 @@ class FlatList<ItemT = any> extends React.PureComponent<FlatListProps<ItemT>> {
701781
this.props.ListItemComponent,
702782
this.props.renderItem,
703783
columnWrapperStyle,
704-
numColumns,
784+
this._itemsPerCell(),
705785
this.props.extraData,
706786
)}
707787
/>

0 commit comments

Comments
 (0)