Flutter Codelab: Update ListView on different screen - dart

I completed the Flutter NameGenerator code lab and wanted to extend it to remove items directly from the "Saved suggestions list".
To do so, I've added the onTap handler below which removes the pair from the list.
However, the list doesn't update until I navigate back and reopen the screen again.
How do I immediately update the list on the second screen?
void _pushSaved() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
final Iterable<ListTile> tiles = _saved.map((WordPair pair) {
return ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
onTap: () => setState(() {
_saved.remove(pair);
}),
);
});
final List<Widget> divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: const Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
}),
);
}

Why your code doesn't work
The reason your list doesn't update is that it's a different screen pushed on the Navigator.
Because your _pushSaved method is inside the original screen, you call setState on that screen and rebuild all the widgets of the original screen.
The pushed screen isn't affected because it's not a child of your original screen.
Rather, the original screen told the Navigator to create a new screen, so it's some subtree of the Navigator of your MaterialApp and not accessible to you.
Solution
Accessing the same live data on different screens is something that's not that easy to do just with StatefulWidgets.
Basically, your project has grown complex enough so that it's time to think about a more sophisticated state management solution.
Here's a video from Google I/O about state management that you could check out for some inspiration.

Related

Error thrown on navigator pop until : "!_debugLocked': is not true."

