iT邦幫忙

2023 iThome 鐵人賽

DAY 5
1
自我挑戰組

React Native 奇幻之旅系列 第 5

【DAY5】RN內建組件(4)-FlatList

  • 分享至 

  • xImage
  •  

這篇分享的是 FlatList 使用上常常會遇到的問題和解決方式。

The same orientation warning

VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing and other functionality - use another VirtualizedList-backed container instead.

這個警告是在說同個組件中不能有多個相同方向的 VirtualizedLists 同時存在(如 FlatList 或 SectionList)

比如下面這個例子就會出現這個警告:

<ScrollView contentContainerStyle={styles.container}>
  <Text>O.O</Text>
  <FlatList
    data={list}
    renderItem={({ item }) => <Text>{item.title}</Text>}
    keyExtractor={({ id }) => id.toString()}
  />
  //...
</ScrollView>

解決方法

  • 將除了 FlatList 以外的內容放在 ListHeaderComponent, ListFooterComponent
    <FlatList
        ListHeaderComponent={
           <Text>O.O</Text>
         }
        data={list}
        renderItem={({ item }) => <Text>{item.title}</Text>}
        keyExtractor={({ id }) => id.toString()}
    />
    
  • 用 map 取代 FlatList
    • 應該是最常見的解決方式
    <ScrollView contentContainerStyle={styles.container}>
      <Text>O.O</Text>
      {list.map(({ id, title }) => (
        <Text key={id}>{title}</Text>
      ))}
      //...
    </ScrollView>
    
  • 雙層 ScrollView 法
    • 為了消除警告無所不用其極XD
    <ScrollView contentContainerStyle={styles.container}>
      <Text>O.O</Text>
      <ScrollView horizontal={true} contentContainerStyle={{ width: '100%', height: '100%' }}>
        <FlatList
          data={list}
          renderItem={({ item }) => <Text>{item.title}</Text>}
          keyExtractor={({ id }) => id.toString()}
        />
      </ScrollView>
    </ScrollView>
    
  1. 眼不見為淨法(x)
    import { LogBox } from 'react-native';
    LogBox.ignoreLogs(['VirtualizedLists should never be nested'])
    

Infinite scroll

若要實現滾動加載資料, 需要用到 FlatList 提供的這兩個屬性:

  • onEndReached:滾動到底部時觸發的函數
  • onEndReachedThreshold:距離底部還有多遠時觸發 onEndReached (是一個比值),設為 0.2 則表示距離內容最底部為當前列表可見長度的 20% 時觸發 onEndReached

這邊以使用 https://picsum.photos/v2/list?page=${page}&limit=${limit} API 為例,每次滾動到底部時就會調用 fetchMoreData 函數,並調用 API 獲取下一頁的數據。

import { useState } from "react"
import { StyleSheet, View, FlatList, Text, ScrollView, Image } from "react-native"

interface ImageProps {
  id: string
  author: string
  width: number
  height: number
  url: string
  download_url: string
}

export const ListPage = () => {
  const [data, setData] = useState<ImageProps[] | []>([])
  const [page, setPage] = useState(0)

  const fetchMoreData = () => {
    fetch(`https://picsum.photos/v2/list?page=${page}&limit=10`)
      .then(res => res.json())
      .then(res => {
        setData(prev => ([...prev, ...res]))
        setPage(page + 1)
      })
  }

  return (
      <View style={styles.list}>
        <FlatList
            contentContainerStyle={{ flexGrow: 1 }}
            data={data}
            ItemSeparatorComponent={() => <View style={styles.divider} />}
            renderItem={({ item }) => <Image source={{ uri: item.download_url }} style={{ width: 100, height: 100 }} />}
            keyExtractor={({ id }) => id}
            onEndReachedThreshold={0.2}
            onEndReached={fetchMoreData}
        />
      </View>
  )
}

const styles = StyleSheet.create({
  list: {
    width: 300,
    height: 200,
    backgroundColor: 'white'
  },
  divider: {
    height: 1,
    backgroundColor: 'black'
  }
})

當然這只是最基本的寫法,因為還需要考慮還有沒有剩餘的資料可以獲取,如果沒有更多的資料就不再繼續調用函數。

