Dynamically build a Flutter PageView - flutter

I need some suggestions on how to use the PageView. I have an online project that allows users to build custom forms, complete with conditions that hide or show questions depending on the answers in other questions. The mobile project allows users to fill out these forms. I've been playing with the PageView, which works for this, but I'm struggling to figure out how to indicate the end to the PageView. Right now, it will allow scrolling to continue forever.
return PageView.builder(
controller: _controller,
onPageChanged: (int index) {
FocusScope.of(context).requestFocus(FocusNode());
},
itemBuilder: (BuildContext context, int index) {
if (index >= _form.controls.length) {
print("Returning null");
return null;
}
return FormControlFactory.createFormControl(
_form.controls[index], null);
},
);
Since I'm not sure until the end of the form how many elements how do end the scrolling?
Update: In my example, I try returning null, but it still scrolls past the end.
Update: Here is where I'm currently at:
class _FormViewerControllerState extends State<FormViewerController> {
int _currentIndex = 0;
List<FormGroupController> _groups = List();
List<StreamSubscription> _subscriptions = List();
Map<int, FormControlController> _controllerMap = Map();
bool _hasVisibilityChanges = false;
#override
void initState() {
super.initState();
for (var i = 0; i < widget.form.controls.length; i++) {
var control = widget.form.controls[i];
if (control.component == ControlType.header) {
_groups.add(FormGroupController(
form: widget.form,
formResponses: widget.responses,
headerIndex: i));
}
}
_controllerMap[_currentIndex] = _getControl(_currentIndex);
_subscriptions.add(FormsEventBus()
.on<FormControlVisibilityChanging>()
.listen(_onControlVisibilityChanging));
_subscriptions.add(FormsEventBus()
.on<FormControlVisibilityChanged>()
.listen(_onControlVisibilityChanged));
}
#override
Widget build(BuildContext context) {
print("Building pageview, current index: $_currentIndex");
return PageView.builder(
controller: PageController(
initialPage: _currentIndex,
keepPage: true,
),
onPageChanged: (int index) {
print("Page changed: $index");
_currentIndex = index;
FocusScope.of(context).requestFocus(FocusNode());
},
itemBuilder: (BuildContext context, int index) {
print("Building $index");
_controllerMap[index] = _getControl(index);
return _controllerMap[index].widget;
},
itemCount: _groups
.map((g) => g.visibleControls)
.reduce((curr, next) => curr + next),
);
}
#override
void dispose() {
_subscriptions.forEach((sub) => sub.cancel());
_groups.forEach((g) => g.dispose());
super.dispose();
}
FormControlController _getControl(int index) {
for (var group in _groups) {
// We want to reduce the index so it can be local to group
if (index >= group.visibleControls) {
index -= group.visibleControls;
continue;
}
for (var instance in group.instances) {
// We want to reduce the index so it can be local to the instance
if (index >= instance.visibleControls) {
index -= instance.visibleControls;
continue;
}
return instance.controls.where((c) => c.visible).elementAt(index);
}
}
throw StateError("Weird, the current control doesn't exist");
}
int _getControlIndex(FormControlController control) {
var index = 0;
for (var group in _groups) {
if (control.groupInstance.group.groupId != group.groupId) {
index += group.visibleControls;
continue;
}
for (var instance in group.instances) {
if (control.groupInstance.groupInstanceId != instance.groupInstanceId) {
index += instance.visibleControls;
continue;
}
for (var c in instance.controls.where((c) => c.visible)) {
if (c.control.id != control.control.id) {
index++;
continue;
}
return index;
}
}
}
throw StateError("Weird, can't find the control's index");
}
_onControlVisibilityChanging(FormControlVisibilityChanging notification) {
_hasVisibilityChanges = true;
}
_onControlVisibilityChanged(FormControlVisibilityChanged notification) {
if (!_hasVisibilityChanges) {
return;
}
setState(() {
print("Setting state");
var currentControl = _controllerMap[_currentIndex];
_controllerMap.clear();
_currentIndex = _getControlIndex(currentControl);
_controllerMap[_currentIndex] = currentControl;
});
_hasVisibilityChanges = false;
}
}
The problem now is that if the changes result in a new page before the current one, in order to stay on the same page, the page index has to change so that it stays on the current one and that part isn't working. The build method is getting called multiple times and ends up showing the original index for some reason.
Here some sample print statements that show what I mean:
flutter: Control Text Box 2 (5) visible: true
flutter: Group instance Section 2 (1) visible controls: 1 -> 2
flutter: Group 1 clearing visible controls count
flutter: Setting state
flutter: Building pageview, current index: 2
flutter: Building 1
flutter: Building 2
flutter: Building pageview, current index: 2
flutter: Building 1
So I'm on index 1 at the beginning. I choose something on that view that results in a new page being inserted before index 1, so a NEW index 1. I call set state to set the current index to 2 since that is the new index of the current view. As you can see, the build method in the widget gets called twice, the first once renders index 1 and 2 in the page view, but the next one only renders index 1 even though the initial index is set to 2.

