2

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,
    ......
FoxDonut
  • 252
  • 2
  • 14

2 Answers2

2

I implemented your code without getX by initializing the scrollController as a final variable outside your controller class. I ran it on microsoft edge and did not face the issue you are describing. What's causing your problem is most probably the way you are handling your state management with getX. I'm guessing your onInit function is run multiple times when you are resizing your web page and that's why the page snaps back up. I would recommend logging how many times the onInit() function is called.

Kimiya Zargari
  • 324
  • 1
  • 14
  • 1
    Thank you for your insight on this. I did take a look, and the onInit() function is only being called once. In onInit(), I added a listener to the scrollController that prints ("scroll detected"). When I scroll, the print statement is added in the console. I believe it may just be an issue of saving the position? – FoxDonut Oct 31 '22 at 22:28
  • I believe you had turned me in the right direction, so I decided to reward you the bounty. Thanks! – FoxDonut Nov 07 '22 at 22:09
  • 1
    Thank you very much :)))) I'm so glad your problem was solved. I wasn't expecting that :) – Kimiya Zargari Nov 08 '22 at 09:04
2

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.

FoxDonut
  • 252
  • 2
  • 14