When popping a screen navigating to other one by clicking on the showBottomSheet, this error is thrown through the following code . I cant get why this is occurring.
class _CheckoutButtonState extends State<_CheckoutButton> {
final GlobalKey<ScaffoldState> _globalKey = GlobalKey();
final DateTime deliveryTime = DateTime.now().add(Duration(minutes: 30));
final double deliveryPrice = 5.00;
#override
Widget build(BuildContext context) {
SubscriptionService subscriptionService =
Provider.of<SubscriptionService>(context);
CheckoutService checkoutService = Provider.of<CheckoutService>(context);
return Container(
height: 48.0,
width: MediaQuery.of(context).size.width * 0.75,
child: StreamBuilder(
stream: subscriptionService.subscription$,
builder: (_, AsyncSnapshot<Subscription> snapshot) {
if (!snapshot.hasData) {
return Text("CHECKOUT");
}
final Subscription subscription = snapshot.data;
final List<Order> orders = subscription.orders;
final Package package = subscription.package;
num discount = _getDiscount(package);
num price = _totalPriceOf(orders, discount);
return StreamBuilder<bool>(
stream: checkoutService.loading$,
initialData: false,
builder: (context, snapshot) {
bool loading = snapshot.data;
return ExtendedFloatingActionButton(
loading: loading,
disabled: loading,
action: () async {
checkoutService.setLoadingStatus(true);
final subscription =
await Provider.of<SubscriptionService>(context)
.subscription$
.first;
try {
await CloudFunctions.instance.call(
functionName: 'createSubscription',
parameters: subscription.toJSON);
final bottomSheet =
_globalKey.currentState.showBottomSheet(
(context) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).primaryColor,
Theme.of(context).primaryColor,
],
stops: [-1.0, 0.5, 1.0],
),
),
child: Column(
children: <Widget>[
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding:
const EdgeInsets.only(bottom: 16.0),
child: Text(
"Thank you for your order",
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.display1,
),
),
SvgPicture.asset(
'assets/images/thumb.svg',
height: 120.0,
width: 100.0,
)
// CircleAvatar(
// radius: 40.0,
// backgroundColor: Colors.transparent,
// child: Icon(
// Icons.check,
// color: Theme.of(context)
// .textTheme
// .display1
// .color,
// size: 80.0,
// ),
// ),
],
),
),
Container(
width:
MediaQuery.of(context).size.width * 0.9,
height: 72.0,
padding: EdgeInsets.only(bottom: 24),
child: ExtendedFloatingActionButton(
text: "ORDER DETAILS",
action: () {
Navigator.of(context).pop();
},
),
),
],
),
);
},
);
bottomSheet.closed.then((v) {
Navigator.of(context)
.popUntil((r) => r.settings.isInitialRoute);
});
} catch (e) {
print(e);
final snackBar =
SnackBar(content: Text('Something went wrong!'));
Scaffold.of(context).showSnackBar(snackBar);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"CHECKOUT ",
style: Theme.of(context)
.textTheme
.display4
.copyWith(color: Colors.white),
),
Text(
"EGP " +
(price + (orders.length * deliveryPrice))
.toStringAsFixed(2),
style: Theme.of(context)
.textTheme
.display4
.copyWith(color: Theme.of(context).primaryColor),
),
],
),
);
});
},
),
);
}
num _totalPriceOf(List<Order> orders, num discount) {
num price = 0;
orders.forEach((Order order) {
List<Product> products = order.products;
products.forEach((Product product) {
price = price + product.price;
});
});
num priceAfterDiscount = price * (1 - (discount / 100));
return priceAfterDiscount;
}
num _getDiscount(Package package) {
if (package == null) {
return 0;
} else {
return package.discount;
}
}
}
Error :
>══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter (24830): The following assertion was thrown building Navigator-[GlobalObjectKey<NavigatorState>
I/flutter (24830): _WidgetsAppState#90d1f](dirty, state: NavigatorState#6b2b6(tickers: tracking 1 ticker)):
I/flutter (24830): 'package:flutter/src/widgets/navigator.dart': Failed assertion: line 1995 pos 12: '!_debugLocked':
I/flutter (24830): is not true.
I/flutter (24830): Either the assertion indicates an error in the framework itself, or we should provide substantially
I/flutter (24830): more information in this error message to help you determine and fix the underlying cause.
I/flutter (24830): In either case, please report this assertion by filing a bug on GitHub:
I/flutter (24830): https://github.com/flutter/flutter/issues/new?template=BUG.md
I/flutter (24830): When the exception was thrown, this was the stack:
Instead of giving you a direct answer, I'm going to walk you through how I thought about this when I saw the question, in the hope that it'll help you in the future.
Let's take a look at the assertion. It says Failed assertion: line 1995 pos 12: '!_debugLocked': I/flutter (24830): is not true.. Hmm, interesting. Let's take a look at that line of code.
assert(!_debugLocked);
Well, that doesn't give me much more information, let's look at the variable.
bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends
That's better. It's there to prevent re-entrant calls to push, pop, etc (by that it means that it doesn't want you calling 'push', 'pop', etc from within a call to 'push', 'pop'). So let's trace that back to your code.
This seems like the likely culprit:
bottomSheet.closed.then((v) {
Navigator.of(context)
.popUntil((r) => r.settings.isInitialRoute);
});
I'm going to skip a step here and use deductive reasoning instead - I'm betting that the closed future is finished during a pop. Go ahead and confirm that by reading the code if you feel like it.
So, if the issue is that we're calling pop from within a pop function, we need to figure out a way to defer the call to pop until after the pop has completed.
This becomes quite simple - there's two ways to do this. The simple way is to just use a delayed future with zero delay, which will have dart schedule the call as soon as possible once the current call stack returns to the event loop:
Future.delayed(Duration.zero, () {
Navigator. ...
});
The other more flutter-y way of doing it would be to use the Scheduler to schedule a call for after the current build/render cycle is done:
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator. ...
});
Either way should eliminate the problem you're having.
Another option is also possible though - in your ExtendedFloatingActionButton where you call pop:
ExtendedFloatingActionButton(
text: "ORDER DETAILS",
action: () {
Navigator.of(context).pop();
},
),
you could instead simply do the call to Navigator.of(context).popUntil.... That would eliminate the need for the doing anything after bottomSheet.closed is called. However, depending on whatever else you might or might not need to do in your logic this may not be ideal (I can definitely see the issue with having the bottom sheet set off a change to the main part of the page and why you've tried to make that happen in the page's logic).
Also, when you're writing your code I'd highly recommend separating it into widgets - for example the bottom sheet should be its own widget. The more you have in a build function, the harder it is to follow and it can actually have an effect on performance as well. You should also avoid using GlobalKey instances wherever possible - you can generally either pass objects (or callbacks) down if it's only through a few layers, use the .of(context) pattern, or use inherited widgets.
For those who are invoking the Navigator as part of the build process. I found that it will intermittently throwing asserting error on the debugLocked
I avoided this issue by wrapping with a addPostFrameCallback:
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => MyPage()));
});
I had this same issue any answer not worked for me and this error doesn't explain any thing.
After going each line code i found that we cannot launch any state in build method like this
#override
Widget build(BuildContext context) {
var viewmodel = Provider.of<ViewModel>(context);
Navigator.of(context).push(MaterialPageRoute(builder:
(context)=>CreateItemPage(viewmodel.catalogData))); // this is way i was getting error.
return Scaffold();
}
I was getting error in CreateItemPage screen because of that line.
Solution of this issue create button which call this line Navigator.of(context).push(MaterialPageRoute(builder: (context)=>CreateItemPage(viewmodel.catalogData)));
For me, it was coming because I created a cycle of pushes that was causing this error.
For example,
In the Initial route which was /loading the code was pushing /home
class _LoadingState extends State<Loading> {
void getTime() async {
// DO SOME STUFF HERE
Navigator.pushNamed(context, '/home');
}
#override
void initState() {
super.initState();
getTime();
}
And in the /home initState I was pushing /loading creating a cycle.
class _HomeState extends State<Home> {
#override
void initState() {
super.initState();
Navigator.pushNamed(context, '/loading');
}
Add Some Delay Then Try to do this Your Problem will be Solved :
Future.delayed(const Duration(milliseconds: 500), () {
// Here you can write your code
setState(() {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => SetCategory()),
(route) => false);
});
});
I had similar error, like a dialog box which had a logout button, which when pressed goes to login screen, but an _debugLocked error occurs, so I used
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);
This removes all routes in the stack so that user cannot go back to the previous routes after they have logged out.
Setting (Route<dynamic> route) => false will make sure that all routes before the pushed route are removed.
I don't know if this is the "real" solution but it helped me as a beginner to Flutter.
I've gotten this error due to a typo accidentally calling Navigator.of(context).push during my build():
E/flutter ( 6954): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: 'package:flutter/src/widgets/navigator.dart': Failed assertion: line 2845 pos 18: '!navigator._debugLocked': is not true.
The simulator flashed a more informative error:
setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework
is already in the process of building widgets. A widget can be marked as needing
to be built during the build phase only if one of its ancestors is currently
building. This exception is allowed because the framework builds parent widgets
before children, which means a dirty descendant will always be built.
Otherwise, the framework might not visit this widget during this build phase The
widget on which setState() or markNeedsBuild() was called was:
Overlay-[LabeledGlobalKey<OverlayState>#de69b]
The widget which was currently being built when the offending call was made was:
FutureBuilder
Basically, you should not be trying to push/pop to a new route in the middle of a build. If you really need to, wait for the build to finish, which is why others are suggesting wrapping it in a SchedulerBinding.instance.addPostFrameCallback to execute after everything is rendered, but you probably should find a better way to do this outside of a build.
In my case, I typed:
onTap: _onTap(context),
when I really meant to type:
onTap: () => _onTap(context),
my _onTap handler was doing the Navigator push. I had forgotten to wrap my handler in a closure that captures the context it needs, and instead actually was executing it instead of passing onTap: my callback.
Dialog solution
For those who encounter this when calling Navigator.push(..) from a Dialog.
You need to do Navigator.pop(context);to programmatically close modal first, then call Navigator.push(..).
For people encountering this issue while using bloc, make sure you are using navigation in a BlocListener (or BlocConsumer's listener). In my case I was using Navigator inside BlocBuilder. I am new to Flutter/Bloc and the accepted answer resolved the problem, but was not the proper solution. Switching my BlocBuilder to a BlocConsumer allowed for me to navigate during specific states.
Example of using BlocConsumer, navigate when state is 'LoginSuccess':
BlocConsumer<LoginBloc, LoginState>(
listener: (BuildContext context, state) {
if (state is LoginSuccess) {
Navigator.of(context).pushReplacement(
// Add your route here
PageRouteBuilder(
pageBuilder: (_, __, ___) => BlocProvider.value(
value: BlocProvider.of<NavigationBloc>(context),
child: HomeScreen(),
),
),
);
}
},
// Only build when the state is not LoginSuccess
buildWhen: (previousState, state) {
return state is! LoginSuccess;
},
// Handle all states other than LoginSuccess here
builder: (BuildContext context, LoginState state) {
if (state is LoginLoading) {
return Center(child: CircularProgressIndicator());
} else .....
In resume, you just need to remove it from your initState.
I would recomend extend the class with AfterLayout and inside the
afterFirstLayout you can redirect it to the page you want. This will garantee that everything is ok before routing.
See bellow the steps:
Add to pubspec: after_layout: ^1.0.7+2
Then, You will extend it to the class you want to use. In my case was a statefull widget named HomePage. So it will looks like:
class HomePage extends StatefulWidget {
#override
HomePageState createState() => HomePageState();
} //no changes here
class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
//the with AfterLayoutMixin<pageName> is the only thing you need to change.
Now, you need to implement a method called afterlayout, that will be executed after the build is completed.
#override
Future<void> afterFirstLayout(BuildContext context) {
//your code here safetly
}
You can find information here:
https://pub.dev/packages/after_layout
For those that still have the same issue, this help me to solve it.
navigationService.popUntil((_) => true);
navigationService.navigateTo(
'authentication',
);
basically i wait until the navigation finish setting everything and then call the navigateTo.
I got this error because my initialRoute was /login. However, the initialRoute is required to be /.
If the route name starts with a slash, then it is treated as a "deep link", and before this route is pushed, the routes leading to this one are pushed also. For example, if the route was /a/b/c, then the app would start with the four routes /, /a, /a/b, and /a/b/c loaded, in that order.
Here is a link to the docs for reference.
I had the same issue and took me some time to figure out. I was listening to the state on the screen based on which it will navigate to different screen. And then on button click I was changing that state and navigating to different screen which was causing an issue.
I am using flutter version 2.3.3
I also faced this issue when i try to pop back into my home screen from a second screen with command Navigator.pop(context)
I solved this issue by replacing this line of code with Navigator.of(context).pop(context)
It worked fine for me hope it hepls

Flutter bring up a list when a button is pressed, and close said list after an option is selected

I'm building an application for a building that can navigate a user, one of the ways I am doing this is by using a floor plan of the building and I want to draw a path between nodes in this floor plan to create a route. The user enters where they want to be and then route finding algorithm outputs a path, the way I want to build this path is by having a user select a source node and a target node from two seperate lists, I want them to press a button on the map view screen and have a list with these nodes appear but no matter what I try the list will not display.
I've tried using setState and having a ListView returned but that seems to be where it's failing as such as I have print statements to help verify where in the code I've reached, I don't know if it's the search terms I'm using but nothing I have found so far seems to be related to this kind of use case. This is similar to what I want but this is just a list already being displayed and then being updated.
Widget build(BuildContext context) {
//currently inside of a scaffold
bottomNavigationBar: BottomAppBar(
child: new Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: Icon(Icons.trip_origin),
onPressed: () {
setState(() {
_sourceList();
});
},
),
],
)),
//extra code as part of scaffold
}
Widget _sourceList() {
print("working1"); //this prints
return ListView.builder(
padding: const EdgeInsets.all(10.0),
itemBuilder: (context, i) {
print("working2"); //this does not
return _buildRow();
});
}
Widget _buildRow() {
return new ListTile(title: Text(a.name));
}
So basically I just want a list to display when the user presses a button, then have that list disappear after a selection is made. The actual result is I can press the button and nothing is displayed, I only reach working1 in the code not working2.

Remove Index wise CustomWidget from List<Widget> in Flutter

I have initially empty list of Widget in Column. Now on Other widget click I am adding new Custom Widget in _contactItems
Column(
children: _contactItems,
)
List<Widget> _contactItems = new List<CustomWidget>();
_contactItems.add(newCustomWidget(value));
Now Suppose I have 6 Records (6 Custom Widgets in Column). I am trying to remove index wise records (Example. I am removing 3rd record then 1st record. Column Widgets (dynamic widgets) should be updated as _contactItems updating in setState())
Now on CustomWidget click I am removing that particular CustomWidget from Column.
setState(() {
_contactItems.removeAt(index);
});
Also tried with
_contactItems.removeWhere((item) {
return item.key == _contactItems[index].key;
});
Try this (assuming that your Column widget keys have this format):
setState(() {
this._contactItems.removeWhere((contact) => contact.key == Key("index_$index"));
});
If this doesn't solve your issue, maybe we'll need more info.
If you want to manipulate a ListView or GridView it is important that you assign a Key to each child Widget of the List/GridView
In short Flutter compares widgets only by Type and not state. Thus when the state is changed of the List represented in the List/GridView, Flutter doesn't know which children should be removed as their Types are still the same and checks out. The only issue Flutter picks up is the number of items, which is why it only removes the last widget in the List/GridView.
Therefore, if you want to manipulate lists in Flutter, assign a Key to the top level widget of each child. A more detailed explanation is available in this article.
This can be achieved be adding
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
children: List.generate(urls.length, (index) {
//generating tiles with from list
return GestureDetector(
key: UniqueKey(), //This made all the difference for me
onTap: () => {
setState(() {
currentUrls.removeAt(index); // deletes the item from the gridView
})
},
child: FadeInImage( // A custom widget I made to display an Image from
image: NetworkImage(urls[index]),
placeholder: AssetImage('assets/error_loading.png')
),
);
}),
);