Since I'm unable to run the minimal repro you've posted. I tried to replicate the behavior locally from a sample app. From my tests, returning null on itemBuilder works as expected. I'm using Flutter stable channel version 1.22.0
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
int pageViewIndex;
class _MyHomePageState extends State<MyHomePage> {
ActionMenu actionMenu;
final PageController pageController = PageController();
int currentPageIndex = 0;
int pageCount = 1;
#override
void initState() {
super.initState();
actionMenu = ActionMenu(this.addPageView, this.removePageView);
}
addPageView() {
setState(() {
pageCount++;
});
}
removePageView(BuildContext context) {
if (pageCount > 1)
setState(() {
pageCount--;
});
else
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("Last page"),
));
}
navigateToPage(int index) {
pageController.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
}
getCurrentPage(int page) {
pageViewIndex = page;
}
createPage(int page) {
return Container(
child: Center(
child: Text('Page $page'),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: <Widget>[
actionMenu,
],
),
body: Container(
child: PageView.builder(
controller: pageController,
onPageChanged: getCurrentPage,
// itemCount: pageCount,
itemBuilder: (context, position) {
if (position == 5) return null;
return createPage(position + 1);
},
),
),
);
}
}
enum MenuOptions { addPageAtEnd, deletePageCurrent }
List<Widget> listPageView = List();
class ActionMenu extends StatelessWidget {
final Function addPageView, removePageView;
ActionMenu(this.addPageView, this.removePageView);
#override
Widget build(BuildContext context) {
return PopupMenuButton<MenuOptions>(
onSelected: (MenuOptions value) {
switch (value) {
case MenuOptions.addPageAtEnd:
this.addPageView();
break;
case MenuOptions.deletePageCurrent:
this.removePageView(context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
PopupMenuItem<MenuOptions>(
value: MenuOptions.addPageAtEnd,
child: const Text('Add Page at End'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.deletePageCurrent,
child: Text('Delete Current Page'),
),
],
);
}
}
Here how the sample app looks.

Related

Flutter: Lazy Loading with low amount of data

I try to use lazy load to show the order of the customer by using the ScrollController.
Of course, the new user has a low number of orders and those items are not enough to take up the entire screen. So the ScrollController doesn't work. What I can do?
This code will show a basic lazy load. You can change the _initialItemsLength to a low value like 1 to see this issue.
You can try this at api.flutter.dev
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
late List myList;
ScrollController _scrollController = ScrollController();
int _initialItemsLength = 10, _currentMax = 10;
#override
void initState() {
super.initState();
myList = List.generate(_initialItemsLength, (i) => "Item : ${i + 1}");
_scrollController.addListener(() {
print("scrolling: ${_scrollController.position.pixels}");
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
_getMoreData() {
print("load more: ${myList.length}");
for (int i = _currentMax; i < _currentMax + 10; i++) {
myList.add("Item : ${i + 1}");
}
_currentMax = _currentMax + 10;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return CupertinoActivityIndicator();
}
return ListTile(
title: Text(myList[i]),
);
},
itemCount: myList.length + 1,
),
);
}
}
First, start _initialItemsLength with 10. The scroller will be available and you will see it in the console. After that, change _initialItemsLength to 1. The console will be blank.
scroll listener will be triggered only if user try to scroll
as an option you need to check this condition _scrollController.position.pixels == _scrollController.position.maxScrollExtent after build method executed and each time when user scroll to bottom
just change a bit initState and _getMoreData methods
#override
void initState() {
super.initState();
myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
_scrollController.addListener(() => _checkIsMaxScroll());
WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll());
}
void _checkIsMaxScroll() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
_getMoreData();
}
}
_getMoreData() {
print('load more: ${myList.length}');
for (int i = _currentMax; i < _currentMax + 10; i++) {
myList.add('Item : ${i + 1}');
}
_currentMax = _currentMax + 10;
setState(() => WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll()));
}
You can set your ListView with physics: AlwaysScrollableScrollPhysics(), and thus it will be scrollable even when the items are not too many. This will lead the listener to be triggered.
Key code part:
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return CupertinoActivityIndicator();
}
return ListTile(
title: Text(myList[i]),
);
},
itemCount: myList.length + 1,
),
);
}
The point is 'Find some parameter that can tell whether scroll is enabled or not. If not just load more until the scroll is enabled. Then use a basic step for a lazy load like the code in my question.'
After I find this parameter on google, I don't find this. But I try to check any parameter as possible. _scrollController.any until I found this.
For someone who faces this issue like me.
You can detect the scroll is enabled by using _scrollController.position.maxScrollExtent == 0 with using some delay before that.
This is my code. You can see it works step by step in the console.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class PageStackoverflow72734370 extends StatefulWidget {
const PageStackoverflow72734370({Key? key}) : super(key: key);
#override
State<PageStackoverflow72734370> createState() => _PageStackoverflow72734370State();
}
class _PageStackoverflow72734370State extends State<PageStackoverflow72734370> {
late final List myList;
final ScrollController _scrollController = ScrollController();
final int _initialItemsLength = 1;
bool isScrollEnable = false, isLoading = false;
#override
void initState() {
super.initState();
print("\ninitState work!");
print("_initialItemsLength: $_initialItemsLength");
myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
_scrollController.addListener(() {
print("\nListener work!");
print("position: ${_scrollController.position.pixels}");
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) _getData();
});
_helper();
}
Future _helper() async {
print("\nhelper work!");
while (!isScrollEnable) {
print("\nwhile loop work!");
await Future.delayed(Duration.zero); //Prevent errors from looping quickly.
try {
print("maxScroll: ${_scrollController.position.maxScrollExtent}");
isScrollEnable = 0 != _scrollController.position.maxScrollExtent;
print("isScrollEnable: $isScrollEnable");
if (!isScrollEnable) _getData();
} catch (e) {
print(e);
}
}
print("\nwhile loop break!");
}
void _getData() {
print("\n_getData work!");
if (isLoading) return;
isLoading = true;
int i = myList.length;
int j = myList.length + 1;
for (i; i < j; i++) {
myList.add("Item : ${i + 1}");
}
print("myList.length: ${myList.length}");
isLoading = false;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return const CupertinoActivityIndicator();
}
return ListTile(title: Text(myList[i]));
},
itemCount: myList.length + 1,
),
);
}
}
You can test in my test. You can change the initial and incremental values at ?initial=10&incremental=1.
I know, this case is rare. Most applications show more data widget height than the height of the screen or the data fetching 2 turns that enough for making these data widget height than the height of the screen. But I put these data widgets in the wrap for users that use the desktop app. So, I need it.