onEndReached 總是在 render 時自動觸發

像上面的例子設置了 onEndReachedThreshold 為 0.2,但其實 FlatList 會在剛渲染的時候就自動觸發 onEndReached

const onEndReached = () => {
  console.log('A')
}

return(
    <FlatList
      onEndReachedThreshold={0.2}
      onEndReached={onEndReached}
      {...}
    />
)

解決方法

這是因為在初始渲染時無法得知 FlatList 準確的大小和位置,所以才會誤觸 onEndReached。StackOverflow 上針對這個bug有很多討論,比較常見的解決辦法是當 distanceFromEnd > 0 的時候再去調用 onEndReached 方法:

<FlatList
  onEndReachedThreshold={0.2}
  onEndReached={({ distanceFromEnd }) => {
    if (distanceFromEnd > 0) {
      onEndReached()
    }
  }}
/>

distanceFromEnd 是用戶滾動到列表底部時,列表底部距離可見區域底部的距離。

在 FlatList 初渲染且不知道大小的情況下,distanceFromEnd 可能為 0 或者非常小的值,所以將 distanceFromEnd 設為大於 0 再去調用 onEndReached 就能夠避免在初始渲染時不小心觸發。

或者也可以把 onEndReached 替換為 onMomentumScrollEnd,在用戶停止滾動並且滾動動畫完成後才觸發 onEndReached:

<FlatList
  onEndReachedThreshold={0.2}
  onMomentumScrollEnd={onEndReached}
/>

getItemLayout 屬性

這是 FlatList 的一個屬性,常用於優化。FlatList 需要事先渲染過一次,動態獲取渲染尺寸之後再真正渲染到頁面中,如果事先知道列表中的每一項高度就能使用 getItemLayout 减少一次渲染。

const ITEM_HEIGHT = 40 // 假設每一項高度固定為 40

<FlatList
  // ...
  getItemLayout={(_, index) => (
    {
      length: ITEM_HEIGHT,
      offset: ITEM_HEIGHT * index,
      index
    }
  )}
/>

注意:如果設了 getItemLayout,那麼 renderItem 的高度必須和這個高度一樣,否則加載一段列表後就會出現空白或跑版。

性能對比

同樣的資料 render 耗時:

沒有使用 getItemLayout 使用 getItemLayout
14.3ms 13.7ms

這是測試的程式碼:

import { useState } from "react"
import { StyleSheet, View, FlatList, Text, ScrollView, Image } from "react-native"

interface ImageProps {
  id: string
  author: string
  width: number
  height: number
  url: string
  download_url: string
}

const ITEM_HEIGHT = 100

export const ListPage = () => {
  const [data, setData] = useState<ImageProps[] | []>([])
  const [page, setPage] = useState(0)

  const fetchMoreData = () => {
    fetch(`https://picsum.photos/v2/list?page=${page}&limit=10`)
      .then(res => res.json())
      .then(res => {
        setData(prev => ([...prev, ...res]))
        setPage(page + 1)
      })
  }

  return (
      <View style={styles.list}>
          <FlatList
            contentContainerStyle={{ flexGrow: 1 }}
            data={data}
            ItemSeparatorComponent={() => <View style={styles.divider} />}
            getItemLayout={(_, index) => (
              { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }
            )}
            renderItem={({ item }) => <Image source={{ uri: item.download_url }} style={{ width: 100, height: 100 }} />}
            keyExtractor={({ id }) => id}
            onEndReachedThreshold={0.2}
            onEndReached={fetchMoreData}
          />
      </View>
  )
}

const styles = StyleSheet.create({
  list: {
    width: 300,
    height: 200,
    backgroundColor: 'white'
  },
  divider: {
    height: 1,
    backgroundColor: 'black'
  }
})

優化這種簡單的資料其實效果甚微XD


上一篇
【DAY4】RN內建組件(3)-Modal
下一篇
【DAY6】RN內建組件(5)-Image
系列文
React Native 奇幻之旅31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
w315899212
iT邦新手 5 級 ‧ 2024-05-06 11:42:02

谢谢你帮我解决这个warning,我是来自大陆的RN开发者

我要留言

立即登入留言