Skip to content

Commit 8b78938

Browse files
[MOO-2184] Migrate to FlashList (#487)
2 parents f700c69 + e0a14e1 commit 8b78938

19 files changed

Lines changed: 1016 additions & 1775 deletions

File tree

packages/pluggableWidgets/gallery-native/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- We migrated from using the native FlatList to @shopify/flash-list.
12+
913
## [2.0.2] - 2025-10-17
1014

1115
### Fixed

packages/pluggableWidgets/gallery-native/e2e/specs/maestro/Gallery_native.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ appId: "${APP_ID}"
2020
direction: UP
2121
- assertVisible:
2222
text: ".*Title 9.*"
23+
- tapOn:
24+
text: "Load more"
25+
- assertVisible:
26+
text: ".*Title 10.*"
27+
- swipe:
28+
from:
29+
text: ".*Title 10.*"
30+
direction: UP
31+
- assertVisible:
32+
text: ".*Title 14.*"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
appId: "${APP_ID}"
2+
---
3+
- runFlow:
4+
file: "../../../../../../maestro/Precondition.yaml"
5+
- tapOn:
6+
text: "G"
7+
- tapOn:
8+
text: "Gallery"
9+
- tapOn:
10+
text: "Gallery"
11+
- assertVisible:
12+
text: ".*Title 0.*"
13+
- repeat:
14+
times: 25
15+
commands:
16+
- swipe:
17+
start: 90%, 10%
18+
end: 15%, 10%
19+
- assertVisible:
20+
text: ".*Title 20.*"

packages/pluggableWidgets/gallery-native/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "gallery-native",
33
"widgetName": "Gallery",
4-
"version": "2.0.2",
4+
"version": "2.1.0",
55
"description": "A flexible gallery widget that renders columns, rows and layouts.",
66
"copyright": "© Mendix Technology BV 2022. All rights reserved.",
77
"license": "Apache-2.0",
@@ -23,6 +23,7 @@
2323
},
2424
"dependencies": {
2525
"@mendix/piw-utils-internal": "*",
26+
"@shopify/flash-list": "2.2.2",
2627
"react-native-device-info": "14.0.4"
2728
},
2829
"devDependencies": {

packages/pluggableWidgets/gallery-native/src/Gallery.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const Gallery = (props: GalleryProps<GalleryStyle>): ReactElement => {
4949
}),
5050
{}
5151
),
52+
// eslint-disable-next-line react-hooks/exhaustive-deps
5253
[props.filterList, viewStateFilters.current]
5354
);
5455

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from "react";
2+
import { View } from "react-native";
3+
4+
// Mock FlashList - render items directly without using FlatList
5+
export const FlashList = React.forwardRef((props: any) => {
6+
const {
7+
data = [],
8+
renderItem,
9+
ListEmptyComponent,
10+
ListFooterComponent,
11+
ListHeaderComponent,
12+
onRefresh,
13+
...rest
14+
} = props;
15+
16+
// simulate the refreshControl structure returned by real FlashList
17+
const refreshControl = onRefresh ? { props: { onRefresh } } : undefined;
18+
19+
const renderItems = (): any => {
20+
if (!data || data.length === 0) {
21+
return ListEmptyComponent;
22+
}
23+
24+
return data.map((item: any, index: number) => (
25+
<View key={item.id || index}>{renderItem?.({ item, index })}</View>
26+
));
27+
};
28+
29+
return (
30+
<View {...rest} refreshControl={refreshControl}>
31+
{ListHeaderComponent}
32+
{renderItems()}
33+
{ListFooterComponent}
34+
</View>
35+
);
36+
});
37+
38+
FlashList.displayName = "FlashList";

packages/pluggableWidgets/gallery-native/src/components/Gallery.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { ReactElement, ReactNode, useCallback } from "react";
2-
import { Text, FlatList, Pressable, View, ViewProps, Platform, TouchableOpacity } from "react-native";
1+
import { ReactElement, ReactNode, useCallback, useMemo } from "react";
2+
import { Text, Pressable, View, ViewProps, Platform, TouchableOpacity, useWindowDimensions } from "react-native";
33
import { ObjectItem, DynamicValue } from "mendix";
44
import DeviceInfo from "react-native-device-info";
55
import { GalleryStyle } from "../ui/Styles";
66
import { PaginationEnum, ScrollDirectionEnum } from "../../typings/GalleryProps";
77
import { isAvailable } from "@mendix/piw-utils-internal";
88
import { extractStyles } from "@mendix/pluggable-widgets-tools";
9+
import { FlashList } from "@shopify/flash-list";
910

