Бесконечная прокрутка позволяет вашему приложению запрашивать / извлекать данные по мере необходимости. Это означает, что когда пользователь прокручивает список до конца, выполняется дополнительный запрос и добавляются данные.

Нет кнопки «Загрузить еще» или «Следующая страница» , запрос выполняется автоматически в фоновом режиме, откуда и происходит название бесконечной прокрутки. Пользователь может прокручивать без усилий и бесконечно (при условии, что данных достаточно, чтобы продолжать бесконечно). Он загружает их, когда они достигают определенной точки на странице, откуда и происходит термин «ленивая загрузка» - он загружает их только тогда, когда вам это нужно.

2. Зачем нужна бесконечная прокрутка

Такие приложения, как Instagram, Facebook, Twitter, YouTube, Pinterest и Medium, используют бесконечную прокрутку, так почему же ее используют крупные технологические компании? Это отличный пользовательский интерфейс, он эффективно запрашивает только необходимые данные и экономит $$$ затрат на передачу данных. В нашем случае мы будем использовать Firebase Cloud Firestore, поэтому бесконечная прокрутка сэкономит затраты на чтение Firebase.

Например, предположим, что у вас есть набор данных из 100 пользователей, но наше приложение может отображать только 9 пользователей одновременно. Было бы неэффективно запрашивать Cloud Firestore для 100 пользователей, если ваш пользователь никогда не прокручивает первые 9 пользователей. Бесконечная прокрутка делает ваше приложение более эффективным, запрашивая по мере необходимости. Это требует минимальных усилий со стороны пользователя, поскольку требуется меньше шагов, поскольку нет номеров страниц и кнопок, которые можно было бы нажимать для запроса дополнительных данных.

Это не только неэффективно с точки зрения затрат на чтение Firebase и пользовательского опыта, но и неэффективно с точки зрения скорости, поскольку оно отправляет вашему приложению только необходимые данные.

3. Пример приложения + код

Репозиторий на Github: https://github.com/jefelewis/« react-native-infinite-scroll-test

A. Обзор приложения

Приложение будет иметь только один экран (InfiniteScroll.js), и оно будет запрашивать из Firebase Cloud Firestore с ограничением в 9 пользователей. Как только вы прокрутите список до конца, будет загружено еще 9 пользователей (всего 20 пользователей).

Все данные будут отображаться в компоненте React Native FlatList.

Б. Скриншот приложения

C. Файловая структура приложения

В этом примере будут использоваться 4 файла:

  1. data.json (пример данных)
  2. config.js (конфигурация Firebase)
  3. App.js (приложение React Native)
  4. InfiniteScroll.js (экран с бесконечной прокруткой)

D. Файлы приложений

Ниже будет представлен образец файла data.json. Этот data.json нужно будет импортировать в Firebase Cloud Firestore.

Если вы не знаете, как импортировать данные JSON в Firebase Cloud Firestore, проверьте Руководство по импорту JSON в Firestore.

data.json

{
  "users": [
    {
      "id": 1,
      "first_name": "Kristin",
      "last_name": "Smith"
    },
    {
      "id": 2,
      "first_name": "Olivia",
      "last_name": "Parker"
    },
    {
      "id": 3,
      "first_name": "Jimmy",
      "last_name": "Robinson"
    },
    {
      "id": 4,
      "first_name": "Zack",
      "last_name": "Carter"
    },
    {
      "id": 5,
      "first_name": "Brad",
      "last_name": "Rayburn"
    },
    {
      "id": 6,
      "first_name": "Krista",
      "last_name": "Foster"
    },
    {
      "id": 7,
      "first_name": "Parker",
      "last_name": "Trotter"
    },
    {
      "id": 8,
      "first_name": "Kevin",
      "last_name": "Carter"
    },
    {
      "id": 9,
      "first_name": "Fred",
      "last_name": "Klein"
    },
    {
      "id": 10,
      "first_name": "Thomas",
      "last_name": "Manchin"
    },
    {
      "id": 11,
      "first_name": "Taylor",
      "last_name": "Welch"
    },
    {
      "id": 12,
      "first_name": "Sam",
      "last_name": "Goldberg"
    },
    {
      "id": 13,
      "first_name": "John",
      "last_name": "Russell"
    },
    {
      "id": 14,
      "first_name": "Steve",
      "last_name": "Bell"
    },
    {
      "id": 15,
      "first_name": "Kelly",
      "last_name": "Black"
    },
    {
      "id": 16,
      "first_name": "Lena",
      "last_name": "Hunt"
    },
    {
      "id": 17,
      "first_name": "Jessica",
      "last_name": "Moore"
    },
    {
      "id": 18,
      "first_name": "Pete",
      "last_name": "Wong"
    },
    {
      "id": 19,
      "first_name": "Harry",
      "last_name": "Fordham"
    },
    {
      "id": 20,
      "first_name": "Ashley",
      "last_name": "Blake"
    }
  ]
}

config.js

// Firebase Config
const firebaseConfig = {
  apiKey: 'API_KEY_HERE',
  authDomain: 'AUTH_DOMAIN_HERE',
  databaseURL: 'DATABASE_URL_HERE',
  projectId: 'PROJECT_ID_HERE',
  storageBucket: 'STORAGE_BUCKET_HERE',
  messagingSenderId: 'MESSAGING_ID_HERE',
};
// Exports
module.exports = firebaseConfig;

App.js

