ExpansionTile force closes once it detects a scrolling behavior i.e immediately I scroll the page... I want to keep the ExpansionTile opened until I tapped on its title.
What could be the cause?
The reason your ExpansionTile is closing immediately is because when you scroll through a listview, especially one created with the ListView.builder the list is rebuilt again, so if you want to preserve the status of a certain ExpansionTile you should save its status value out of your build function and use the initiallyExpanded property:
List<bool> status = <bool>[] ; //store the statuses of your tiles here
ExpansionTile(
title: Text('Item'),
backgroundColor: Colors.transparent,
initiallyExpanded: status[index],
),
Add and remove indices from a list with onExpansionChanged callback and set initiallyExpanded if it contains that index.
class _WidgetState extends State<Widget> {
List expandedIndices = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return ExpansionTile(
title: Text('Item'),
onExpansionChanged: (expanded) {
if(expanded) {
expandedIndices.add(index);
}
else{
expandedIndices.remove(index);
}
},
initiallyExpanded: expandedIndices.contains(index),);
}
)
);
}
}
Related
I am working on flutter project. I want to get content size of horizontal listview. When i click on option in list view , i want to check that option is in proper bound of screen or out of bound. If it is out of bound, then how to move in of bound?
Please suggest and help me to sort out
Thanks in advance
You can use ScrollController to get the size of listView. If it is on Column widget, wrap with Expanded widget to get available space.
class _TDState extends State<XT> {
late final ScrollController controller = ScrollController()
..addListener(() {
print(controller.position.maxScrollExtent);
});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: ListView.builder(
controller: controller,
itemCount: 44,
itemBuilder: (context, index) => Text("item $index"),
))
],
),
);
}
}
I created bottom navigation for iOS using CupetinoTabBar in flutter and I'd like to detect re-select tab event and go back to the its first page.
Is there any way to implement the function?
For iOS, it's a pretty common function but I don't know how to do it.
The code is below.
Thank you in advance.
class _MainTabScreenState extends State<MainTabScreen> {
final List<Widget> _pages = [FeedScreen(), Feed2Screen(), HomeScreen()];
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
backgroundColor: Colors.black,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home)),
BottomNavigationBarItem(icon: Icon(Icons.movie)),
BottomNavigationBarItem(icon: Icon(Icons.person)),
],
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
builder: (ctx) {
return _pages[index];
},
);
},
);
}
}
I have a code, that uses dismissible in the listview (showes items from database). After dismissing an item it is supposed to show snackbar but it is not showing it and it seems that the dismissible is still part of the tree. Can you help me with that?
return ListView.builder(
itemCount: count,
itemBuilder: (BuildContext context, int position) {
final ThemeData theme = Theme.of(context);
return Dismissible(
key: Key(this.objs[position].id.toString()),
onDismissed: (direction) {
setState(() async {
int result = await helper.delete(this.objs[position].id);
});
Scaffold.of(context)
.showSnackBar(SnackBar(
content: Text(this.objs[position].title + "dismissed")));
},
background: Container(
color: Colors.red,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0)
)
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: getColor(this.objs[position].priority),
child: Text(this.objs[position].id.toString()),
),
title: Text(obj[position].title),
subtitle: Text(objs[position].date),
onTap: () {
debugPrint("Tapped on " + objs[position].id.toString());
navigateToDetail(this.objs[position]);
},
),
);
},
);
this is called inside a Scaffold. And objs is a list that contains all my objects from the database.
Here is my delete code that is called inside onDismissed:
Future<int> delete(int id) async {
Database db = await this.db;
var result = await db.rawDelete("DELETE FROM $tblT WHERE $colId=$id");
return result;
}
I've noticed if I delete one item, and immediately try to create another one (I have an option to insert to DB):
It sometimes throws the error: A dismissed Dismissible widget is still part of the tree
Update:
Moved the delete part, before setState and I am getting the error: A dismissed Dismissible widget is still part of the tree every time I swipe to dismiss
You could try the following code for the onDismissed: property.
The problem is the future inside the onDismissed function. We need to reorder the async and await keywords.
Anyway, take care with the timings when removing successive items.
onDismissed: (direction) async {
String title = this.obj[position].title;
await helper.delete(this.obj[position].id);
setState(() {});
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$title dismissed")));
},
It also moves the async out of the setState() and stores the title to be used later by the SnackBar.
Inside content in SnackBar you can try :
Text(this.obj[position].title.toString() + "dismissed")
I'm looking for a way to insert new items into a list view, while maintaining the scroll offset of the user. Basically like a twitter feed after pulling to refresh: the new items get added on top, while the scroll position is maintained. The user can then just scroll up to see the newly added items.
If I just rebuild the list/scroll widget with a couple of new items at the beginning, it -of course- jumps, because the height of the scroll view content increased. Just estimating the height of those new items to correct the jump is not an option, because the content of the new items is variable.
Even the AnimatedList widget which provides methods to dynamically insert items at an arbitrary position jumps when inserting at index 0.
Any ideas on how to approach this? Perhaps calculating the height of the new items beforehand using an Offstage widget?
Ran into this problem recently: I have a chat scroll that async loads previous or next messages depending on the direction of the scroll. This solution worked out for me.
The idea of the solution is the following. You create two SliverLists and put them inside CustomScrollView.
CustomScrollView(
center: centerKey,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the upper group
child: top[index]
)
}
),
SliverList(
// Key parameter makes this list grow bottom
key: centerKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the bottom group
child: bottom[index]
)
}
),
)
The first list scrolls upwards while the second list scrolls downwards. Their offset zero points are fixed at the same point and never move. If you need to prepend an item you push it to the top list, otherwise, you push it to the bottom list. That way their offsets don't change and your scroll view does not jump.
You can find the solution prototype in the following dartpad example.
UPD: Fixed null safety issue in this dartpad example.
I don't know if you managed to solve it... Marcin Szalek has posted a very nice solution on his blog about implementing an infinite dynamic list. I tried it and works like a charm with a ListView. I then tried to do it with an AnimatedList, but experienced the same issue that you reported (jumping to the top after each refresh...). Anyway, a ListView is quite powerful and should do the trick for you!
The code is:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(primarySwatch: Colors.blue),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<int> items = List.generate(10, (i) => i);
ScrollController _scrollController = new ScrollController();
bool isPerformingRequest = false;
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
_getMoreData() async {
if (!isPerformingRequest) {
setState(() => isPerformingRequest = true);
List<int> newEntries = await fakeRequest(
items.length, items.length + 10); //returns empty list
if (newEntries.isEmpty) {
double edge = 50.0;
double offsetFromBottom = _scrollController.position.maxScrollExtent -
_scrollController.position.pixels;
if (offsetFromBottom < edge) {
_scrollController.animateTo(
_scrollController.offset - (edge - offsetFromBottom),
duration: new Duration(milliseconds: 500),
curve: Curves.easeOut);
}
}
setState(() {
items.addAll(newEntries);
isPerformingRequest = false;
});
}
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isPerformingRequest ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Infinite ListView"),
),
body: ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return _buildProgressIndicator();
} else {
return ListTile(title: new Text("Number $index"));
}
},
controller: _scrollController,
),
);
}
}
/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
return Future.delayed(Duration(seconds: 2), () {
return List.generate(to - from, (i) => i + from);
});
}
A gist containing whole class can be found here.
I think reverse + lazyLoading will help you.
Reverse a list:
ListView.builder(reverse: true, ...);
for lazyLoading refer here.
I'm currently developing a reader and using PageView to slide the page of images. How do I make the next page preload so that the user can slide to next page without waiting for the page to load? I don't want to download all the pages first because it will load the server and freezes my app. I just want to download just next one or two pages when the user browsing on current page.
Here is the excerpt of my code.
PageController _controller;
ZoomableImage nextPage;
Widget _loadImage(int index) {
ImageProvider image = new CachedNetworkImageProvider("https://example.com/${bookId}/${index+1}.jpg}");
ZoomableImage zoomed = new ZoomableImage(
image,
placeholder: new Center(
child: CupertinoActivityIndicator(),
),
);
return zoomed;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Container(
child: PageView.builder(
physics: new AlwaysScrollableScrollPhysics(),
controller: _controller,
itemCount: book.numPages,
itemBuilder: (BuildContext context, int index) {
return index == 0 || index == 1 ? _loadImage(index) : nextPage;
},
onPageChanged: (int index) {
nextPage = _loadImage(index+1);
},
),
),
);
}
Thank you!
Simple! Just set allowImplicitScrolling: true, // in PageView.builder
I ended up using FutureBuilder and CachedNetworkImageProvider from the package cached_network_image to prefetch all the images. Here is my solution:
PageController _controller;
ZoomableImage currPage, nextPage;
Future<List<CachedNetworkImageProvider>> _loadAllImages(Book book) async {
List<CachedNetworkImageProvider> cachedImages = [];
for(int i=0;i<book.numPages;i++) {
var configuration = createLocalImageConfiguration(context);
cachedImages.add(new CachedNetworkImageProvider("https://example.com/${bookId}/${index+1}.jpg}")..resolve(configuration));
}
return cachedImages;
}
FutureBuilder<List<CachedNetworkImageProvider>> _futurePages(Book book) {
return new FutureBuilder(
future: _loadAllImages(book),
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.hasData) {
return new Container(
child: PageView.builder(
physics: new AlwaysScrollableScrollPhysics(),
controller: _controller,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
ImageProvider image = snapshot.data[index];
return new ZoomableImage(
image,
placeholder: new Center(
child: CupertinoActivityIndicator(),
),
);
},
onPageChanged: (int index) {},
),
);
} else if(!snapshot.hasData) return new Center(child: CupertinoActivityIndicator());
},
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: _futurePages(widget.book),
);
}
As people mentioned before the cached_network_image library is a solution, but not perfect for my situation. There are a full page PageView(fit width and height) in my project, when I try previous code my PageView will show a blank page first, then show the image.
I start read PageView source code, finally I find a way to fit my personal requirement. The basic idea is change PageView source code's cacheExtent
This is description about how cacheExtent works:
The viewport has an area before and after the visible area to cache items that are about to become visible when the user scrolls.
Items that fall in this cache area are laid out even though they are not (yet) visible on screen. The cacheExtent describes how many pixels the cache area extends before the leading edge and after the trailing edge of the viewport.
Change flutter's source code directly is a bad idea so I create a new PrelodPageView widget and use it at specific place when I need preload function.
Edit:
I add one more parameter preloadPagesCount for preload multiple pages automatically.
https://pub.dartlang.org/packages/preload_page_view