Jumping to a particular sliver in a flutter sliverlist

How can I programmatically jump (or scroll) to a particular sliver in a sliver list where the slivers vary in height? The code below loads the text of a book into a custom scroll view, with a chapter for each widget. When the action button is pressed I want the view to jump to chapter 3, but nothing happens. What am I doing wrong?
class BookPage extends StatefulWidget {
BookPage({Key? key, this.title = "book"}) : super(key: key);
String title;
final _chapter3key = new GlobalKey(debugLabel: "chap3key");
get chapter3Key => _chapter3key;
#override
_BookState createState() => _BookState();
}
class _BookState extends State<BookPage> {
ScrollController? scrollController = new ScrollController();
Map<int, String> chapterHTML = {};
String title = "";
final chapterCount=10;
void fetchText() async {
for (int i = 1; i <= 10; i++) {
chapterHTML[i] = await getChapterHtml(i);
}
setState(() {});
}
_BookState() {
fetchText();
}
Widget chapterSliver(int i) {
if (i==3) {
return Html(key: widget.chapter3Key, data: chapterHTML[i] ?? "", );
}
return Html(
data: chapterHTML[i] ?? "",
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: CustomScrollView(controller: scrollController, slivers: [
SliverList(
delegate: SliverChildBuilderDelegate ((BuildContext context, int index) {
if (index > chapterCount) return null;
return chapterSliver(index);
}// first sliver is empty (chapter 0!)
))
]),
floatingActionButton: FloatingActionButton(onPressed: () {
Scrollable.ensureVisible(widget.chapter3Key.currentContext);
})
);
}
}
For anyone finding this question, I've found a solution - use a
scrollablePositionedList

