iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Mobile Development

30天React Native之旅:從入門到活用系列 第 20

Day 20:React Native的滾動組件與無限滾動

  • 分享至 

  • xImage
  •  

在Instagram、Facebook,或是各種電商APP中,我們都可以在列表中向下滑動來查看更多內容。這種不間斷的滑動體驗就是所謂的「無限滾動」。在手機APP的開發中,因為手機性能有限,所以如何實現這種無限滾動但又確保性能,是一門學問。

ScrollView

我們先從最基本的滾動組件開始,ScrollView這是一個最基礎的滾動組件,它支援橫向及縱向滾動。使用它相當簡單:只需要將子組件置於ScrollView組件的內層。當子組件的高度或寬度超過ScrollView的範圍時,使用者即可透過滑動來查看更多。

ScrollView的缺點是。它會依據列表數量直接渲染所有的列表。如果列表少還好,但是,當列表一多時,就會導致性能問題。

FlatList

為了解決滾動列表性能問題,React Native 2017年推出了高性能列表組件FlatList。
FlatList底層是VirtualizedList,而VirtualizedList底層是ScrollView,
所以VirtualizedList 和 ScrollView 组件中的大部分属性,FlatList 组件也都可以使用。

FlatList和ScrollView最大的區別在於,FlatList是「按需渲染」。例如,當使用者正在看列表中的0至9項,FlatList只會渲染前0至19項。當使用者滑動到第10至19項時,FlatList會預先加載第20至29項,確保使用者瀏覽的流暢性。而當使用者繼續滾動到20至29項時,FlatList會回收、釋放最初的0至9項。

透過這種方式,FlatList可以確保在任何時刻都只渲染一小部分的列表,以節省資源,並優化性能,提供更流暢的使用者體驗。

  • 常用屬性
    • data (array):: 要渲染的資料來源。
    • renderItem (function): 渲染的每個項目。
    • getItemLayout (function): 預先指定每個項目的高度。如果沒有設定,FlatList會根據每個項目的渲染結果動態計算高度,這對性能有一定影響,尤其是當列表很多時。
    • numColumns (number): 指定列表的列數。例如,當你要展示一個網格列表時會用到。預設值為1。
    • onEndReached (function): 這是當用戶滾動到列表底部時調用的函數。
    • refreshing (boolean): 布林值,用於指定列表是否正在刷新。
    • onRefresh (function): 這是當用戶下拉刷新列表時會被調用的函數。
    • ItemSeparatorComponent (component): 這是一個組件,它會在列表項之間渲染,通常用於顯示分隔線。
    • ListEmptyComponent (component): 當列表沒有資料時,這個組件會被渲染。
    • keyExtractor (function): 這個函數用於指定每個列表項的唯一鍵。基本上一定要設的,和性能有關。
    • initialNumToRender (number): 這個屬性允許你指定初次渲染時應該渲染的項目數量。
    • onScroll (function): 當用戶滾動列表時,這個函數會被調用。
    • onEndReachedThreshold (number): 值介於0和1之間,表示當滾動列表接近底部多少時會觸發 onEndReached。例如,0.5表示當滾動到列表底部的50%時就會觸發。

