How to create a ListView scrolled to its top? - flutter

I'm a very novice Flutter programmer. I meet the following problem: the code shown below when recreates the ListView after changing data in the stream remembers the previous scroll position whereas I would like to scroll the list to its top. How could I do that?
class HintViewState extends State<HintView> {
#override
Widget build(BuildContext context) {
return StreamBuilder<List<String>>(
stream: model.hintsStream.stream,
initialData: [],
builder: (
BuildContext context,
AsyncSnapshot<List<String>> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active ||
snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Eraro dum prenado de sugestoj!');
} else if (snapshot.hasData) {
return Expanded(
child: ListView(
padding: const EdgeInsets.all(0.0),
scrollDirection: Axis.vertical,
children: snapshot.data!.map((word) => Padding(...)).toList(),
),
);
} else {
return const Text("Ne estas sugestoj!");
}
} else {
return Text('Stato: ${snapshot.connectionState}');
}
},
);
}
}

Create a scroll controller like as below
final ScrollController _scrollController = ScrollController();
Set it to the list view like as below
child: ListView.builder(
controller: _scrollController,
.....
When you want to scroll it to top, please add below code.
_scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn);
In your example, you can do like,
else if (snapshot.hasData) {
_scrolltoTop();
return Expanded(
child: ListView(
padding: const EdgeInsets.all(0.0),
scrollDirection: Axis.vertical,
children: snapshot.data!.map((word) => Padding(...)).toList(),
),
);
}
void _scrolltoTop(){
Future.delayed(const Duration(milliseconds: 1000), () {
_scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn);
});
}

In a Flutter community, they explained the situation to me. And the answer is the following.
The key thing here is the parameter "key" of a widget (not a bad pun, yeah?). When during rebuilding at some place in the UI tree, a widget appears of the same type and with the same key as the previous one, then this widget is not recreated, but modified only. And in my case, evidently, modification of a ListView does not imply scrolling to a new position. "That's not a bug, that's a feature!".
One way to force complete recreation a widget when new data appear in the stream is to pass a new key. The best way to get a new key is to use the function ValueKey(...), which, evidently, computes some hash of the object passed to it and returns a key based on the hash. So, when the function is used with the data to be shown, a change of the data in the stream leads to a change of the key and, therefore, to a complete recreation of the widget, which in the case of a ListView assumes scrolling to the top.
So, the correction of the code is the following:
..............
return Expanded(
child: ListView(
key: ValueKey(snapshot.data),
padding: const EdgeInsets.all(0.0),
scrollDirection: Axis.vertical,
..............
But I'm not sure if in a general case, this widget only is recreated or its entire subtree. If the latter, then it might be costly.

Related

My `GridView` breaks or is not scrollable

I am pulling my hair trying to make my grid view displays within the user's profile page, which is basically a Column Widget.
Either I get error regarding the "unbounceness" of the widget or other error like RenderFlex children have non-zero flex but incoming height constraints are unbounded..
If I set the shrinkWrap to true the grid is there, but unscrollable.
I tried many solution such as adding a Flexible or Expanded parent with a mainAxisSize to min for the Column.
My grid view code is a as follows:
/// A grid view of the user's ads
class UserAdWidget extends StatelessWidget {
final String userId;
const UserAdWidget({super.key, required this.userId});
#override
Widget build(BuildContext context) {
final query = ClassifiedAd.getQueryFromUserId(userId);
return FirestoreQueryBuilder<ClassifiedAd>(
query: query,
builder: (context, snapshot, _) => Flexible(
child: GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
crossAxisSpacing: 2,
mainAxisSpacing: 20),
itemCount: snapshot.docs.length,
itemBuilder: (context, index) {
// if we reached the end of the current items, get more
if (snapshot.hasMore && index + 1 == snapshot.docs.length) {
snapshot.fetchMore();
}
final ad = snapshot.docs[index].data();
return ad.galleryWidget(context,
withAvatar: false, onTap: () {});
})));
}
}
The component code where the grid is rendered within a Column is as follows:
#override
Widget build(BuildContext context) {
return Consumer<StateModel>(builder: (context, appState, child) {
final selfProfile = appState.loggedUser?.id == widget.user.id;
return Scaffold(
appBar: AppBar(
title:
Text(selfProfile ? "Votre profil" : widget.user.displayName)),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(mainAxisSize: MainAxisSize.min, children: [
// === User's profile widget
UserProfileWidget(user: widget.user),
const Divider(),
// == Grid of user's ads
const Text("The ads:"),
const Text(""),
UserAdWidget(userId: widget.user.id),
])));
});
}
The current code renders like so but does not scroll:

