flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.34k stars 27.53k forks source link

One-side infinity scroll is not possible to build due to bugs in slivers #139303

Closed ryzizub closed 10 months ago

ryzizub commented 11 months ago

Is there an existing issue for this?

Steps to reproduce

I'm attempting to create a list that refreshes when pulled down from the top and allows infinite scrolling at the bottom. I feel the current sliver setup doesn't manage this behavior effectively.

Here's how the list should function: When at the top, pulling the list down should reveal a circular progress indicator and refresh the list. However, my position in the list should remain the same, with new items appearing above where I am. As I scroll down, I should be able to keep scrolling indefinitely, as long as the source continues to add new items. It's unlikely to reach the end of the list unless someone scrolls for several years. 😄

Expected results

I aim to utilize Slivers in a manner that enables the creation of behavior commonly seen in various non-Flutter apps.

Actual results

It's feasible to construct this with two slivers, using the center key on the bottom one. Initially, it functions correctly, but issues arise during the second refresh. Adjusting the center key causes items to rebuild, leading to performance degradation. Adding more slivers further slows down the list. This setup presents numerous problems.

Additionally, attempting to create a button that scrolls to the top without visible overscroll is challenging and leads to many issues.

Code sample

It's minimal code to try to build such behaviour

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _mainListKey = const Key('list');
  final ScrollController scrollController = ScrollController();

  final List<String> topItems = [
    'Top 1',
    'Top 2',
    'Top 3',
  ];
  final List<String> bottomItems = [
    'Bottom 1',
    'Bottom 2',
    'Bottom 3',
  ];

  void _updateTop() {
    setState(() {
      // bottomItems.addAll(topItems);
      // topItems.removeRange(0, 3);
      topItems.addAll([
        'Top 1 reworked',
        'Top 2 reworked',
        'Top 3 reworked',
      ]);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: RefreshIndicator.adaptive(
          onRefresh: () async {
            _updateTop();
          },
          child: CustomScrollView(
            physics: const BouncingScrollPhysics(
              parent: AlwaysScrollableScrollPhysics(),
            ),
            center: _mainListKey,
            controller: scrollController,
            slivers: <Widget>[
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                    return ComplexWidget(
                      somethingIn: topItems[index],
                    );
                  },
                  childCount: topItems.length,
                ),
              ),
              SliverList(
                key: _mainListKey,
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                    return ComplexWidget(
                      somethingIn: bottomItems[index],
                    );
                  },
                  childCount: bottomItems.length,
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateTop,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

class ComplexWidget extends StatelessWidget {
  const ComplexWidget({
    super.key,
    required this.somethingIn,
  });

  final String somethingIn;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(20.0),
      color: Colors.red,
      child: Text(somethingIn),
    );
  }
}

Screenshots or Video

No response

Logs

No response

Flutter Doctor output

Doctor output ```console [✓] Flutter (Channel stable, 3.13.7, on macOS 14.0 23A344 darwin-arm64, locale cs-CZ) • Flutter version 3.13.7 on channel stable at /Users/ryzizub/.asdf/installs/flutter/3.13.7-stable • Upstream repository https://github.com/flutter/flutter.git • Framework revision 2f708eb839 (7 weeks ago), 2023-10-09 09:58:08 -0500 • Engine revision a794cf2681 • Dart version 3.1.3 • DevTools version 2.25.0 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/ryzizub/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.0.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 15A507 • CocoaPods version 1.12.1 ```
ryzizub commented 11 months ago

Just to add, I was able to build such behaviour but not without extensive hacking https://pub.dev/packages/scrollable_positioned_list a with a loss in performance

huycozy commented 11 months ago

Hi @ryzizub

I aim to utilize Slivers in a manner that enables the creation of behavior commonly seen in various non-Flutter apps.

Could you share a specific app or demo for this?

Adjusting the center key causes items to rebuild, leading to performance degradation. Adding more slivers further slows down the list. This setup presents numerous problems.

I observed this through DevTools when running the app in profile mode on Realme 6, Android 11 on the latest stable channel 3.16.2), not sure how to detect performance degradation. How many times did you refresh to see this issue? What symptoms appear when an issue occurs? (waste memory, laggy, slow, etc)

ryzizub commented 11 months ago

Hi @huycozy

Could you share a specific app or demo for this?

Best examples are X(Twitter), Ivory(Mastodon client)

I observed this through DevTools when running the app in profile mode on Realme 6, Android 11 on the latest stable channel 3.16.2), not sure how to detect performance degradation. How many times did you refresh to see this issue? What symptoms appear when an issue occurs? (waste memory, laggy, slow, etc)

It's somewhat complex to explain. The example I provided isn't the complete version. I was attempting to convey that, in trying to replicate the behavior of the apps I mentioned, I explored multiple approaches, but none were ideal. Adding slivers above the current position led to memory and slowness issues, and changing the center resulted in the need to rebuild, among other problems.

huycozy commented 11 months ago

Maybe worth trying infinite_scroll_pagination package to see if it can solve your case.

If you believe this should be done in framework SDK, please share a complete version so that we may verify this. Thank you!

github-actions[bot] commented 10 months ago

Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now. If you find this problem please file a new issue with the same description, what happens, logs and the output of 'flutter doctor -v'. All system setups can be slightly different so it's always better to open new issues and reference the related ones. Thanks for your contribution.

github-actions[bot] commented 10 months ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.