用FlatList實作無限滾動列表

  1. 初始化狀態:
    使用useState初始化所需的狀態,列表資料、頁碼、加載狀態、所有資料加載完畢。

    const [data, setData] = useState([]);
    const [page, setPage] = useState(1);
    const [loading, setLoading] = useState(false);
    const [allDataLoaded, setAllDataLoaded] = useState(false);
    
  2. 加載Function:
    使用fetchData函數來從API加載資料。這裡,我們用一個mock API jsonplaceholder來模擬。

    const PAGE_SIZE = 10; // 一次10筆
    const fetchData = async (pageNum) => {
        // 設置加載狀態為true,以顯示加載中的UI提示
        setLoading(true);
    
        // 根據當前的頁碼計算請求API要帶的資料範圍
        const start = (pageNum - 1) * PAGE_SIZE;
        const end = start + PAGE_SIZE;
    
        // 發起API請求,這裡使用的是jsonplaceholder mock API
        const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_start=${start}&_end=${end}`);
        const result = await response.json();
    
        // 關閉加載狀態
        setLoading(false);
    
        if (result && result.length > 0) {
            // 如果有資料,則將新資料加到當前資料的後面
            setData(prevData => [...prevData, ...result]);
    
            // 增加頁碼,以供下次加載時使用
            setPage(pageNum + 1);
        } else {
            // 如果返回的資料為空,則設置allDataLoaded為true,表示所有資料已經加載完畢
            setAllDataLoaded(true);
        }
    };
    
    
  3. 初始資料加載
    在useEffect加載初次資料:

    useEffect(() => {
        fetchData(1);
    }, []);
    
  4. 加載更多:
    寫一個handleLoadMore Function,當用戶滾動到列表底部時觸發

    const handleLoadMore = useCallback(() => {
        // 如果現在沒有在加載資料且還有更多資料可以加載
        if (!loading && !allDataLoaded) {
            // 加載下一頁
            fetchData(page);
        }
        // 使用 `useCallback` 確保這個function在組件更新時保持不變,,
        // 除非 `loading`, `allDataLoaded`, 或 `page` 這些值有所改變。
    }, [loading, allDataLoaded, page]);
    
    
  5. 底部渲染處理:
    寫一個renderFooter Function來處理我們要在底部顯示的:加載中動畫、無更多資料的訊息。

    const renderFooter = () => {
        if (allDataLoaded) {
            return <Text style={styles.infoText}>沒有更多資料了</Text>;
        }
    
        return loading ? <ActivityIndicator size="large" color="#0000ff" /> : null;
    };
    
  6. FlatList:

    <FlatList
        data={data}  // 要顯示的資料
        renderItem={({ item }) => <Text style={styles.itemText}>{item.title}</Text>}
        keyExtractor={(item) => item.id.toString()}  
        onEndReached={handleLoadMore}  // 當用戶滾動到列表的底部時觸發的Function
        onEndReachedThreshold={0.2}  // 在用戶滾動到距離底部20%時就觸發 `onEndReached`
        ListFooterComponent={renderFooter}  // 在列表的底部渲染的組件
        ListEmptyComponent={<Text style={styles.infoText}>無資料</Text>}  // 當 `data` 為空時要渲染的組件
    />
    
    

完成!
cKutXU3

完整代碼

import React, { useState, useEffect, useCallback } from 'react';
import { FlatList, View, Text, ActivityIndicator, StyleSheet } from 'react-native';

const PAGE_SIZE = 10;

const Home = () => {
    const [data, setData] = useState([]);
    const [page, setPage] = useState(1);
    const [loading, setLoading] = useState(false);
    const [allDataLoaded, setAllDataLoaded] = useState(false);

    const fetchData = async (pageNum) => {
        setLoading(true);
        const start = (pageNum - 1) * PAGE_SIZE;
        const end = start + PAGE_SIZE;

        const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_start=${start}&_end=${end}`);
        const result = await response.json();

        setLoading(false);

        if (result && result.length > 0) {
            setData(prevData => [...prevData, ...result]);
            setPage(pageNum + 1);
        } else {
            setAllDataLoaded(true);
        }
    };

    useEffect(() => {
        fetchData(1);
    }, []);

    const handleLoadMore = useCallback(() => {
        if (!loading && !allDataLoaded) {
            fetchData(page);
        }
    }, [loading, allDataLoaded, page]);

    const renderFooter = () => {
        if (allDataLoaded) {
            return <Text style={styles.infoText}>沒有更多資料了</Text>;
        }

        return loading ? <ActivityIndicator size="large" color="#0000ff" /> : null;
    };

    return (
        <FlatList
            data={data}
            renderItem={({ item }) => <Text style={styles.itemText}>{item.title}</Text>}
            keyExtractor={(item) => item.id.toString()}
            onEndReached={handleLoadMore}
            onEndReachedThreshold={0.1}
            ListFooterComponent={renderFooter}
            ListEmptyComponent={<Text style={styles.infoText}>無資料</Text>}
        />
    );
};

const styles = StyleSheet.create({
    itemText: {
        padding: 10,
        borderBottomColor: '#ccc',
        borderBottomWidth: 1,
    },
    infoText: {
        padding: 10,
        textAlign: 'center',
        color: '#888'
    }
});

export default Home;

RecyclerListView

RecyclerListView是社群開發的列表套件,與 FlatList 一樣採「按需渲染」的方式,兩者主要差別在於處理不使用元件上的策略。

在 FlatList 中,當列表項超出可視區時,它會被回收釋放,而新的列表項則會被創建。這種頻繁的新增、刪除,記憶體需要頻繁的分配和釋放,會有較高的計算和資源開銷,尤其在快速滾動時,可能會有卡頓、掉幀的現象。

而 RecyclerListView 則採用一種「重用策略」。當列表項不在可視區時,不將它刪除,而是將它重定向到新的位置。因此,與 FlatList 的新增、刪除相比,RecyclerListView 只需要調整和重用已存在的元件,大幅降低了計算量和資源開銷,所以性能上會比FlatList更勝一籌。

對RecyclerListView有興趣也可以看看作者Blog的介紹:RecyclerListView: High performance ListView for React Native and Web

小結

在移動設備上,如何在確保流暢的滑動體驗的同時避免性能瓶頸,是許多APP開發者都會遇到問題。

從基本的ScrollView到FlatList,再到社群開發的RecyclerListView,我們可以看到不同的渲染策略和性能優化的方法。FlatList按需渲染、回收創建,以及RecyclerListView的元件重用策略,都是解決移動應用中列表滾動性能問題的一種方法。

選擇哪一種方式實現滾動列表,取決於具體的需求。如果是簡單的短列表,ScrollView就夠用了。如果是大量資料和需要無限滾動功能的場景,FlatList和RecyclerListView會是更好的選擇。總之,瞭解這些工具和它們的原理,可以幫助我們做出更合適的技術選擇,打造出高性能的APP。

Ref

https://juejin.cn/post/7252684645979242533?searchId=2023100415544745D0A35A9BDD9116FB2F


上一篇
Day 19:PixelRatio的使用
下一篇
Day 21:URL Scheme與Deeplink
系列文
30天React Native之旅:從入門到活用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言