How to prevent Flutter app from scrolling to top after calling setState? - dart

I have an ExpansionPanelList inside a SingleChildScrollView. Whenever I (un)fold a panel and therefore call setState, the SingleChildScrollView scrolls back to the top. How can I prevent this?
#override
Widget build(BuildContext context) {
final _scaffoldKey = new GlobalKey<ScaffoldState>();
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text(widget.title),
),
body: new SingleChildScrollView(
child: new ExpansionPanelList(
children: <ExpansionPanel>[
// panels
],
expansionCallback: (int index, bool isExpanded) {
setState(() {
// toggle expanded
});
},
), // ExpansionPanelList
), // SingleChildScrollView
); // Scaffold
}
This answer suggests using a custom ScrollController with keepScrollOffset set to true, however this is the default value and setting it explicitly to true therefore does not change anything.

That's because you are using a new Key every time you rebuild the widget (setState).
To fix your issue just move the code below outside the build method
final _scaffoldKey = new GlobalKey<ScaffoldState>();
Like this :
final _scaffoldKey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text(widget.title),
),

I was having the same problem, and somehow the answer from #diegoveloper did not do the trick for me.
What i ended up doing was separating the SingleChildScrollView in an independent StatefulWidget: That also fixed the scroll animation.
My code then ended up being something like
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: MyExpansionList(),
...
class MyExpansionListextends StatefulWidget {
#override
_MyExpansionListState createState() => _MyExpansionListState();
}
class _MyExpansionListState extends State<MyExpansionList> {
#override
Widget build(BuildContext context) {
return Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: [
ExpansionPanelList(animationDuration: Duration(seconds: 1),
In this way the setState() did update only the ExpansionPanelList/ScrollView and not the whole Scaffold.
I hope this also helps others facing same problem...

Nothing helped me until I realised that in the build() function I was calling jumpTo() of the controller that was attached to my list. Like this:
#override
Widget build(BuildContext context) {
if (widget.controller.hasClients) {
widget.controller.jumpTo(0);
}
...
}
I removed these lines and the problem was gone. Happy coding :)

You have to pass a key to the SingleChildScrollView. Otherwise, its state is renewed every setState call.
final _scrollKey = GlobalKey();
SingleChildScrollView(
key: _scrollKey,
child:

Related

How to make drawer button UnClickable in Flutter

When my app starts, some data will load in background I don't want users to click on Drawer icon at that time, So I did something like this.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
),
drawer: _isLoading ? null : HomeDrawer(),
body: _isLoading ? CircularProgressIndicator() : _body(),
);
}
In this case, when the app loads the drawer button is notVisible
After loading, the button comes back.
What I want is
The drawer button should be visible during loading state but it should not be clickable.
You can try this.
bool _isLoading = false;
GlobalKey<ScaffoldState> _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
key: _key,
appBar: AppBar(
title: Text("App"),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _isLoading ? null : () => _key.currentState.openDrawer(),
),
),
drawer: YourDrawer(),
);
}
I used the custom drawer button where I assign/remove it's onPressed event based on the _isLoaded variable. After 3 seconds since the page load I set the _isLoaded variable value to true which rerenders the page and enables the drawer button and hides the loader.
class _MyHomePageState extends State<MyHomePage> {
bool _isLoaded = false;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
this._apiCallSample();
}
void _apiCallSample() async {
await Future.delayed(Duration(seconds: 3)).then((_) {
setState(() {
this._isLoaded = true;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: !this._isLoaded ? null : () => this._scaffoldKey.currentState.openDrawer(),
),
title: Text("Drawer"),
),
body: Center(
child: Builder(
builder: (BuildContext context) {
if(!this._isLoaded) {
return CircularProgressIndicator();
}
return Text("Loaded!");
},
),
),
drawer: Drawer(
child: Center(
child: Text("Drawer"),
),
),
key: this._scaffoldKey,
);
}
}
If you want to prevent the user from clicking anywhere on the page then use the AbsorbPointer.
When absorbing is true, this widget prevents its subtree from receiving pointer events by terminating hit testing at itself.

Flutter Switch widget does not work if created inside initState()

