How to dynamically disable vertical swipe in PageView - flutter

I would like to find a way to update the physics param to disable the swipe action in a parent PageView from a child widget.
I am using riverpod for updating the state in a child widget when it builds to know when I should pass NeverScrollableScrollPhysics to the physics param in a parent widget. But the thing is that this approach is causing my child widget to rebuild recursively, because this is making the PageView rebuild to update the physics param. So I really don't know what to do here.
I have this parent Widget that builds the PageView:
Widget build(BuildContext context) {
final _navBar = useProvider(bottomNavigationBarProvider);
return PageView(
physics: navBar.isSwipeBlocked ? const NeverScrollableScrollPhysics() : null,
controller: pageController,
onPageChanged: onPageChanged,
children: [
Beamer(
key: const Key('feed-tab'),
routerDelegate: BeamerDelegate(locationBuilder: (state) => FeedLocation(state)),
),
]
)
}
And the child Widget that updates the state variable:
Widget build(BuildContext context) {
final _navBar = useProvider(bottomNavigationBarProvider.notifier);
useEffect(() {
Future.microtask(() => _navBar.blockSwipe());
}, []);
return Container(...);
}
So when FeedLocation loads, it updates _navBar in an attempt for disabling the scroll behavior. But as I mentioned, this causes the parent to rebuild and FeedLocation to build again and then the recursive state..
The idea was to be able to go to FeedLocation, disable the scroll, then when go back, enable it again, but I don't see a solution for that.
I think I did already what this guy suggested https://github.com/flutter/flutter/issues/37510#issuecomment-738051469 using Riverpod
And I guess I am a similar situation as this guys from the same thread https://github.com/flutter/flutter/issues/37510#issuecomment-864416592
Is anybody able to see a solution or what I am doing wrong?

You should replace null as the secondary ScrollPhysics with PageScrollPhysics() and make sure to add setState(() {}); when you update the isSwipeBlocked variable.

Related

Scroll position lost if tab changed while scrolling animation is still ongoing

I have 3 views which are accessible via the bottom navigation tab. Each view has its own ListView, which looks like this:
// primary = bottomTabNavigation.index //
ListView(
controller: primary ? null : scrollController,
key: const PageStorageKey<String>('view1'),
primary: primary,
physics: primary
? AlwaysScrollableScrollPhysics()
: NeverScrollableScrollPhysics(),
children: const [
Text("A"),
SizedBox(height: 1000),
Text("B"),
],
),
If I start a big swipe on view1, and switch to view2 via bottom tab navigator, the scroll position when I come back to view1 is still at the top. Somehow, the scroll position only saves upon the scrolling animation completing.
Is there some way to switch tabs and store the last position (without waiting for animation)?
Create a Key outside the build method
final _key = GlobalKey();
Step 1: make your widgets staefulWidget.
Step 2: now use AutomaticKeepAliveClientMixin using with keyword.
class _DealListState extends
State<DealList> with
AutomaticKeepAliveClientMixin<DealList>
{
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
// your current widget build
}}
It will keep your listview and other states when you moves from one page to another.
Note: if it's impossible to change every page to stateful widget then just make a new StatefulWidget that use AutomaticKeepAliveClientMixin and will take a child widget from outside and now you can use this widget to wrap your already present widget and can be used through the app.

How to refresh ListView after Sorting in Flutter?

I have a ListView which contains my customers. This ListView currently is a Stateless Widget. After sorting the list of customers inside the parent, I need to refresh the list.
parent where the List is displayed (Parent is stateful):
MyLieferListe(
bestellungen.anzahlPositionen,
bestellungen.kunde,
notifyParent: refresh,
),
MyLieferListe (currently Stateless):
SingleChildScrollView(
child: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
getBeleg();
this.notifyParent(kunden[index]);
},
child: MyBestellungText(
kunden[index].kundenbezeichnung,
kunden[index].kundenNr,
"${kunden[index].strasse}, ${kunden[index].plz} ${kunden[index].ort}",
kunden[index].tourHinweis),
);
},
itemCount: kunden.length,
),
),
How to refresh the ListView of the Child which is displayed inside of the parent?
Firstly you have to make your list stateful because on changing the list. You have to call setState.
A bad solution would be to use Navigator.pushReplacement.
A good solution would be to make your list a stateful widget.
A state change triggers a rebuild in Flutter, so in order to ensure your widget rebuilds after you sort the list, you should call setState
// a state variable
List<Kunde> kunden;
onSortButtonClick() {
List<Kunde> newKunden = kunden.sort(...);
// store to the state
setState((){
kunden = newKunden;
});
}
build(WidgetContext context) {
return ListView.builder(/* use kunden, from state, in here */)
}
As you mention, your parent is already stateful. You should move bestellungen.kunde to the state variables (set it first in initState and use setState every time you change it), then it should automatically rebuild widget if you update kunden via setState.
Alternatively, you could make the child widget stateful and put kunden in the state of that widget. Theoretically, that should be slightly better since you then only rebuild the child, not the parent, but it realistically shouldn't make much difference in this case.

How do I keep my widget state when reordering them via Draggable&DragTarget?