// Imports: Dependencies
import React from 'react';
import * as firebase from 'firebase';
import 'firebase/firestore';
import firebaseConfig from './config/config';
// Imports: Screens
import InfiniteScroll from './screens/InfiniteScroll';
// Firebase: Initialize
firebase.initializeApp({
  apiKey: firebaseConfig.apiKey,
  authDomain: firebaseConfig.authDomain,
  databaseURL: firebaseConfig.databaseURL,
  projectId: firebaseConfig.projectId,
  storageBucket: firebaseConfig.storageBucket,
  messagingSenderId: firebaseConfig.messagingSenderId,
});
// Firebase: Cloud Firestore
export const database = firebase.firestore();
// React Native: App
export default function App() {
  return (
    <InfiniteScroll />
  );
}

InfiniteScroll.js

// Imports: Dependencies
import React, { Component } from 'react';
import { ActivityIndicator, Dimensions, FlatList, SafeAreaView, StyleSheet, Text, View } from 'react-native';
import { database } from '../App';
// Screen Dimensions
const { height, width } = Dimensions.get('window');
// Screen: Infinite Scroll
export default class InfiniteScroll extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      documentData: [],
      limit: 9,
      lastVisible: null,
      loading: false,
      refreshing: false,
    };
  }
  // Component Did Mount
  componentDidMount = () => {
    try {
      // Cloud Firestore: Initial Query
      this.retrieveData();
    }
    catch (error) {
      console.log(error);
    }
  };
  // Retrieve Data
  retrieveData = async () => {
    try {
      // Set State: Loading
      this.setState({
        loading: true,
      });
      console.log('Retrieving Data');
      // Cloud Firestore: Query
      let initialQuery = await database.collection('users')
        .where('id', '<=', 20)
        .orderBy('id')
        .limit(this.state.limit)
      // Cloud Firestore: Query Snapshot
      let documentSnapshots = await initialQuery.get();
      // Cloud Firestore: Document Data
      let documentData = documentSnapshots.docs.map(document => document.data());
      // Cloud Firestore: Last Visible Document (Document ID To Start From For Proceeding Queries)
      let lastVisible = documentData[documentData.length - 1].id;
      // Set State
      this.setState({
        documentData: documentData,
        lastVisible: lastVisible,
        loading: false,
      });
    }
    catch (error) {
      console.log(error);
    }
  };
  // Retrieve More
  retrieveMore = async () => {
    try {
      // Set State: Refreshing
      this.setState({
        refreshing: true,
      });
      console.log('Retrieving additional Data');
      // Cloud Firestore: Query (Additional Query)
      let additionalQuery = await database.collection('users')
        .where('id', '<=', 20)
        .orderBy('id')
        .startAfter(this.state.lastVisible)
        .limit(this.state.limit)
      // Cloud Firestore: Query Snapshot
      let documentSnapshots = await additionalQuery.get();
      // Cloud Firestore: Document Data
      let documentData = documentSnapshots.docs.map(document => document.data());
      // Cloud Firestore: Last Visible Document (Document ID To Start From For Proceeding Queries)
      let lastVisible = documentData[documentData.length - 1].id;
      // Set State
      this.setState({
        documentData: [...this.state.documentData, ...documentData],
        lastVisible: lastVisible,
        refreshing: false,
      });
    }
    catch (error) {
      console.log(error);
    }
  };
  // Render Header
  renderHeader = () => {
    try {
      return (
        <Text style={styles.headerText}>Items</Text>
      )
    }
    catch (error) {
      console.log(error);
    }
  };
  // Render Footer
  renderFooter = () => {
    try {
      // Check If Loading
      if (this.state.loading) {
        return (
          <ActivityIndicator />
        )
      }
      else {
        return null;
      }
    }
    catch (error) {
      console.log(error);
    }
  };
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <FlatList
          // Data
          data={this.state.documentData}
          // Render Items
          renderItem={({ item }) => (
            <View style={styles.itemContainer}>
              <Text>(ID: {item.id}) {item.first_name} {item.last_name}</Text>
            </View>
          )}
          // Item Key
          keyExtractor={(item, index) => String(index)}
          // Header (Title)
          ListHeaderComponent={this.renderHeader}
          // Footer (Activity Indicator)
          ListFooterComponent={this.renderFooter}
          // On End Reached (Takes a function)
          onEndReached={this.retrieveMore}
          // How Close To The End Of List Until Next Data Request Is Made
          onEndReachedThreshold={0}
          // Refreshing (Set To True When End Reached)
          refreshing={this.state.refreshing}
        />
      </SafeAreaView>
    )
  }
}
// Styles
const styles = StyleSheet.create({
  container: {
    height: height,
    width: width,
  },
  headerText: {
    fontFamily: 'System',
    fontSize: 36,
    fontWeight: '600',
    color: '#000',
    marginLeft: 12,
    marginBottom: 12,
  },
  itemContainer: {
    height: 80,
    width: width,
    borderWidth: .2,
    borderColor: '#000',
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontFamily: 'System',
    fontSize: 16,
    fontWeight: '400',
    color: '#000',
  },
});

Прокрутка

Вот и все! Теперь вы реализовали бесконечную прокрутку в своем приложении React Native с Firebase Cloud Firestore, что сэкономило вам $ $$ на чтении и улучшило UX.

Никто не идеален. Если вы обнаружили какие-либо ошибки, хотите предложить улучшения или расширить тему, отправьте мне сообщение. Я обязательно добавлю какие-либо улучшения или исправлю любые проблемы.