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

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.

Related

Flutter pull to refresh on lower part of screen

I have a app that looks like the picture above.
When i swipe right or left, page switches along with the pageview content. but NOT the image part, it stays still.
When i implement pull to refresh on this case, the gap opens above image part showing progress indicator and refreshes.
I want the refresher crack open between image and content part, how do i achieve this?
thank you so much for reply in advance, you are the hero.
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
const Expanded(
child: SizedBox(height: 200), //The 'Image Part'
),
SmartRefresher(
child: PageView.builder(itemBuilder: (context, state) => Column(
children:[SizedBox(height:200),PageContent()], itemCount: 3),//The 'Content Part'
)
],
));
}
}
Example code added above, this represents what i would like to implement, I want the refresher only accessible inside PageContent()
but i could not because it contains Column inside
seems like an issue with your stack.
if you want the image part to also follow the swipe it has to be within the pageview.builder.
For the refresh, under pageview.builder wrap the content part in the pull-to-refresh widget but not the image.
This is possibly caused by the Stack widget
Change your Stack to a Column
Wrap your SmartRefresher with and Expanded widget for your ListviewBuilder to take the remaining space in vertical axis. You will have an unbounded high error otherwise
Set the shrinkwrap of your ListviewBuilder to true for it to build your list elements on demand
That's it! ☑️

How to dynamically disable vertical swipe in PageView

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.

Flutter - PageView - Only scroll when you swipe on top of a specific component instead of any location of the page

Totally new to flutter, less than 10h on it.
I have a PageView handing 3 pages, it scrolls horizontally through them perfectly.
But I would like to change the "allowed swipe" area. I don't want to let the user change the pages scrolling from any position of the page, but instead, for example, just let him scroll the pages if he swipes on the AppBar component for example.
I saw the PageView has the applyTo method, I'm just lost about how to give to it the ID(keys) or the appBar to see if this will work.
Is there a way of achieving this "only scroll if the user swipes on the component X"?
Edit 1
Solution suggested by Alberto Miola works like a charm, here is my code (I had to implement from PreferredSizeWidget since is required to modify the AppBar).
class GestureDetectorForAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
final AppBar appbar;
final GestureDragUpdateCallback onPanUpdate;
GestureDetectorForAppBar({
Key key,
this.appbar,
this.onPanUpdate,
#required this.height,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(child: this.appbar, onPanUpdate: this.onPanUpdate,);
}
#override
Size get preferredSize => Size.fromHeight(height);
}
First of all, you need to "block" the swipe gesture on the PageView itself using NeverScrollableScrollPhysics:
PageView(
physics: const NeverScrollableScrollPhysics(),
children: [...],
controller: ...
);
Note the usage of a const constructor. In this way you won't be able to move among pages with your finger. In Flutter you have widgets and not components.
Give a look at the GestureDetector widget which can be used to listen on swipes and change the currently visible content of your PageView. It can be used to detect swipes:
GestureDetector(
onPanUpdate: (data) {
if (data.delta.dx > 0) {
// right swipe
}
if (data.delta.dx < 0) {
// right swipe
}
}
);
In order, I suggest you to first read about NeverScrollableScrollPhysics() in the official documentation. It's used to "block" scrolling behaviors. Then, use GestureDetector() to wrap the widget you want to be used as a "scroll director" (the one that actually scrolls the pages).
Inside onPanUpdate you'll deal with the animateToPage method of the PageController to change the currently visible page.

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.

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.