I'm using this reorderables package. This package works by having a list of children widgets that are each wrapped with a Draggable and put inside a DragTarget. Before that the childs key is assigned to a GlobalObjectKey.
After the dragTarget is created, it is assigned(or rebuild?) to a KeyedSubTree:
dragTarget = KeyedSubtree(key: keyIndexGlobalKey, child: dragTarget);
According to the comments in the package source code, this should preserve the child widgets state (toWrap) when being dragged:
// We pass the toWrapWithGlobalKey into the Draggable so that when a list
// item gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap.key);
The reordering itself happens not with the DragTarget accepting the Draggable dragged into it, but rather by using the DragTarget around each child to get index of the current position the Draggable is hovering over. When the Draggable is let go, a reorder function will get called, which removes the widget (that was being dragged) from the list and inserting it into the new position.
Now comes my problem: The state of the widget is not being preserved. I made a simple TestWidget to test this:
class TestWidget extends StatefulWidget{
#override
_TestWidgetState createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
Color boxColor;
#override
void initState() {
super.initState();
boxColor= Colors.blue;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: BoxDecoration(color: boxColor),
child: Text("Test"),
),
FlatButton(
onPressed: (){
setState(() {
boxColor = Colors.red;
});
},
padding: EdgeInsets.all(8.0),
child: Text("Change to Red"),
color: Colors.grey,
)
],
);
}
}
This widget has a Container with a initial blue background (boxColor) and a button. When the button is pressed, it will change the boxColor to red. The moment the dragging on the widget is initiated, it is rebuild and defaults to the initial state (at least the Draggable feedback is). After the reordering that doesn't change and the widget is still in it's default state.
My plan here is to have a list of different custom widgets, where the User can modify their content and if they are not happy with the order, they can drag those widgets around and rearrange them.
My question is: How do I preserve the state of my widgets?
I'm thinking of creating a class for each widget with all state relevant variables and use that to build my widgets but that seems very bloated and not really in the mind of flutter. Isn't that supposed to be the role of the state of the StatefulWidget?
EDIT:
So I solved my problem by creating an additional class for my widget state with ChangeNotifier and then moving all my variables that I want to keep track of into this class. So I basically now have two lists, one for my widgets in the reorderable list and one for their states. I still think that this is kinda scuffed. If a widget in my list has additional children of its own, I would need to create separate state classes for each of them that need it and save them somewhere. This can get very messy, very quickly.

Adding OverlayEntry in Flutter

I am trying to insert a Container to the Overlay, but I had an error with this code.
class _MyHomePageState extends State<MyHomePage> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
final entry = OverlayEntry(builder: (BuildContext overlayContext) {
return Container(
height: 50.0,
width: 50.0,
color: Colors.blue,
);
});
_addOverlay(entry);
}
void _addOverlay(OverlayEntry entry) async {
Overlay.of(context).insert(entry);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter'),
),
body: Center(),
);
}
}
This is 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...
Thank you in advance.
Since the last update to flutter 0.8.1 I noticed this change too. I fixed this to add the overlay after a minimal delay
Timer.run(() { Overlay.of(context).insert(calendarOverlay);});
Now this works but it feels like a hack...
So in my build i use this code when the overlay should present itself..
If anyone has a better solution, I am interested ;-)
John
UPDATE: I found this code to be working too:
final overlay = Overlay.of(context);
WidgetsBinding.instance.addPostFrameCallback((_) => overlay.insert(entry));
It saves me from including timers...
Just share some of my findings. I am about to implement overlay in my app too. So found this SO question by searching.
Many people build overlay before the normal widget. For example, in your code, the overlay insert in didChangeDependencies is called before building the Scaffold. This is the cause of all the async problems. I found people do this (couple the overlay insert and corresponding normal widget in a stateful widget) is because they want to find the corresponding child widget's position, but the child widget is build after the overlay insert call, thus the overlay insert has to be in an async function.
But If you just call overlay insert after building the normal widget (make overlay insert call independent from building the base widget. Separate/decouple them), you won't need any async or Timer functions at all. In my current implementation, I separate them just to make the code safe (I feel it's safer). So no need for any async calls.

Flutter: Use Navigator with TabBar and TabBarView

I'm trying to understand the class Navigator.
A Navigator Route seem to always return a new widget but what if I want to manage the TabBar and TabBarView so that each Tab when tapped or swipe on, will be pushed to the Navigator stack, I don't find a what to do that.
On a more general case, can I react to a route change without creating a new widget but instead taking another action like scrolling to a specific item in a listView?
I've tried recreating the entire app structure every time but doing this way I don't have the nice default animation and, also, doesn't seem a good approach to me.
You can use WillPopScope widget and its onWillPop to catch the back button pressure and handle it yourself. Find more info here https://docs.flutter.io/flutter/widgets/WillPopScope-class.html
On a more general case, can I react to a route change without creating
a new widget but instead taking another action like scrolling to a
specific item in a listView?
This looks more specific rather than general. However, you do need to set a ScrollController in your ListView and let it scroll the list for you to the desired point. A simple example function returning to top:
class MyFancyClass extends StatelessWidget{
...
ScrollController _scrollController;
...
#override
Widget build(BuildContext Context){
return ....
new ListView(
...
controller: _scrollController,
...
}
void _toTop() {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.ease,
);
}
Check https://docs.flutter.io/flutter/widgets/ScrollController-class.html for more details and behaviors. In case you want the back button to bring you to the top:
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onTop,
child:
...
),
);
}
Concerning the tab behavior I suggest you to read https://docs.flutter.io/flutter/material/TabController-class.html to better understand how to implement what you have in your mind.