I am trying to create a Switch widget add it to a List of widgets inside the initState and then add this list to the children property of a Column in the build method. The app runs successfully and the Switch widget does show but clicking it does not change it as if it is not working. I have tried making the same widget inside the build method and the Switch works as expected.
I have added some comments in the _onClicked which I have assigned to the onChanged property of the Switch widget that show the flow of the value property.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: App(),
));
}
class App extends StatefulWidget {
#override
AppState createState() => new AppState();
}
class AppState extends State<App> {
List<Widget> widgetList = new List<Widget>();
bool _value = false;
void _onClicked(bool value) {
print(_value); // prints false the first time and true for the rest
setState(() {
_value = value;
});
print(_value); // Always prints true
}
#override
void initState() {
Switch myWidget = new Switch(value: _value, onChanged: _onClicked);
widgetList.add(myWidget);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('My AppBar'),
),
body: new Container(
padding: new EdgeInsets.all(32.0),
child: new Center(
child: new Column(children: widgetList),
),
),
);
}
}
initState is to initialize the state, not widgets. build is to create widgets.
There reason it fails is because the widgets needs to be rebuilt when the value changes (when you call setState), but it isn't because when build() is called, the previously (in initState) created widget is reused.
#override
Widget build(BuildContext context) {
List<Widget> widgetList = [];
Switch myWidget = new Switch(value: _value, onChanged: _onClicked);
widgetList.add(myWidget);
return new Scaffold(
appBar: new AppBar(
title: new Text('My AppBar'),
),
body: new Container(
padding: new EdgeInsets.all(32.0),
child: new Center(
child: new Column(children: widgetList),
),
),
);
}

Flutter Access parent Scaffold from different dart file

I have this:
final GlobalKey<ScaffoldState> _scaffoldkey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
key: _scaffoldkey,
drawer: Menu(),
appBar: AppBar(
title: Container(
child: Text('Dashboard'),
),
bottom: TabBar(
tabs: <Widget>[
...
],
),
),
body: TabBarView(
children: <Widget>[
...
],
),
),
);
}
}
Now, the drawer: Menu() is imported from another menu.dart file, which looks like this:
class Menu extends StatelessWidget {
final GlobalKey<ScaffoldState> drawerKey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return new Drawer(
key: drawerKey,
child: new ListView(
children: <Widget>[
new ListTile(
dense: true,
title: new Text('My Text'),
onTap: () {
// On tap this, I want to show a snackbar.
scaffoldKey.currentState.showSnackBar(showSnack('Error. Could not log out'));
},
),
],
),
);
}
}
With the above approach, I get
NoSuchMethodError: The method 'showSnackBar' was called on null.
An easy solution is to tuck the entire menu.dart contents in the drawer: ... directly.
Another way I'm looking at is being able to reference the parent scaffold in order to display the snackbar.
How can one achieve that?
Why can't one even just call the snackbar from anywhere in Flutter and compulsorily it has to be done via the Scaffold? Just why?
You should try to avoid using GlobalKey as much as possible; you're almost always better off using Scaffold.of to get the ScaffoldState. Since your menu is below the scaffold in the widget tree, Scaffold.of(context) will do what you want.
The reason what you're attempting to do doesn't work is that you are creating two seperate GlobalKeys - each of which is its own object. Think of them as global pointers - since you're creating two different ones, they point to different things. And the state should really be failing analysis since you're passing the wrong type into your Drawer's key field...
If you absolutely have to use GlobalKeys for some reason, you would be better off passing the instance created in your outer widget into your Menu class as a member i.e. this.scaffoldKey, but this isn't recommended.
Using Scaffold.of, this is what your code would look like in the onTap function:
onTap: () {
// On tap this, I want to show a snackbar.
Scaffold.of(context).showSnackBar(showSnack('Error. Could not log out'));
},
You can achieve this functionality by using builder widget you don't need to make separate GlobalKey or pass key as a parameter. Just wrap a widget to Builder widget
class CustomDrawer extends StatelessWidget {#override Widget build(BuildContext context) {
return new Drawer(
child: new ListView(
children: <Widget>[
new Builder(builder: (BuildContext innerContext) {
return ListTile(
dense: true,
title: new Text('My Text'),
onTap: () {
Navigator.of(context).pop();
Scaffold.of(innerContext).showSnackBar(SnackBar(
content: Text('Added added into cart'),
duration: Duration(seconds: 2),
action: SnackBarAction(label: 'UNDO', onPressed: () {}),
));
}
);
})
],
),
);}}
From your first question
In other to reference the parent scaffold in the menu widget you can pass the _scaffoldkey to the menu widget as parameter and use ScaffoldMessenger.of() to show snackbar as shown below
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// Root Widget
#override
Widget build(BuildContext context) {
return MaterialApp(
// App name
title: 'Flutter SnackBar',
// Theme
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test(title: 'SnackBar'),
);
}
}
class Test extends StatefulWidget {
final String? title;
final GlobalKey<ScaffoldState> _scaffoldkey = new GlobalKey<ScaffoldState>();
Test({#required this.title});
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
key: widget._scaffoldkey,
drawer: Menu(parentScaffoldkey:widget._scaffoldkey),
appBar: AppBar(
title: Container(
child: Text('Dashboard'),
),
bottom: TabBar(
tabs: <Widget>[
Tab(text:"Home"),
Tab(text:"About")
],
),
),
body: TabBarView(
children: <Widget>[
Text("Home"),
Text("About")
],
),
),
);
}
}
Menu part as shown
class Menu extends StatelessWidget {
final parentScaffoldkey;
Menu({this.parentScaffoldkey});
#override
Widget build(BuildContext context) {
return new Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
dense: true,
title: new Text('My Text'),
onTap: () {
// On tap show a snackbar.
// ScaffoldMessenger will call the nearest Scaffold to show snackbar
ScaffoldMessenger.of(this.parentScaffoldkey.currentContext).showSnackBar(SnackBar(content:Text('Error. Could not log out')));
},
),
],
),
);
}
}
Also,you have to call snackbar via Scaffold because it provides the SnackBar API and manages it

Where to call web-services to fetch data in Flutter Widget?

I have the following screen:
import 'package:flutter/material.dart';
import '../models/patient.dart';
import '../components/patient_card.dart';
import '../services.dart';
class Home extends StatefulWidget {
var patients = <Patient>[];
#override
_HomeState createState() => new _HomeState();
}
class _HomeState extends State<Home> {
#override
initState() {
super.initState();
Services.fetchPatients().then((p) => setState(() => widget.patients = p));
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
),
body: new Container(
child: new ListView(
children: widget.patients.map(
(patient) => new PatientCard(patient),
).toList()
)
)
);
}
}
As you can see I do the endpoint call when I overwrite initState() in _HomeState. But it only runs once initially when the app starts. I can't just type r in my terminal and let the app hot reload and call the endpoint again.. I have to use Shift + r to do a full restart first.
So the question is, am I calling the web service in the recommended spot? And if it not... where does it go? Also, shouldn't ListView have a function / property that gets called on "pull to refresh" or something?
As mentioned by #aziza you can use a Stream Builder or if you want to call a function every time widget gets built then you should call it in build function itself. Like in your case.
#override
Widget build(BuildContext context) {
Services.fetchPatients().then((p) => setState(() => widget.patients = p));
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
),
body: new Container(
child: new ListView(
children: widget.patients.map(
(patient) => new PatientCard(patient),
).toList()
)
)
);
}
If you want to add pull-to-refresh functionality then wrap your widget in refresh indicator widget. Add your call in onRefresh property.
return new RefreshIndicator(child: //Your Widget Tree,
onRefresh: handleRefresh);
Note that this widget only works with vertical scroll view.
Hope it helps.
Have a look on StreamBuilder. This widget will allow you to deal with async data that are frequently updated and will update the UI accordingly by listening onValue at the end of your stream.
Flutter have FutureBuilder class, you can also create your widget as shown below
Widget build(BuildContext context) {
var futureBuilder = new FutureBuilder(
future: Services.fetchPatients().then((p) => setState(() => widget.patients = p)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
if (snapshot.data != null) {
return new Container(
child: new ListView(
children: snapshot.data.map(
(patient) => new PatientCard(patient),
).toList()
)
);
}
} else {
return new Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(16.0),
child: new CircularProgressIndicator());
}
});
return new Container(child: futureBuilder);
}
Example project : Flutter - Using the future builder with list view.

Flutter: Focus keyboard on back navigation

I want to focus a textField after the user navigated back with the back button in the navigation bar. How do you do that? I tried the autofocus property of the TextField widget. But this only works when navigating forward when the widget gets created for the first time. On iOS there is the viewDidAppear method, is there something similar in Flutter?
Thanks!
You will need to provide your TextField a FocusNode, and then you can await the user to go back and set FocusScope.of(context).requestFocus(myNode) when the navigation happens.
Simple demonstration:
class FirstPage extends StatefulWidget {
#override
_FirstPageState createState() => new _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
FocusNode n = new FocusNode();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title:new Text("First Page")),
body: new Center(
child: new Column(
children: <Widget>[
new TextField(
focusNode: n,
),
new RaisedButton(onPressed: ()async{
bool focus = await Navigator.of(context).push(new MaterialPageRoute(builder: (_)=>new SecondPage()));
if (focus == true|| focus==null){
FocusScope.of(context).requestFocus(n);
}
},
child: new Text("NAVIGATE"),),
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title:new Text("Second Page")),
body: new Center(
child: new RaisedButton(onPressed: (){Navigator.pop(context,true);},child: new Text("BACK"),),
),
);
}
}

Resources