AnimatedList auto scrolling to last item and back

I've implemented an Animated list with SlideTransition like this
#override
Widget build(BuildContext context) {
return Expanded(
child: Container(
child: ListView(
children: [
// Other widgets
animatedList(),
],
),
),
);
}
Widget animatedList() {
return AnimatedList(
shrinkWrap: true,
key: _myKeyList,
initialItemCount: _myItemsList.length,
itemBuilder: (context, index, animation) {
return SlideTransition(
position: animation.drive(_offset),
child: _buildMyItemTile[index],
);
},
);
}
where _offset variable is a Tween animation. Each item of list is inserted and animated with a delay of 500 milliseconds.
Now, when all items are added to AnimatedList, i would like that AnimatedList content scroll automatically from first item to last (and back) continuously for show all its content.
How can i do?
Just do it
add a controller
final controll = ScrollController();
then add this controller in the AnimatedList
AnimatedList(
controller: controll,
...
)
Make a function to call the last position or the first
0 for first position and 1 for last position or true false, I don't know, use your imagination
void getLastItem(int 0){
var position = int == 0
? controll.position.minScrollExtent
: controll.position.maxScrollExtent;
controll.animateTo(
position,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInCubic,
)
}
I hope I helped you.

Go to the end of the list as soon as you create it, Flutter

I read many great answers about a ScrollController in the ListView. Then when a button is clicked we do scrollController.jumpTo(scrollController.position.maxScrollExtent);
My question is, can we go to the bottom of a List, but at the creation of it ?
return ListView.builder(
itemCount: data.length,
shrinkWrap: true,
// CAN WE DO SOMETHING HERE FOR EXAMPLE ?
physics: const ScrollPhysics(),
itemBuilder: (context, index)
{
return Item(data[index]);
}
);
(For me, it's a chat so the reverse option doesn't look good)
If, as you said, you are using this list for a chat log, then the reverse options does work for you. You just have to also invert your chat log data:
data.reversed.toList()
There is not direct option on a ListView to go to the end of the list.
For a more complete example from one of my applications:
class MessageList extends StatelessWidget {
const MessageList(this.displayMessages);
final List<Map> displayMessages;
#override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
reverse: true,
children: displayMessages.map((message) {
return ChatMessage(message);
}).toList(),
),
),
);
}
}
displayMessages = List<Map>.from(clientMessages.data).reversed.toList();

I can't refresh the screen

I use the get package, I have a bottom navigation that is updated with Obx. There is also a search for elements, at the top level of the pages everything is updated well, but when I do push, the current page is not updated, only when you call hot reload. There are suspicions that the nested page is not updated due to the fact that it goes beyond the BottomNavigation, and there is no Obx widget.
My Page Navigation controller:
Widget build(BuildContext context) {
SizeConfig().init(context);
final BottomPageController landingPageController =
Get.put(BottomPageController(), permanent: false);
return SafeArea(
child: Scaffold(
bottomNavigationBar:
BottomNavigationMenu(landingPageController: landingPageController),
body: Obx(
() => IndexedStack(
index: landingPageController.tabIndex.value,
children: [
ProductPage(),
MapPage(),
ClientPage(),
NotePage(),
],
),
),
),
);
}
My page where you need to update the ListView, depending on the entered value in the search bar:
class Product extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetX<ProductController>(
init: ProductController(),
builder: (controller) {
return FutureBuilder<List<ProductsCombined>>(
future: controller.filterProduct.value,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Expanded(
child: Container(
child: ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) =>
ProductCards(
product: snapshot.data[index],
),
),
),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
);
},
);
}
}
There is also a nested page in the Product class, which is accessed in this way:
onTap: () => {
Get.toNamed(StoresScreen.routeName, arguments: companies)}
The Product class is updated immediately, you do not need to click Hot reload to do this, and the ProductScreen class that the transition is made to can no longer be updated, these 2 classes are completely identical. The search bar works, but it filters out the elements only after Hot reload.
If you need some other parts of my code, such as a controller that handles ListView, please write, I will attach it.
EDIT: I add a link to the video, with the screen that is not updated
Obx is a little tricky to work with. I always suggest to use GetX instead.
Try this :
Obx(
() => IndexedStack(
index: Get.find<BottomPageController>().tabIndex.value,
children: [
...
],
),
),
If it didn't work use GetX<BottomPageController> for this situation too. It works for all conditions

Flutter PageView, can i animate removing items from list?