ListView animation when item is deleted using Dismissible

I'm using Dismissible to dismiss the items, but when an item is dismissed I get default boring animation. Is there a way to change that animation like Gmail does?
Example:
My own animation (not smooth)
So, in my animation, you can see slight pause when the item is deleted and next item coming up on the screen taking up old item position.
That's the default animation of Dismissible.
List<String> content;
ListView.builder(
itemCount: content.length,
itemBuilder: (context, index) {
return Dismissible(
key: ValueKey(content[index]),
onDismissed: (_) {
setState(() {
content = List.from(content)..removeAt(index);
});
},
background: Container(color: Colors.green),
child: ListTile(
title: Text(content[index]),
),
);
},
)
Thanks to #Rémi Rousselet for his efforts.
Finally I found the reason for that ugly animation. Never use itemExtent when you are planning to use Dismissible. I was mad, I used it.

Swipe to go back gesture flutter

How do i implement the swipe from the left to go back gesture in flutter? Not sure if it was already implemented automatically for iOS, but I wanted it for Android as well (as things are becoming gesture based).
Use CupertinoPageRoute to make it work on Android;
import 'package:flutter/cupertino.dart';
(as answered on How to implement swipe to previous page in Flutter?)
You could set your Theme.platform to TargetPlatform.ios. This will make use that the swipe back gesture is used on every device.
You can use CupertinoPageRoute() as Tom O'Sullivan said above.
However, if you want to customize it (eg. using custom transition duration) using PageRouteBuilders and get the same swipe to go back gesture, then you can override buildTransitions().
For iOS, the default PageTransitionBuilder is CupertinoPageTransitionsBuilder(). So we can use that in buildTransitions(). This automatically give us the swipe right to go back gesture.
Here's some sample code for the CustomPageRouteBuilder:
class CustomPageRouteBuilder<T> extends PageRoute<T> {
final RoutePageBuilder pageBuilder;
final PageTransitionsBuilder matchingBuilder = const CupertinoPageTransitionsBuilder(); // Default iOS/macOS (to get the swipe right to go back gesture)
// final PageTransitionsBuilder matchingBuilder = const FadeUpwardsPageTransitionsBuilder(); // Default Android/Linux/Windows
CustomPageRouteBuilder({this.pageBuilder});
#override
Color get barrierColor => null;
#override
String get barrierLabel => null;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return pageBuilder(context, animation, secondaryAnimation);
}
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 900); // Can give custom Duration, unlike in MaterialPageRoute
#override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return matchingBuilder.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
}
Then to go to a new page:
GestureDetector(
onTap: () => Navigator.push(
context,
CustomPageRouteBuilder(pageBuilder: (context, animation, secondaryAnimation) => NewScreen()),
),
child: ...,
)
You can set the platform of your theme (and darkTheme) to TargetPlatform.iOS, you can set the pageTransitionsTheme of your themes to,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
and you can load the new page using CupertinoPageRoute ... and none of that will work until you make sure to use Navigator.push (instead of Navigator.pushReplacement) to get to that new screen! I hope this helps anyone out there who was working with existing transitions and didn't notice this crucial detail. :)
Use this plugin:
https://pub.dev/packages/cupertino_back_gesture
A Flutter package to set custom width of iOS back swipe gesture area.
For basic use:
import 'package:cupertino_back_gesture/cupertino_back_gesture.dart';
BackGestureWidthTheme(
backGestureWidth: BackGestureWidth.fraction(1 / 2),
child: MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
//this is default transition
//TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
//You can set iOS transition on Andoroid
TargetPlatform.android: CupertinoPageTransitionsBuilderCustomBackGestureWidth(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilderCustomBackGestureWidth(),
},
),
),
home: MainPage(),
),
)
More details on plugin's page
in my case, the solution turned out to be very simple. I just used context.push('screen') instead of context.go('/screen')
This should not be implemented on Android since it makes interactions inconsistent across the OS.
Swiping from the screens edge to go back is not something that Android wants you to implement, so you should better don't do it.

Resources