The issue:
I have 2 tabs using Default Tabs Controller, like so:
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
drawer: Menu(),
appBar: AppBar(
title: Container(
child: Text('Dashboard'),
),
bottom: TabBar(
tabs: <Widget>[
Container(
padding: EdgeInsets.all(8.0),
child: Text('Deals'),
),
Container(
padding: EdgeInsets.all(8.0),
child: Text('Viewer'),
),
],
),
),
body: TabBarView(
children: <Widget>[
DealList(),
ViewersPage(),
],
),
),
);
}
}
The DealList() is a StatefulWidget which is built like this:
Widget build(BuildContext context) {
return FutureBuilder(
future: this.loadDeals(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
print('Has error: ${snapshot.hasError}');
print('Has data: ${snapshot.hasData}');
print('Snapshot data: ${snapshot.data}');
return snapshot.connectionState == ConnectionState.done
? RefreshIndicator(
onRefresh: showSomething,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
print('A Deal: ${deal}');
return _getDealItem(deal, context);
},
),
)
: Center(
child: CircularProgressIndicator(),
);
},
);
}
}
With the above, here's what happens whenever I switch back to the DealList() tab: It reloads.
Is there a way to prevent re-run of the FutureBuilder when done once? (the plan is for user to use the RefreshIndicator to reload. So changing tabs should not trigger anything, unless explicitly done so by user.)
There are two issues here, the first:
When the TabController switches tabs, it unloads the old widget tree to save memory. If you want to change this behavior, you need to mixin AutomaticKeepAliveClientMixin to your tab widget's state.
class _DealListState extends State<DealList> with AutomaticKeepAliveClientMixin<DealList> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context); // need to call super method.
return /* ... */
}
}
The second issue is in your use of the FutureBuilder -
If you provide a new Future to a FutureBuilder, it can't tell that the results would be the same as the last time, so it has to rebuild. (Remember that Flutter may call your build method up to once a frame).
return FutureBuilder(
future: this.loadDeals(), // Creates a new future on every build invocation.
/* ... */
);
Instead, you want to assign the future to a member on your State class in initState, and then pass this value to the FutureBuilder. The ensures that the future is the same on subsequent rebuilds. If you want to force the State to reload the deals, you can always create a method which reassigns the _loadingDeals member and calls setState.
Future<...> _loadingDeals;
#override
void initState() {
_loadingDeals = loadDeals(); // only create the future once.
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context); // because we use the keep alive mixin.
return new FutureBuilder(future: _loadingDeals, /* ... */);
}
Related
I am trying to create a UX that looks like WhatsApp Dashboard in Flutter. I created a Scaffold with an AppBar and put the TabBar in the bottomNavigationBar slot instead of the bottom slot of the AppBar. Each of the TabBarView children is a StreamBuilder that listens to particular stream. The problem is that whenever the stream emits a value the StreamBuilder rebuilds (checked via logging build function) but the UI doesn't update until I switch tabs and come back to the tab.
I have tried creating a stateful widget that hosts the StreamBuilder and instantiating that as a child of the TabBarView. I also tried adding a listener to the stream and calling setState there but it didn't work either.
I expect the page to update the UI whenever a chat message is received but it doesn't update until I switch tabs.
body: TabBarView(
controller: _tabController,
children: <Widget>[
ChatListView(),
...
class ChatListView extends StatefulWidget {
#override
_ChatListViewState createState() => _ChatListViewState();
}
class _ChatListViewState extends State<ChatListView>
with AutomaticKeepAliveClientMixin {
List<ListTile> itemList = <ListTile>[];
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: chatsListBloc.chatList,
builder: (context, snapshot) {
print("rebuilt");
if (!snapshot.hasData) {
chatsListBloc.fetchChatList();
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'You have not started any chat yet. To start a chat, click on the Start Chat icon.',
textAlign: TextAlign.center,
),
),
);
} else {
List<ChatListItem> dataList = List<ChatListItem>.from(snapshot.data);
itemList.clear();
for (int i = 0; i < dataList.length; i++) {
itemList.add(ListTile(
onTap: () {
}
},
title: Text(dataList[i].displayName),
subtitle: dataList[i].lastMessage,
leading: CircleAvatar(
backgroundColor: Colors.blueGrey,
backgroundImage:MemoryImage(dataList[i].avatar),
child: Stack(
children: <Widget>[
Icon(
Icons.account_circle,
size: 40,
),
(dataList[i].type == ChatType.Incognito)
? Icon(Icons.lock,
color: Colors.blueGrey[700], size: 10)
: Container(),
],
),
),
trailing: StreamBuilder(
stream: Stream.periodic(Duration(seconds: 1),
(computationCount) => computationCount)
.asBroadcastStream(),
builder: (context, snapshot) => Text(timeLabel(
DateTime.fromMillisecondsSinceEpoch(
dataList[i].lastAccessed))),
)));
}
return ListView(
children: itemList,
);
}
}
});
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => false;
}
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
How would I properly access the _runThisFunction(...) within the onTap()?
...
class _DealList extends State<DealList> with AutomaticKeepAliveClientMixin {
void _runThisFunction() async {
print('Run me')
}
#override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: _loadingDeals,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return snapshot.connectionState == ConnectionState.done
? RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
return _getDealItem(deal, context);
},
),
)
: Center(
child: CircularProgressIndicator(),
);
},
);
}
}
Container _getDealItem(Map deal, context) {
return new Container(
height: 90.0,
child: Material(
child: InkWell(
child: _getDealRow(deal), // <-- this renders the row with the `deal` object
onTap: () {
// Below call fails
// 'The function isn't defined'
_runThisFunction();
},
),
),
);
}
The reason for that is that you are out of scope.
Little hint: The word "function" always indicates that the function you are trying to call is not part of a class and the keyword "method" shows you that the function you are trying to call is part of a class.
In your case, _runThisFunction is defined inside of _DealList, but you are trying to call it from outside.
You either need to move _getDealItem into _DealList or _runThisFunction out.
/// In this case both methods [_runThisFunction()] and [_getDealItem()] are defined inside [_DealList].
class _DealList extends State<DealList> with AutomaticKeepAliveClientMixin {
void _runThisFunction() ...
Container _getDealItem() ...
}
/// In this case both functions are defined globally.
void _runThisFunction() ...
Container _getDealItem() ...
You wil need to make sure that you also apply the same logic to _getDealRow and other nested calls.
Our app is built on top of Scaffold and to this point we have been able to accommodate most of our routing and navigation requirements using the provided calls within NavigatorState (pushNamed(), pushReplacementNamed(), etc.). What we don't want though, is to have any kind of 'push' animation when a user selects an item from our drawer (nav) menu. We want the destination screen from a nav menu click to effectively become the new initial route of the stack. For the moment we are using pushReplacementNamed() for this to ensure no back arrow in the app bar. But, the slide-in-from-the-right animation implies a stack is building.
What is our best option for changing that initial route without animation, and, can we do that while also concurrently animating the drawer closed? Or are we looking at a situation here where we need to move away from Navigator over to just using a single Scaffold and updating the 'body' directly when the user wants to change screens?
We note there is a replace() call on NavigatorState which we assume might be the right place to start looking, but it's unclear how to access our various routes originally set up in new MaterialApp(). Something like replaceNamed() might be in order ;-)
What you're doing sounds somewhat like a BottomNavigationBar, so you might want to consider one of those instead of a Drawer.
However, having a single Scaffold and updating the body when the user taps a drawer item is a totally reasonable approach. You might consider a FadeTransition to change from one body to another.
Or, if you like using Navigator but don't want the default slide animation, you can customize (or disable) the animation by extending MaterialPageRoute. Here's an example of that:
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyCustomRoute<T> extends MaterialPageRoute<T> {
MyCustomRoute({ WidgetBuilder builder, RouteSettings settings })
: super(builder: builder, settings: settings);
#override
Widget buildTransitions(BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
if (settings.isInitialRoute)
return child;
// Fades between routes. (If you don't want any animation,
// just return child.)
return new FadeTransition(opacity: animation, child: child);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Navigation example',
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/': return new MyCustomRoute(
builder: (_) => new MyHomePage(),
settings: settings,
);
case '/somewhere': return new MyCustomRoute(
builder: (_) => new Somewhere(),
settings: settings,
);
}
assert(false);
}
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Navigation example'),
),
drawer: new Drawer(
child: new ListView(
children: <Widget> [
new DrawerHeader(
child: new Container(
child: const Text('This is a header'),
),
),
new ListTile(
leading: const Icon(Icons.navigate_next),
title: const Text('Navigate somewhere'),
onTap: () {
Navigator.pushNamed(context, '/somewhere');
},
),
],
),
),
body: new Center(
child: new Text(
'This is a home page.',
),
),
);
}
}
class Somewhere extends StatelessWidget {
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Text(
'Congrats, you did it.',
),
),
appBar: new AppBar(
title: new Text('Somewhere'),
),
drawer: new Drawer(
child: new ListView(
children: <Widget>[
new DrawerHeader(
child: new Container(
child: const Text('This is a header'),
),
),
],
),
),
);
}
}
Use PageRouteBuilder like:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => Screen2(),
transitionDuration: Duration.zero,
),
);
And if you want transition, simply add following property to above PageRouteBuilder, and change seconds to say 1.
transitionsBuilder: (_, a, __, c) => FadeTransition(opacity: a, child: c),
I made a GridView with children that each has a GestureDetector and a onTap method set. But the onTap event gets called only when the view is created and not when the item has been tapped. What am I doing wrong here?
class MyGridView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Expanded(
child: new GridView.count(
crossAxisCount: 2,
children: [
new GridItem(0),
new GridItem(1)
]
)
)
]
);
}
}
class GridItem extends StatelessWidget {
final int code;
GridItem(this.code);
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTap: print(code),
child: new Container(
height: 48.0,
child: new Text('$code')
)
);
}
}
You want:
onTap: () { print(code); },
What you're doing is calling print, then saving the return value from print (which will be a null) as the onTap handler, which actually disables the onTap handler. If you see anything in the logs it'll be from the time you actually did the build, not when you tapped.