I'm pretty new to flutter and i'm trying to do some animation on a PageView. to be precise, I want to animate removing an item.
I've tried serveral ways to animate it and apart from a solution, the way how you guys would solve such a problem would also be helpful for my flutter skils.
What I've tried so far:
Animating the padding and opacity
the problem with this is that when i set the padding in the setState in the onLongPress it rebuilds the widget and it overrides the padding again with the active or inactive CardPadding (i think)
Animating the width and height
I just can't seem to get both of these values to work
Animating the viewportFraction on the PageViewController
Would not know how to go about this and if it would be possible to do this only for a specific 'Page'
Below is the (stripped down) code I've written thus far.
class Main extends StatefulWidget {
#override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
int activeCard = 0;
EdgeInsets inActiveCardPadding = EdgeInsets.symmetric(vertical: 120.0, horizontal: 20.0);
EdgeInsets activeCardPadding = EdgeInsets.symmetric(vertical: 105.0, horizontal: 10.0);
PageController pageController = PageController(
initialPage: 0,
viewportFraction: 0.8,
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: <Widget>[
PageView.builder(
itemCount: PlantCareApp.plants.length,
controller: pageController,
onPageChanged: (activeCardIndex) {
setState(() {
this.activeCard = activeCardIndex;
});
},
itemBuilder: (context, cardIndex) {
return AnimatedContainer(
padding: (activeCard == cardIndex) ? activeCardPadding : inActiveCardPadding;,
duration: Duration(milliseconds: 250),
child: PlantCard(
PlantCareApp.plants[cardIndex],
onTap: () {
Navigator.pushNamed(context, PlantDetailScreen.route, arguments: PlantCareApp.plants[cardIndex]);
},
onLongPress: () {
setState(() {
//
// ANIMATE OR TRIGGER ANIMATION HERE
//
// do the actual removing
/*
PlantCareApp.plants[cardIndex].remove(); // remove from db
PlantCareApp.plants.removeAt(cardIndex); // remove from List
*/
});
//PlantCareApp.plants[cardIndex].remove();
},
),
);
},
),
],
),
),
);
}
}
Any help will be greatly appreciated! How would you guys tackle a problem like this, or how would you tackle this specific use case.
I guess actually animating viewportFraction would be the nicest because of the adjecent 'Pages' moving toward each other as well?
Thanks!
I'm not certain if this is what you are looking for, but here goes.
One way of doing this is simply using the provided Widgets within Flutter. Two of these will help you out: AnimatedList and Dismissible.
Now, you could do something like this:
// define somewhere
final _animatedListGK = GlobalKey<AnimatedListState>();
// put in a function somewhere
return AnimatedList(
key: _animatedListGK,
padding: const EdgeInsets.all(0),
initialItemCount: PlantCareApp.plants.length,
itemBuilder: (context, index, animation) {
return FadeTransition(
opacity: animation,
child: _buildDismissibleRow(context, index, PlantCareApp.plants[index])
);
}
);
Note: you don't have to use the _animatedListGK global key per se, it depends on whether you can use AnimatedList.of(context) or not. Although it is the easier way.
The _animatedListGK is simply a Global Key that provides access to the AnimatedList so you can perform insertions/removals with animation.
Your dismissible row might look something like:
Widget _buildDismissibleRow(BuildContext context, int index, PlantModel plantModel) {
return Dismissible(
key: ValueKey<String>(plantModel.someKey),
direction: DismissDirection.startToEnd,
background: Container(color: Colors.red),
onDismissed: (direction) {
// You could use:
// AnimatedList.of(context)
_animatedListGK.currentState.removeItem(
index,
(context, animation) => Container(),
duration: Duration.zero
);
},
child: _buildContent(context, index, plantModel)
);
}
You could also do it without a dismissible row or even within the child of the dismissible row (_buildContent() for example). Something similar to:
// You could use:
// AnimatedList.of(context)
_animatedListGK.currentState.removeItem(
index,
(context, animation) {
return FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Interval(0.5, 1.0)),
child: SizeTransition(
sizeFactor: CurvedAnimation(parent: animation, curve: Interval(0.0, 1.0)),
child: _builContent(context, index, plantModel)
)
);
},
duration: const Duration(milliseconds: 300)
);
Notice how the SizeTransition simply "calls itself" by calling _builContent(context, index, plantModel)? That's how you can animate the row itself (out of existence).
Be sure to watch the videos in the aforementioned documentation pages! They will help understanding certain constructs.
A preview of what the dismissible might look like:
A preview of what the SizedTransition might look like: