Search code examples
flutterflutter-webflutter-getxscrollcontroller

SingleChildScrollView + Controller - Page resets to top when resizing window


I am creating a flutter Web application, but have issues when resizing the window with a SingleChildScrollView + ScrollController.

When I resize the browser window, the page "snaps" back up to the very top. Being a web app, most of the page "sections" are made from Columns with responsively coded widgets as children, with such widgets as Flexible or Expanded. From what I have read, the SingleChildScrollView widget doesn't work well with Flexible or Expanded widgets, so I thought that may be my issue.

For testing purposes, I created a new page with a single SizedBox that had a height of 3000, which would allow me to scroll. After scrolling to the bottom and resizing the window, I was still snapped up to the top of the page. Thus, with or without using Expanded or Flexible widgets, I have the same result.

Test with a SizedBox only

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      color: Colors.white,
      body: SingleChildScrollView(
        controller: controller.scrollController,
        primary: false,
        child: Column(
          children: [
            SizedBox(
              width: 150,
              height: 3000,
            ),
          ],
        ),
      ),
    );
  }

I am using Getx with this project to try getting a demo app up and running a bit quicker while I am still learning the core concepts. Below is my controller.

Controller

class HomePageScrollControllerX extends GetxController {
  late ScrollController scrollController;

  @override
  void onInit() {
    super.onInit();

    scrollController = ScrollController(
      initialScrollOffset: 0.0,
      keepScrollOffset: true,
    );
    
  }
}

Thank you in advance for any insight on this subject!

EDIT

I have added a listener on my ScrollController, which is able to print to the console that I am scrolling. However, the listener does not get called when I resize the window (tested in both Chrome and Edge).

Currently, I believe my only option is to use the listener to update an "offset" variable in the controller, and pass the window's width over to the controller when the widget rebuilds. If done properly, I should be able to use the controller to scroll to the saved offset. Something like this:

if (scrollController.hasClients) {
  if (offset > scrollController.position.maxScrollExtent) {
    scrollController.jumpTo(scrollController.position.maxScrollExtent);
  } else if (offset < scrollController.position.minScrollExtent) {
    scrollController.jumpTo(scrollController.position.minScrollExtent);
  } else {
    scrollController.jumpTo(offset);
  }
}

However, I feel like this shouldn't be necessary - and I bet this solution would be visually evident to the user.

Edit 2

While I did get this to work with adding the below code just before the return statement, it appears that my initial thoughts were correct. When I grab the edge of the window and move it, it pops up to the top of the window, then will jump to the correct scroll position. It looks absolutely terrible!

  @override
  Widget build(BuildContext context) {
    Future.delayed(Duration.zero, () {
      controller.setWindowWithAndScroll(MediaQuery.of(context).size.width);
    });
    return PreferredScaffold(
      color: Colors.white,
      body: SingleChildScrollView(
        controller: controller.scrollController,
    ......

Solution

  • I found the answer, and it was my scaffold causing the issue - specifically, the scaffold key. But, before that, the Getx usage to get the answer is very easy, so for those of you looking for that particular answer, it is shown below.

    Getx Controller

    import 'package:flutter/cupertino.dart';
    import 'package:get/get.dart';
    
    class HomePageScrollControllerX extends GetxController {
      late ScrollController scrollController;
      
      @override
      void onInit() {
        super.onInit();
        scrollController =
            ScrollController(keepScrollOffset: true, initialScrollOffset: 0.0);
      }
    
      @override
      void onClose() {
        super.onClose();
        scrollController.dispose();
      }
    
    }
    

    Stateless Widget Build Function

    class HomePage extends StatelessWidget {
      HomePage({
        Key? key,
      }) : super(key: key);
    
      // All child widgets can use Get.find(); to get instance
      final HomePageScrollControllerX controller =
          Get.put(HomePageScrollControllerX());
    
      @override
      Widget build(BuildContext context) {
    
        return PreferredScaffold(
          color: Colors.white,
          body: SingleChildScrollView(
            controller: controller.scrollController,
            primary: false,
            ... Etc
    

    So, why didn't this work for me? I created a class called "PreferredScaffold" to save myself a few lines of repetitive code.

    PreferredScaffold

    class PreferredScaffold extends StatelessWidget {
      final Widget? body;
      final Color? color;
      const PreferredScaffold({Key? key, this.body, this.color = Colors.white})
          : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
        return Scaffold(
          key: scaffoldState,
          backgroundColor: color,
          appBar: myNavBar(context, scaffoldState),
          drawer: const Drawer(
            child: DrawerWidget(),
          ),
          body: body,
        );
      }
    }
    

    The problem with the above is, when the window is adjusted, the build function is called. When the build function is called for the scaffold, the scaffoldKey is being set. When set, it returns the scroll position back to 0, or the top of the screen.

    In the end, I had to make another Controller that would basically hand over the same instance of a key to the scaffold, so it wouldn't be reset when the build function was called.

    ScaffoldController

    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    
    class ScaffoldControllerX extends GetxController {
      static ScaffoldControllerX instance = Get.find();
      final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
    }
    

    This changed my PreferredScaffold to the following

    PreferredScaffold (version 2)

    import 'package:flutter/material.dart';
    import 'drawer/drawer_widget.dart';
    import 'nav/my_navigation_bar.dart';
    
    class PreferredScaffold extends StatelessWidget {
      final Widget? body;
      final Color? color;
      PreferredScaffold({Key? key, this.body, this.color = Colors.white})
          : super(key: key);
    
      final ScaffoldControllerX scaffoldControllerX = ScaffoldControllerX();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          key: scaffoldControllerX.scaffoldState,
          backgroundColor: color,
          appBar: NavBar(context, scaffoldControllerX.scaffoldState),
          drawer: const Drawer(
            child: DrawerWidget(),
          ),
          body: body,
        );
      }
    }
    

    I hope this helps if someone has a similar situation.