0

我基本上有一个聊天,一开始只加载了一部分。现在我需要在顶部和底部添加更多数据(小部件)。

现在在我的示例中,我已经实现了,当用户向上滚动并且剩余 50 个或更少的小部件时,它会调用后端来加载更多数据(因此,当用户进一步向上滚动时,加载已经在进行或完成)。工作正常,一旦加载数据,用户就可以进一步向上滚动。就像在 WhatsApp 和更多带有提要的应用程序中一样。

然而,也需要加载新消息。这些是在底部添加的。但是,这会移动列表视图中的每个项目,这是一个巨大的问题,因为当用户查看历史记录时,我们不想“自动滚动”新小部件的数量......

如何在不“移动”内容的情况下双向添加小部件?

###小后端模拟###

import 'dart:math';

class Msg {
  int i; // index of message
  String data;
  Msg({required this.i}) : data = 'Msg ${i + 1}';
}

class Backend {
  static const
      FORWARD = 1,
      BACKWARD = -1,
      LIMIT = 100;

  /// simulate backend call to fetch history or new data
  Future<List<int>> _loadData(int index, int direction) async {
    final random = new Random();

    // // 1000ms - 4000ms response time
    final waitMillis = 1000 + random.nextInt(4101 - 1000);
    await Future.delayed(Duration(milliseconds: waitMillis));

    if (direction == BACKWARD) {
      // backward (last 50): index - LIMIT - 1 -> ... -> index - 1
      // LOOKS LIKE: 50, 51, 52, 53, ... index - 1

      if (index <= 0) return []; // no messages left

      // prevent that count is higher than amount of messages left before current index
      // so minimal index is 0
      final count = index < LIMIT ? index : LIMIT;

      return List<int>.generate(count, (i) => index - count + i);
    }
    // forward (next 50): index + 1 -> ... -> index + LIMIT
    // LOOKS LIKE: index + 1, 50, 51, 53, ...

    // 40% 0, 20% 1, 20% 2 and 20% 3 new messages
    final randI = random.nextInt(6);
    return List<int>.generate(randI < 2 ? 0 : (randI - 2), (i) => index + i + 1);
  }

  /// calls the backend simulation and parses the data
  Future<List<Msg>> loadEntries(int index, int direction) async {
    if (direction == BACKWARD && index <= 0) return [];
    final data = await _loadData(index, direction);
    return data.map<Msg>((i) => Msg(i: i)).toList();
  }
}

### 我的自定义“列表视图”###

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:list/backend.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

class CustomListWidget extends StatefulWidget {
  const CustomListWidget({Key? key}) : super(key: key);

  @override
  _CustomListWidgetState createState() => _CustomListWidgetState();
}

class _CustomListWidgetState extends State<CustomListWidget> {
  // when the user scrolls up and x messages are left more history gets loaded
  static const MIN_HISTORY_LOADING_TRIGGER = 50;

  static const _SPIN_KIT = SpinKitRing(
    lineWidth: 1.5,
    color: Colors.black,
    size: 20.0,
  );

  final _itemScrollController = ItemScrollController();
  final _itemPositionListener = ItemPositionsListener.create();

  final _backend = Backend();
  bool _isLoadingHistory = false, _showLoading = false, _disposed = false;

  // startIndex - LIMIT + 1 -> ... -> startIndex
  // 400 to begin with for testing
  List<Msg> entries = List<Msg>.generate(Backend.LIMIT, (i) => Msg(i: 400 - Backend.LIMIT + i + 1));

  /// loads more history data
  Future<void> _loadMoreHistory() async {
    if (entries.first.i == 0) return; // no messages left
    _isLoadingHistory = true;
    final newMsgsPending =  _backend.loadEntries(entries.first.i, Backend.BACKWARD);
    bool isLoadingHistoryThisInstance = true;
    Future.delayed(Duration(milliseconds: 1000)).then((_) {
      // show loading if after 1000 seconds not loaded yet
      if (isLoadingHistoryThisInstance) setState(() => _showLoading = true);
    });
    final List<Msg> newMsgs = await newMsgsPending;
    isLoadingHistoryThisInstance = false;
    if (newMsgs.isNotEmpty) {
      setState(() {
        entries = newMsgs + entries;
        _showLoading = false;
      });
    } else if (_showLoading) setState(() => _showLoading = false);
    _isLoadingHistory = false;
  }

  /// loads new data
  Future<void> _loadUpdates() async {
    while (!_disposed) {
      final newMsgs = await _backend.loadEntries(entries.last.i, Backend.FORWARD);
      if (newMsgs.isNotEmpty) {
        setState(() {
          entries = entries + newMsgs;
        });
      }
    }
  }

  @override
  void setState(VoidCallback fn) {
    if (_disposed) return;
    super.setState(fn);
  }

  @override
  void dispose() {
    super.dispose();
    _disposed = true;
  }

  @override
  void initState() {
    super.initState();
    _itemPositionListener.itemPositions.addListener(_scrollListener);
    _loadUpdates();
  }

  /// memory efficient list widget with widget tracking
  Widget _getScrollablePositionedList() {
    return Scrollbar(
      child: ScrollablePositionedList.builder(
        reverse: true,
        itemCount: entries.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(entries[entries.length - index - 1].data),
        ),
        itemScrollController: _itemScrollController,
        itemPositionsListener: _itemPositionListener,
      ),
    );
  }

  /// loading indicator when backend call takes longer
  Widget _getLoadingWidget() {
    return Positioned(
      top: 15,
      right: 15,
      child: Opacity(
        opacity: _showLoading ? 1.0 : 0.0,
        child: _SPIN_KIT,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        _getScrollablePositionedList(),
        _getLoadingWidget(),
      ],
    );
  }

  /// called while scrolling to track what the position is, calls to load more history
  void _scrollListener() {
    if (!_isLoadingHistory) {
      // topPosition is 1 when "oldest" or first widget of entries got loaded, so we can use it to
      // determine if we should load more data
      final topPosition = (_itemPositionListener.itemPositions.value.last.index - entries.length) * -1;
      if (topPosition <= MIN_HISTORY_LOADING_TRIGGER) {
        // x entries remaining, load more history
        _loadMoreHistory();
      }
    }
    // could also verify which widgets have been viewed for tracking and reading verifications
  }
}

在此处输入图像描述

在此处输入图像描述

4

0 回答 0