How to show PhotoViewGallery class index value in appBar

I'm using photo_view_gallery class and I would like to show dynamically the value of current image index of PhotoViewGallery.builder into the appBar.
I'm new in Flutter, I googled a lot but I can't found any solution for this
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (context, index) {
saveIndex(index);
String myImg =
'http://www.attilofficina.altervista.org/phpbackend/JOB/000004/fullsize/' +
listaPagine[index].nomefile.toString() +
'.jpg';
return PhotoViewGalleryPageOptions(
imageProvider: AdvancedNetworkImage(myImg,
retryLimit: 1, timeoutDuration: Duration(seconds: 30)),
);
},
),
I also try a function that save index to another variable, but the value is still unavailable in appBar
Here the code and position of this function (in appBar is shown null)
class GalleryPageState extends State<GalleryPage> {
int curr;
...
#override
Widget build(BuildContext context) {
...
saveIndex(int index) {
int curr = index;
print('*** curr = ' + curr.toString()); /// PRINTS CORRECT VALUE
return curr;
}
...
return Scaffold(
appBar: AppBar(
title: Text(curr.toString(), /// BUT IT SHOWS NULL
),
),
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (context, index) {
salvaIndex(index);
String myImg =
'http://www.attilofficina.altervista.org/phpbackend/JOB/000004/fullsize/' +
listaPagine[index].nomefile.toString() +
'.jpg';
return PhotoViewGalleryPageOptions(
imageProvider: AdvancedNetworkImage(myImg,
retryLimit: 1, timeoutDuration: Duration(seconds: 30)),
);
},
),
);
}
}
Can someone help me?
special thanks mario
I think you can use onPageChanged
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final List<String> listaPagine = [
'https://picsum.photos/id/451/200/300',
'https://picsum.photos/id/200/200/300'
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('# $_currentIndex'),
),
body: PhotoViewGallery.builder(
itemCount: listaPagine.length,
builder: (BuildContext context, int index) {
String myImg = listaPagine[index];
return PhotoViewGalleryPageOptions(
imageProvider: NetworkImage(myImg),
);
},
onPageChanged: (int index) {
setState(() {
_currentIndex = index;
});
},
),
);
}
}
There's three things wrong in your following code:
saveIndex(int index) {
int curr = index;
print('*** curr = ' + curr.toString()); /// PRINTS CORRECT VALUE
return curr;
}
Here you are creating new int curr variable which should be curr to use the existing global scope variable.
You should create the saveIndex method outside of build function.
Also you should update the variable in setState like
setState((){
curr=index;
});
This will update the variable and recreate the widget tree with AppBar with updated values.
Also you don't need the return statement in saveIndex
Editing for #attila's help
The basic about the state is, it just keep the current state or values in memory. You can always change any value & it will be updated in the state but it will not reflected on the UI. Because your application or view still don't know that some value is updated & it needs to re-render the UI.
That's where the setState come in picture. What it does is while updating the value it also call the build function so new UI with updated values could be re-rendered on screen.
To test it, you can just update the values & call setState((){}); after that. It will still work as same.
Hope this helps you.

