diff --git a/Example/App.tsx b/Example/App.tsx
index e7dda40..51d6546 100644
--- a/Example/App.tsx
+++ b/Example/App.tsx
@@ -2,6 +2,9 @@ import React from 'react';
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import ScrollViewExample from './src/ScrollViewExample';
+// @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import SectionListExample from './src/SectionListExample';
import FlatListExample from './src/FlatListExample';
const App = () => {
diff --git a/Example/src/SectionListExample.tsx b/Example/src/SectionListExample.tsx
new file mode 100644
index 0000000..13b47e8
--- /dev/null
+++ b/Example/src/SectionListExample.tsx
@@ -0,0 +1,147 @@
+import React, {useState} from 'react';
+import {
+ View,
+ TouchableOpacity,
+ Text,
+ SafeAreaView,
+ StyleSheet,
+ SectionListData,
+} from 'react-native';
+import {SectionList} from '@stream-io/flat-list-mvcp';
+
+type Item = {
+ id: string;
+ value: number;
+};
+
+const AddMoreButton = ({onPress}: {onPress: () => void}) => (
+
+ Add 5 items from this side
+
+);
+
+const ListItem = ({item}: {item: Item}) => (
+
+ List item: {item.value}
+
+);
+
+const ListHeader = ({
+ section,
+}: {
+ section: SectionListData- ;
+}) => (
+
+ Title: {section.title}
+
+);
+
+// Generate unique key list item.
+export const generateUniqueKey = () =>
+ `_${Math.random().toString(36).substr(2, 9)}`;
+
+const SectionListExample = () => {
+ const [sections, setSections] = useState([
+ {
+ title: 'Section 0 to 4',
+ data: Array.from(Array(5).keys()).map((n) => ({
+ id: generateUniqueKey(),
+ value: n,
+ })),
+ },
+ {
+ title: 'Section 5 to 9',
+ data: Array.from(Array(5).keys()).map((n) => ({
+ id: generateUniqueKey(),
+ value: n + 5,
+ })),
+ },
+ ]);
+
+ const addToEnd = () => {
+ setSections((prev) => {
+ const additionalSection = {
+ title: `Section ${prev.length * 5} to ${prev.length * 5 + 4}`,
+ data: Array.from(Array(5).keys()).map((n) => ({
+ id: generateUniqueKey(),
+ value: n + prev.length * 5,
+ })),
+ };
+
+ return prev.concat(additionalSection);
+ });
+ };
+
+ const addToStart = () => {
+ setSections((prev) => {
+ const additionalSection = {
+ title: `Section ${prev[0].data[0].value - 5} to ${
+ prev[0].data[0].value - 1
+ }`,
+ data: Array.from(Array(5).keys())
+ .map((n) => ({
+ id: generateUniqueKey(),
+ value: prev[0].data[0].value - n - 1,
+ }))
+ .reverse(),
+ };
+
+ return [additionalSection].concat(prev);
+ });
+ };
+
+ return (
+
+
+
+ item.id}
+ maintainVisibleContentPosition={{
+ minIndexForVisible: 1,
+ }}
+ renderItem={ListItem}
+ renderSectionHeader={ListHeader}
+ />
+
+
+
+ );
+};
+
+export default SectionListExample;
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ },
+ addMoreButton: {
+ padding: 8,
+ backgroundColor: '#008CBA',
+ alignItems: 'center',
+ },
+ addMoreButtonText: {
+ color: 'white',
+ },
+ listContainer: {
+ paddingVertical: 4,
+ flexGrow: 1,
+ flexShrink: 1,
+ backgroundColor: 'black',
+ },
+ listItem: {
+ flex: 1,
+ padding: 32,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 8,
+ backgroundColor: 'white',
+ },
+ listTitle: {
+ flex: 1,
+ padding: 16,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'grey',
+ },
+});
diff --git a/src/SectionList.android.tsx b/src/SectionList.android.tsx
new file mode 100644
index 0000000..c269f6e
--- /dev/null
+++ b/src/SectionList.android.tsx
@@ -0,0 +1,137 @@
+import React, { MutableRefObject, useEffect, useRef } from 'react';
+import {
+ DefaultSectionT,
+ NativeModules,
+ Platform,
+ SectionList as RNSectionList,
+ SectionListProps as RNSectionListProps,
+} from 'react-native';
+
+export const ScrollViewManager = NativeModules.MvcpScrollViewManager;
+
+export default React.forwardRef(
+ (
+ props: RNSectionListProps,
+ forwardedRef:
+ | ((instance: RNSectionList | null) => void)
+ | MutableRefObject | null>
+ | null
+ ) => {
+ const sectionListRef = useRef | null>(null);
+ const isMvcpEnabledNative = useRef(false);
+ const handle = useRef(null);
+ const enableMvcpRetriesCount = useRef(0);
+ const isMvcpPropPresentRef = useRef(!!props.maintainVisibleContentPosition);
+
+ const autoscrollToTopThreshold = useRef(
+ props.maintainVisibleContentPosition?.autoscrollToTopThreshold ||
+ -Number.MAX_SAFE_INTEGER
+ );
+ const minIndexForVisible = useRef(
+ props.maintainVisibleContentPosition?.minIndexForVisible || 1
+ );
+ const retryTimeoutId = useRef();
+ const debounceTimeoutId = useRef();
+ const disableMvcpRef = useRef(async () => {
+ isMvcpEnabledNative.current = false;
+ if (!handle?.current) {
+ return;
+ }
+ await ScrollViewManager.disableMaintainVisibleContentPosition(
+ handle.current
+ );
+ });
+ const enableMvcpWithRetriesRef = useRef(() => {
+ // debounce to wait till consecutive mvcp enabling
+ // this ensures that always previous handles are disabled first
+ if (debounceTimeoutId.current) {
+ clearTimeout(debounceTimeoutId.current);
+ }
+ debounceTimeoutId.current = setTimeout(async () => {
+ // disable any previous enabled handles
+ await disableMvcpRef.current();
+
+ if (
+ !sectionListRef.current ||
+ !isMvcpPropPresentRef.current ||
+ isMvcpEnabledNative.current ||
+ Platform.OS !== 'android'
+ ) {
+ return;
+ }
+ const scrollableNode = sectionListRef.current.getScrollableNode();
+
+ try {
+ handle.current = await ScrollViewManager.enableMaintainVisibleContentPosition(
+ scrollableNode,
+ autoscrollToTopThreshold.current,
+ minIndexForVisible.current
+ );
+ } catch (error: any) {
+ /**
+ * enableMaintainVisibleContentPosition from native module may throw IllegalViewOperationException,
+ * in case view is not ready yet. In that case, lets do a retry!! (max of 10 tries)
+ */
+ if (enableMvcpRetriesCount.current < 10) {
+ retryTimeoutId.current = setTimeout(
+ enableMvcpWithRetriesRef.current,
+ 100
+ );
+ enableMvcpRetriesCount.current += 1;
+ }
+ }
+ }, 300);
+ });
+
+ useEffect(() => {
+ // when the mvcp prop changes
+ // enable natively again, if the prop has changed
+ const propAutoscrollToTopThreshold =
+ props.maintainVisibleContentPosition?.autoscrollToTopThreshold ||
+ -Number.MAX_SAFE_INTEGER;
+ const propMinIndexForVisible =
+ props.maintainVisibleContentPosition?.minIndexForVisible || 1;
+ const hasMvcpChanged =
+ autoscrollToTopThreshold.current !== propAutoscrollToTopThreshold ||
+ minIndexForVisible.current !== propMinIndexForVisible ||
+ isMvcpPropPresentRef.current !== !!props.maintainVisibleContentPosition;
+
+ if (hasMvcpChanged) {
+ enableMvcpRetriesCount.current = 0;
+ autoscrollToTopThreshold.current = propAutoscrollToTopThreshold;
+ minIndexForVisible.current = propMinIndexForVisible;
+ isMvcpPropPresentRef.current = !!props.maintainVisibleContentPosition;
+ enableMvcpWithRetriesRef.current();
+ }
+ }, [props.maintainVisibleContentPosition]);
+
+ const refCallback = useRef<
+ (instance: RNSectionList) => void
+ >((ref) => {
+ sectionListRef.current = ref;
+ enableMvcpWithRetriesRef.current();
+ if (typeof forwardedRef === 'function') {
+ forwardedRef(ref);
+ } else if (forwardedRef) {
+ forwardedRef.current = ref;
+ }
+ }).current;
+
+ useEffect(() => {
+ const disableMvcp = disableMvcpRef.current;
+ return () => {
+ // clean up the retry mechanism
+ if (debounceTimeoutId.current) {
+ clearTimeout(debounceTimeoutId.current);
+ }
+ // clean up any debounce
+ if (debounceTimeoutId.current) {
+ clearTimeout(debounceTimeoutId.current);
+ }
+ disableMvcp();
+ };
+ }, []);
+
+ return ref={refCallback} {...props} />;
+ }
+);
diff --git a/src/SectionList.tsx b/src/SectionList.tsx
new file mode 100644
index 0000000..116348d
--- /dev/null
+++ b/src/SectionList.tsx
@@ -0,0 +1,3 @@
+import { SectionList } from 'react-native';
+
+export default SectionList;
diff --git a/src/index.ts b/src/index.ts
index 8729b8b..5d52bc6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,3 @@
export { default as FlatList } from './FlatList';
export { default as ScrollView } from './ScrollView';
+export { default as SectionList } from './SectionList';