1011
const DEFAULT_RIPPLE_COLOR = "rgba(0, 0, 0, 0.2)";
1112

@@ -33,6 +34,7 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
3334
const firstItemId = props.items?.[0]?.id;
3435
const lastItemId = props.items?.[props.items.length - 1]?.id;
3536
const { name, style, itemRenderer } = props;
37+
const { width } = useWindowDimensions();
3638

3739
const onEndReached = (): void => {
3840
if (props.pagination === "virtualScrolling" && props.hasMoreItems) {
@@ -43,8 +45,9 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
4345
const renderItem = useCallback(
4446
(item: { item: T }): ReactElement =>
4547
itemRenderer((children, onPress) => {
48+
const itemStyle = isScrollDirectionVertical ? undefined : { width };
4649
const listItemWrapperProps: ViewProps = {
47-
style: isScrollDirectionVertical && { width: `${100 / numColumns}%` },
50+
style: itemStyle,
4851
testID: `${name}-list-item-${item.item.id}`
4952
};
5053
const renderListItemContent = (
@@ -67,9 +70,9 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
6770
);
6871
}, item.item),
6972
[
70-
isScrollDirectionVertical,
71-
numColumns,
7273
itemRenderer,
74+
isScrollDirectionVertical,
75+
width,
7376
name,
7477
style.listItem,
7578
style.firstItem,
@@ -79,7 +82,7 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
7982
]
8083
);
8184

82-
const loadMoreButton = (): ReactElement | null => {
85+
const loadMoreButton = useMemo((): ReactElement | null => {
8386
const renderButton = (
8487
<Text style={props.style.loadMoreButtonCaption}>
8588
{props.loadMoreButtonCaption && isAvailable(props.loadMoreButtonCaption)
@@ -118,16 +121,26 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
118121
<TouchableOpacity {...buttonProps}>{renderButton}</TouchableOpacity>
119122
)
120123
) : null;
121-
};
124+
// eslint-disable-next-line react-hooks/exhaustive-deps
125+
}, [
126+
props.style.loadMoreButtonCaption,
127+
props.loadMoreButtonCaption,
128+
props.style.loadMoreButtonPressableContainer,
129+
name,
130+
props.pagination,
131+
props.hasMoreItems,
132+
props.loadMoreItems
133+
]);
122134

123-
const renderEmptyPlaceholder = (): ReactElement => (
124-
<View style={props.style.emptyPlaceholder}>{props.emptyPlaceholder}</View>
135+
const renderEmptyPlaceholder = useMemo(
136+
(): ReactElement => <View style={props.style.emptyPlaceholder}>{props.emptyPlaceholder}</View>,
137+
[props.style.emptyPlaceholder, props.emptyPlaceholder]
125138
);
126139

127140
return (
128141
<View testID={`${name}`} style={props.style.container}>
129142
{props.filters ? <View>{props.filters}</View> : null}
130-
<FlatList
143+
<FlashList
131144
{...(isScrollDirectionVertical && props.pullDown ? { onRefresh: props.pullDown } : {})}
132145
{...(isScrollDirectionVertical ? { numColumns } : {})}
133146
ListFooterComponent={loadMoreButton}

packages/pluggableWidgets/gallery-native/src/components/__tests__/Gallery.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { render, fireEvent, act } from "@testing-library/react-native";
77
import { Gallery, GalleryProps } from "../Gallery";
88

99
jest.mock("react-native-device-info", () => ({ isTablet: jest.fn().mockReturnValue(false) }));
10+
jest.mock("@shopify/flash-list"); // Mocking FlashList API because it causes issues with the test renderer
1011

1112
const itemWrapperFunction =
1213
({ onClick }: { onClick?: () => void }): GalleryProps<ObjectItem>["itemRenderer"] =>

0 commit comments

Comments
 (0)