How to prevent rebuild of multiple sliver lists with "infinite" items?

Want to start off by saying this is unrelated to what is being discussed here as I use the Bloc pattern.
I have a widget where I create a CustomListView with multiple SliverLists based on the items returned by the StreamBuilder on top of the CustomListView. Each SliverList is infinite in the sense that the childCount is set to null. This is for lazy loading purposes. The problem is that when I push to and pop back from a page, all the items of all the SliverLists are rebuild, which causes a delay, especially when I'm already pretty far down the list.
I thought perhaps this might be solvable with Keys, but this seems to be unrelated to that? I think the issue is that I'm rebuilding the list of SliverLists dynamically in the build method (see build() in _ItemsBrowserState). The solution that I can think of is storing these widgets inside the state, but that just seems like I'm treating the symptom rather than the cause? I feel the same way about using AutomaticKeepAliveClientMixin, but feel free to change my mind on this.
class ItemsBrowser extends StatefulWidget {
final RepositoryBloc repoBloc;
ItemsBrowser({Key key, #required this.repoBloc}) : super(key: key);
#override
_ItemsBrowserState createState() => _ItemsBrowserState();
}
class _ItemsBrowserState extends State<ItemsBrowser> {
ScrollController _scrollController;
ItemBrowsersBloc bloc;
List<ItemBrowserBloc> blocs = [];
int atBloc = 0;
bool _batchLoadListener(ScrollNotification scrollNotification) {
if (!(scrollNotification is ScrollUpdateNotification)) return false;
if (_scrollController.position.extentAfter > 500) return false;
if (atBloc == blocs.length) return false;
if (blocs[atBloc].isLoading.value) return false;
if (blocs[atBloc].wasLastPage) atBloc++;
if (atBloc < blocs.length) blocs[atBloc].loadNextBatch();
return false;
}
#override
void initState() {
super.initState();
bloc = ItemBrowsersBloc(widget.repoBloc);
bloc.collections.listen((collections) {
if (_scrollController.hasClients) _scrollController.jumpTo(0.0);
_disposeItemBlocs();
atBloc = 0;
blocs = [];
for (var i = 0; i < collections.length; i++) {
var itemBloc = ItemBrowserBloc(collections[i], initLoad: i == 0);
blocs.add(itemBloc);
}
});
_scrollController = ScrollController();
}
void _disposeItemBlocs() {
if (blocs != null) {
for (var b in blocs) {
b.dispose();
}
}
}
#override
void dispose() {
super.dispose();
bloc?.dispose();
_disposeItemBlocs();
}
#override
Widget build(BuildContext context) {
print('Building Item Browser');
return StreamBuilder<List<Collection>>(
stream: bloc.collections,
builder: (context, snapshot) {
if (!snapshot.hasData) return Container();
List<Widget> slivers = [];
for (var i = 0; i < snapshot.data.length; i++) {
slivers.add(ItemList(blocs[i], key: UniqueKey()));
slivers.add(_buildLoadingWidget(i));
}
slivers.add(const SliverToBoxAdapter(
child: const SizedBox(
height: 90,
)));
return NotificationListener<ScrollNotification>(
onNotification: _batchLoadListener,
child: CustomScrollView(
controller: _scrollController, slivers: slivers),
);
});
}
Widget _buildLoadingWidget(int index) {
return StreamBuilder(
stream: blocs[index].isLoading,
initialData: true,
builder: (context, snapshot) {
return SliverToBoxAdapter(
child: Container(
child: snapshot.data && !blocs[index].initLoaded
? Text(
'Loading more...',
style: TextStyle(color: Colors.grey.shade400),
)
: null,
),
);
},
);
}
}
class ItemList extends StatelessWidget {
final ItemBrowserBloc bloc;
ItemList(this.bloc, {Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: bloc.isLoading,
initialData: true,
builder: (context, snapshot) {
var isLoading = snapshot.data;
var isInitialLoad = isLoading && !bloc.initLoaded;
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
// Index: 0 1 2 3
// Return: Header Item Item null
print('INDEX $index');
if (index == 0) return _buildHeader();
if (index > bloc.items.value.length) return null;
// var itemIndex = (index - 1) % bloc.batchSize;
var itemIndex = index - 1;
var item = bloc.items.value[itemIndex];
return InkWell(
key: ValueKey<String>(item.key),
child: ItemTile(item),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => ItemPage(item)));
},
);
}, childCount: isInitialLoad ? 0 : null),
);
});
}
Widget _buildHeader() {
return Container();
}
}
Behaviour: I open the page and see the first list. In the logs I see 'INDEX 0', 'INDEX 1', .... 'INDEX 8' (see build() in ItemList), because Flutter lazily builds only the first 9 items. As I scroll down more items are build. I stop at 'INDEX 30' and tap on a item, which pushes a new page. Now the problem: The page loading takes a sec. The logs show 'INDEX 0' ... 'INDEX 30', i.e. all the items are rebuild, causing a delay. I pop the page, and again all items from 0 to 30 are rebuild, causing a delay.
As expected, If I scroll down to the second SliverList, the entirety of the first SliverList and the lazily build items of the second SliverList are all rebuild on push/pop.
Expected behavior: Only the surrounding items should be rebuild.
Ladies and gentleman, we got him:
slivers.add(ItemList(blocs[i], key: UniqueKey()));
Replacing the UniqueKey with a ValueKey (or removing it) removed the awful delay!

