flutterpull-to-refresh

Refresh indicator "jumps" on iOS when the list updates in Flutter


I do not understand why the list is "jumping" when I use a refresh indicator within a SingleChildScrollView. Here is a minimal example to reproduce the "bug".

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  late Timer _timer;

  @override
  void initState() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _counter++;
      });
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          await Future<void>.delayed(const Duration(seconds: 1));
        },

        child: SingleChildScrollView(
          child: Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(16),
                child: Text('Time count : $_counter'),
              ),
              for(int i = 0; i < 100; i++)
                ListTile(
                  title: Text('Item $i'),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

When you pull the list to refresh, the screen moves downward on iOS (on Android, the bug does not appear as only the refresh indicator appears but the list itself is not "pulled down"). Every time the list is refreshed - here every second to change the text of the first element - the list jumps back to its initial value, making the pull to refresh janky. I've tried wrapping the counter text within a StatefulWidget, but it doesn't change anything. Also, I have no problem scrolling into the SingleChildScrollView, as expected, so it seems there is a problem only within the refresh indicator logic. Do you have any idea how to overcome this?


Solution

  • You can modify the code to look something like this:

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key});
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
      late Timer _timer;
    
      @override
      void initState() {
        _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
          setState(() {
            _counter++;
          });
        });
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text('widget.title'),
          ),
          body: RefreshIndicator(
            onRefresh: () async {
              await Future<void>.delayed(const Duration(seconds: 1));
            },
            child: ListView(
              physics: BouncingScrollPhysics(),
              children: [
                Container(
                  height: 55,
                  padding: const EdgeInsets.all(16),
                  child: Text('Time count : $_counter'),
                ),
                ListView.builder(
                  physics: NeverScrollableScrollPhysics(),
                  itemCount: 100,
                  shrinkWrap: true,
                  itemBuilder: (context, index) {
                  return ListTile(
                    title: Text('Item $index'),
                  );
                },)
                // for (int i = 0; i < 100; i++)
                //   ListTile(
                //     title: Text('Item $i'),
                //   ),
              ],
            ),
          ),
        );
      }
    }