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,
......
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.
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.
Related
my appbar is disappeared when i go new page using navigator
actually, there was a yellow underline on the new page's text.
I found that's reason is the page didn't have Material.
so i added the Material Widget on that, so I could fixed the problem of text underline.
i think appbar disappeared problem also relates with Material,
but even i wrapped the code with Material widget, still the problem is not solved.
what can i do?
here is the navigator code.
exhibition[i] is what i wanted to transmit to new page(=DetailScreen)
InkWell(
onTap: () {Navigator.push(
context,
MaterialPageRoute(builder: (context)=>DetailScreen(exhibition: exhibitions[i])),
);
And the below is DetailScreen(new page)'s code.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'model_exhibitions.dart';
class DetailScreen extends StatefulWidget {
final Exhibition exhibition;
DetailScreen({required this.exhibition});
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
bool bookmark = false;
#override
void initState() {
super.initState();
bookmark = widget.exhibition.bookmark;
}
#override
Widget build(BuildContext context) {
return Material(
child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(20, 40, 0, 0),
child: Row( ...
Thank you for your help sincerely
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'model_exhibitions.dart';
class DetailScreen extends StatefulWidget {
final Exhibition exhibition;
DetailScreen({required this.exhibition});
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
bool bookmark = false;
#override
void initState() {
super.initState();
bookmark = widget.exhibition.bookmark;
}
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold( //add scaffold
body: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(20, 40, 0, 0),
child: Row( ...
It will not work without scaffold.
I think you are not getting what scaffold is and how it's behaviour
is?
Scaffold is a widget which provides your screen/route a default
behaviour similar to your android/ios screens like AppBar, Body,
Title, FloatingActionButton, Drawer etc.
So that you do not have to make yourself a new structure.
If you are not using scaffold, then your page will act like a plain
body structure in which you have to fill custom widgets as per your
requirements.
I have a List view. Builder with an image. The image has an opacity depending on a provider element (provider element is true, the opacity is 1, if it false opacity is 0). Then I have another class, from that class I can update the provider of the list view image, however in order to change the effect of the change, I have to set state the list view widget.
Even if it is good and is working properly, I don't want to do that because when I set state the list view widget, the list view position restart and I need to scroll again at the element of the list view, I know that I can save the scroll position (with a controller) but because my list view is too large, it needs a lot of time to get at the position, so I don't like it.
Any idea ?
If you want to rebuild the UI without calling setState, you can use the help of ValueNotifier and the ValueListenableBuilder widget.
Here is a simplified counter app, if you take a look at the console, you'll see that print('build'); is only called initially running the build method and not called again when calling _increment():
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: PageLoadApp()));
final counter = ValueNotifier<int>(0);
class PageLoadApp extends StatefulWidget {
const PageLoadApp({Key? key}) : super(key: key);
#override
State<PageLoadApp> createState() => _PageLoadAppState();
}
class _PageLoadAppState extends State<PageLoadApp> {
void _increment() {
counter.value++;
}
#override
Widget build(BuildContext context) {
print('build');
return Scaffold(
body: Scaffold(
body: ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, _) {
return Center(
child: Text('$value', style: const TextStyle(fontSize: 30)),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
)));
}
}
Additionally, this widget can be a StatelessWidget.
Since you haven't provided a code example, you'll have to adapt this provided example for your case.
Here is a YouTube video by the Google team explaining ValueListenableBuilder
Essentially, I'm trying to devise a way for a child of a TabView to customise aspects of the active Scaffold instance, such as the floating action button, application bar and so on.
Using the code snippet below, the idea is for_ATabState to set the floatingActionButton of the Scaffold instance to a custom widget that it controls.
class MasterWidget extends StatefulWidget {
#override
_MasterWidgetState createState() => _MasterWidgetState();
Widget? setFloatingActionButton() {
// how to access state and invoke `setState`?
}
}
class _MasterWidgetState extends State<MasterWidget> {
#override
Widget build(BuildContext context) {
final tabs = [ATab()];
return Scaffold(
appBar: AppBar(),
floatingActionButton: activeTab.buildFloatingActionButton(),
body: DefaultTabController(
length: tabs.length,
child: TabBarView(
children: tabs,
controller: tabController,
),
),
);
}
}
class ATab extends StatefulWidget {
#override
_ATabState createState() => _ATabState();
}
class _ATabState extends State<ATab> {
#override
Widget build(BuildContext context) {
// Doesn't work:
// context.findAncestorWidgetOfExactType<Scaffold>()?.floatingActionButton = AFloatingActionButton();
// context.findAncestorWidgetOfExactType<MasterWidget>()?.setFloatingActionButton(AFloatingActionButton());
return SomeWidget();
}
}
Here's what I tried:
Try to context.findAncestorWidgetOfExactType<Scaffold>() in _ATabState and somehow set Scaffold's floatingActionButton attribute; unfortunately there does not seem to be a setter available.
Try to context.findAncestorWidgetOfExactType<MasterWidget>() in _ATabState but then I'm not able to access the state where the rendering takes place.
What's the approach applicable here?
I want to keep child widget state using GlobalKey after parent's state is changed. There is a workaround by using Opacity in order to solve the problem, but I wonder why GlobalKey doesn't work as expected in this scenario.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Retrieve Text Input',
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
final _key = GlobalKey();
bool _showTimer = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextButton(
onPressed: () => setState(() {
_showTimer = !_showTimer;
}),
child: Text('show/hide')),
_showTimer ? TimerWidget(key: _key) : Container()
],
),
));
}
}
class TimerWidget extends StatefulWidget {
const TimerWidget({Key key}) : super(key: key);
#override
_TimerWidgetState createState() => _TimerWidgetState();
}
const int TIME_REMINDING_SECONDS = 480;
class _TimerWidgetState extends State<TimerWidget> {
Timer _timer;
int _start = TIME_REMINDING_SECONDS;
#override
Widget build(BuildContext context) {
return Text(
'${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
}
#override
initState() {
super.initState();
_startTimer();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
_startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
} else {
_start = _start - 1;
}
},
),
);
}
}
You will see the timer restarts to initial value every times the parent's state is changed. I tried with the solutions here but didn't work.
as an option you can skip GlobalKey and simple use Offstage widget
Offstage(offstage: !_showTimer, child: TimerWidget()),
another answer mentioned Visibility with maintainState parameter.
This is pointless because it uses Offstage under the hood.
By Every time in the previous code every time the state changes it creates a new instance of timer so GlobalKey won't take effect there since its new instance.
Global keys uniquely identify elements. Global keys provide access to
other objects that are associated with those elements, such as
BuildContext. For StatefulWidgets, global keys also provide access to
State.
https://api.flutter.dev/flutter/widgets/GlobalKey-class.html
By the Above statement, the global key is used to access the state within the widgget.
So in your case when TimerWidget() switches it's disposed of its state and not gonna preserve that's why its timer getting reset every time you change state.
--- Update ---
Instead of _showTimer ? TimerWidget(key: _key) : Container()
Use below code:
Visibility(
visible: _showTimer,
maintainState: true,
child: page
)
Here, maintain state is keeping the state of the widget.
Update
The following code moves the scope of a globally unique key so that it will maintain its state while the app lives. When adding this key to an Offset widget, you can show/hide the timer while retaining its state. Without this step, the timer widget would continue to reset as the timer widget is removed and re-added to the rendering tree. I also added the late modifier to the state class _timer variable.
Removing the timer widget from the tree will normally call the dispose method; so one alternative is to use Offstage which is designed to temporarily remove widgets based on state. This seems to be precisely what you are attempting to do. However, the Visibility widget does this same behavior without having to maintain a Global Key (but your focus seemed to be on wanting to leverage a key). Note the other widgets discussed in Visibility notes may provide other alternatives.
Some important considerations:
Animations continue to run when using Offstage widget.
From the docs (on the Offstage widget):
A widget that lays the child out as if it was in the tree, but without
painting anything, without making the child available for hit testing,
and without taking any room in the parent.
Offstage children are still active: they can receive focus and have
keyboard input directed to them.
Animations continue to run in offstage children, and therefore use
battery and CPU time, regardless of whether the animations end up
being visible.
Offstage can be used to measure the dimensions of a widget without
bringing it on screen (yet). To hide a widget from view while it is
not needed, prefer removing the widget from the tree entirely rather
than keeping it alive in an Offstage subtree.
From the docs (on the Visibility widget):
By default, the visible property controls whether the child is
included in the subtree or not; when it is not visible, the
replacement child (typically a zero-sized box) is included instead.
A variety of flags can be used to tweak exactly how the child is
hidden. (Changing the flags dynamically is discouraged, as it can
cause the child subtree to be rebuilt, with any state in the subtree
being discarded. Typically, only the visible flag is changed
dynamically.)
These widgets provide some of the facets of this one:
Opacity, which can stop its child from being painted. Offstage, which can stop its child from being laid out or painted.
TickerMode, which can stop its child from being animated. ExcludeSemantics, which can hide the child from accessibility tools. IgnorePointer, which can disable touch interactions with
the child. Using this widget is not necessary to hide children. The
simplest way to hide a child is just to not include it, or, if a
child must be given (e.g. because the parent is a StatelessWidget)
then to use SizedBox.shrink instead of the child that would
otherwise be included.
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
//create a key that will persist in app scope
var timerKey = GlobalKey();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Retrieve Text Input',
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
bool _showTimer = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextButton(
onPressed: () => {
setState(() {
_showTimer = !_showTimer;
})
},
child: Text('show/hide')),
//reuse the current timer logic to show/hide the time
Offstage(
offstage: _showTimer,
child: TimerWidget(
key: (timerKey),
),
)
],
),
));
}
}
class TimerWidget extends StatefulWidget {
const TimerWidget({Key? key}) : super(key: key);
#override
_TimerWidgetState createState() => _TimerWidgetState();
}
const int TIME_REMINDING_SECONDS = 480;
class _TimerWidgetState extends State<TimerWidget> {
late Timer _timer;
int _start = TIME_REMINDING_SECONDS;
#override
Widget build(BuildContext context) {
return Text(
'${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
}
#override
initState() {
super.initState();
_startTimer();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
_startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
} else {
_start = _start - 1;
}
},
),
);
}
}
Nota Bene
Visibility does not require a key at all.
Visibility(
visible: _showTimer,
maintainState: true,
child: TimerWidget(),
),
Original
Review my related question here. You will want to ensure that a Unique Key is available to the parent widget before you start to use the child. My example is pretty in-depth; let me know if you have follow-up issues.
I imagine a new kind of screen, and I would like to do it with Flutter since it's very powerful for rendering fast and smoothly.
I want to achieve a kind of infinite screen with kind of square or zone where you can move into. Actually exactly like a map (in fact not infinite but very large) but where I can:
Drag and translate
Zoom in and out
Click and press on the different component of the screen (square or whatever)
I imagine use GestureDetector on my widget "map" combine with transform on each component insde and refresh the screen after each move, or redrawing each component with draw but I'm not sure it's the best way to follow with this.
Thanks for helping if you have any idea !!
I've implemented part of things you asked for except for the "infinite map".
The code is quite beefy so I've described this stuff in an article on Medium.
It allows:
move map by dragging
place new objects on the map
zoom in/out
click objects
GitHub repo.
Interesting proposal. I don't have the implementation, after all, it's up to you but I have some pointers.
Translation, I imagine, can easily be handled by 2 nested ListViews. One, that scrolls X and one that scrolls in Y direction. ScrollController can be queries for all kinds of info.
Zoom is also fairly easy at first blick: you can wrap the entire screen in a Transform.scale() widget.
You could wrap each tappable widget in a GuestureDetector, query for their RenderBox to get their position on screen in local or global coordinates, get their size.
Note: in games, there is a concept called clipping distance. Figuring out how to implement that in Flutter is going to be a fun challenge. It allows you not to render those Widgets that are too small, you zoomed out a lot eg. Let me know how it goes! Curious.
The InteractiveViewer widget
One solution could be using the InteractiveViewer widget with its constrained property set to false as it will out of the box support:
Drag and translate
Zooming in and out - Simply set minScale and maxScale
Clicking and pressing widgets like normal
InteractiveViewer as featured on Flutter Widget of the Week: https://www.youtube.com/watch?v=zrn7V3bMJvg
Infinite size
Regarding the question's infinite size part, a maximum size for the child widget must be specified, however this can be very large, so large that it is actually hard to re-find widgets in the center of the screen.
Alignment of the child content
By default the child content will start from the top left, and panning will show content outside the screen. However, by providing a TransformationController the default position can be changed by providing a Matrix4 object in the constructor, f.ex. the content can be center aligned if desired this way.
Example code
The code contains an example widget that uses the InteractiveViewer to show an extremely large widget, the example centers the content.
class InteractiveViewerExample extends StatefulWidget {
const InteractiveViewerExample({
Key? key,
required this.viewerSize,
required this.screenHeight,
required this.screenWidth,
}) : super(key: key);
final double viewerSize;
final double screenHeight;
final double screenWidth;
#override
State<InteractiveViewerExample> createState() =>
_InteractiveViewerExampleState();
}
class _InteractiveViewerExampleState extends State<InteractiveViewerExample> {
late TransformationController controller;
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InteractiveViewer.builder(
boundaryMargin: const EdgeInsets.all(40.0),
minScale: 0.001,
maxScale: 50,
transformationController: controller,
builder: (BuildContext context, vector.Quad quad) {
return Center(
child: SizedBox(
width: widget.viewerSize,
height: widget.viewerSize,
child: const InteractiveViewerContent(),
),
);
},
),
),
);
}
#override
void initState() {
super.initState();
// Initiate the transformation controller with a centered position.
// If you want the InteractiveViewer TopLeft aligned remove the
// TransformationController code, as the default controller in
// InteractiveViewer does that.
controller = TransformationController(
Matrix4.translation(
vector.Vector3(
(-widget.viewerSize + widget.screenWidth) / 2,
(-widget.viewerSize + widget.screenHeight) / 2,
0,
),
),
);
}
}
// Example content; some centered and top left aligned widgets,
// and a gradient background.
class InteractiveViewerContent extends StatelessWidget {
const InteractiveViewerContent({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
TextStyle? style = Theme.of(context).textTheme.headline6;
return Container(
padding: const EdgeInsets.all(32.0),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[Colors.orange, Colors.red, Colors.yellowAccent],
),
),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.topLeft,
child: SelectableText("Top Left", style: style),
),
SelectableText("Center", style: style),
],
),
);
}
}
Usage
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: InteractiveViewerScreen(),
);
}
}
class InteractiveViewerScreen extends StatelessWidget {
const InteractiveViewerScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InteractiveViewerExample(
viewerSize: 50000,
screenHeight: MediaQuery.of(context).size.height,
screenWidth: MediaQuery.of(context).size.width,
),
),
);
}
}
What does the code do