How to display RefreshIndicator at top with ListView.builder reverse:true

I'm building a simple messaging system, where the user will see a list of messages.
I have a ListView.Builder with reverse:true since I want the list to appear at the bottom when they load the messaging page.
When they pull down to scroll all the way to the top I want a refresh indicator to appear so they can load previous messages, like most popular chat applications do.
However due to having reverse:true on the list they have to pull up at the bottom of the screen to load previous messages while using a RefreshIndicator.
Is there a way to make the RefreshIndicator trigger when pulling down rather than up when using reverse:true?
In my opinion,do you want to load more at the bottom of the listview,i think you just need to add one load more view to the last item of the listview,like the following code:
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(new MaterialApp(
home: new Scaffold(
body: new LoadMoreListView(enableLoadMore: true, count: 30,),
),
));
}
class LoadMoreListView extends StatefulWidget {
bool enableLoadMore;
int count;
LoadMoreListView({this.enableLoadMore = true, this.count = 15});
#override
State<StatefulWidget> createState() {
return new LoadMoreListViewState();
}
}
class LoadMoreListViewState extends State<LoadMoreListView> {
ScrollController _scrollController = new ScrollController();
bool isRequesting = false;
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
///load more when the listView attached the bottom
loadMore();
}
});
}
Future<Null> loadMore() async {
if (isRequesting) {
///if is requesting ,return the next action
return null;
}
setState(() {
isRequesting = true;
});
///loading your data from any where,eg:network
return null;
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemCount: _count(),
itemBuilder: _buildItem);
}
_count() {
if (widget.enableLoadMore) {
return widget.count + 1;
}
return widget.count;
}
Widget _buildItem(BuildContext context, int index) {
if (index == widget.count) {
return _buildLoadMoreView();
}
return new Container(
height: 36.0,
child: new Center(
child: new Text("I am the $index item"),
),
);
}
Widget _buildLoadMoreView() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: 1.0,
child: new CircularProgressIndicator(),
),
),
);
}
}