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
Related
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.
ListView with GetWidget not updating.
MyItem.dart
class MyItem {
var title = "";
var isChecked = false;
}
MyController.dart
class MyController extends GetxController {
final items = <MyItem>[].obs;
#override
void onReady() {
refresh();
super.onReady();
}
void refresh() async {
var items = <MyItems>[];
for (var i = 0; i < 10; i++) {
var item = MyItem()
..title = i.toString()
..isChecked = i < 3; // Only the first 3 in the list set the check status to true.
items.add(item);
}
this.items.value = items;
}
}
MyPage.dart
class MyPage extends GetView<MyController> {
...
#override
Widget build(BuildContext context) => RefreshIndicator(
onRefresh: controller.onRefresh,
child: Obx(() => ListView.builder(
itemBuilder: (context, i) {
var items = controller.items.value;
var item = items[i];
return MyItemWidget(item: item);
},
itemCount: controller.items.value.length,
)),
);
}
MyItemWidget.dart
class NotificationItem extends GetWidget<MyItemController> {
final MyItem item;
const MyItemWidget({Key? key, required this.item }) : super(key: key);
#override
Widget build(BuildContext context) {
controller.item.value = item;
return Obx(() => InkWell(
child: Container(
width: double.infinity,
height: 50,
color: item.isChecked ? Colors.red : Colors.white,
child : Center(child: Text(item.title))
),
onTap: controller.toggle
));
}
}
MyItemController.dart
class MyItemController extends GetxController {
final item = MyItem().obs;
void toggle() {
item.value.isChecked = !item.value.isChecked;
item.refresh();
}
}
To Reproduce
Click to list item.
Check status change.
pull to refresh.
Widgets of listview children that are not updated
Expected behavior
When the list is refreshed, the default value of isChecked is true only for the first 3 items in the list, so the first 3 items were expected to change to the checked state, but the widget is not updated.
Instead, when the list is scrolled and moved and then back to position, the build method of Item Widget is called again and drawn on the screen draw to the state.
When using GetX or GetBuilder instead of GetWidget for the items in the list, the didUpdateWidget callback was called and the state could be updated normally. Are these not possible with GetWidget?
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.
I am using a ListView with selectable items similar to this example.
Each stateful widget in the ListView has a _selected boolean to determine it's selected status which is flipped when the item is tapped.
When the user is in selection mode, there is a "back" option in the app bar. Determining when the back button is pressed and handling underlying core logic is working fine. I just want to reset the _selected flag on each individual list item so that they no long display as selected. You can see in the included gif that once back is pressed, the ListView items remain selected.
I am obviously missing something extremely basic.
The underlying question is, how do I trigger a reset of a ListView children items programatically.
Edit: Sample code added
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'List selection demo',
home: new MyHomePage(title: 'List selection demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<String> playerList = [
"Player 1",
"Player 2",
"Player 3",
"Player 4"
];
List<String> selectedPlayers = [];
bool longPressFlag = false;
void longPress() {
setState(() {
if (selectedPlayers.isEmpty) {
longPressFlag = false;
} else {
longPressFlag = true;
}
});
}
void clearSelections(){
setState(() {
selectedPlayers.clear();
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(selectedPlayers.length == 0?widget.title: selectedPlayers.length.toString() + " selected"),
leading: selectedPlayers.length == 0? null: new IconButton(
icon: new Icon(Icons.arrow_back),
onPressed: () {clearSelections();
})),
body: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new PlayerItem(
playerName: playerList[index],
longPressEnabled: longPressFlag,
callback: () {
if (selectedPlayers.contains(playerList[index])) {
selectedPlayers.remove(playerList[index]);
} else {
selectedPlayers.add(playerList[index]);
}
longPress();
});
},
itemCount: playerList.length,
));
}
}
class PlayerItem extends StatefulWidget {
final String playerName;
final bool longPressEnabled;
final VoidCallback callback;
const PlayerItem(
{Key key, this.playerName, this.longPressEnabled, this.callback})
: super(key: key);
#override
_PlayerItemState createState() => new _PlayerItemState();
}
class _PlayerItemState extends State<PlayerItem> {
bool selected = false;
#override
Widget build(BuildContext context) {
return new GestureDetector(
onLongPress: () {
setState(() {
selected = !selected;
});
widget.callback();
},
onTap: () {
if (widget.longPressEnabled) {
setState(() {
selected = !selected;
});
widget.callback();
} else {
final snackBar = SnackBar(content: Text(widget.playerName + " tapped"));
Scaffold.of(context).showSnackBar(snackBar);
}
},
child: new Card(
color: selected ? Colors.grey[300] : Colors.white,
elevation: selected ? 4.0 : 1.0,
margin: const EdgeInsets.all(4.0),
child: new ListTile(
leading: new CircleAvatar(
child: new Text(widget.playerName.substring(0, 1)),
),
title: new Text(widget.playerName),
)));
}
}
You have to call following method for reset widgets.
setState(() {
});
I am trying to update my stateful widget of my class while calling it from Navigation Drawer. stateless widget are being updated when they are called from Navigation Drawer. Here is my Navigation drawer from where I am calling 'Fragment First'.
class DrawerItem {
String title;
IconData icon;
DrawerItem(this.title, this.icon);
}
class HomePage extends StatefulWidget {
final drawerItems = [
new DrawerItem("First Fragment", Icons.rss_feed),
new DrawerItem("Second Fragment", Icons.local_pizza),
new DrawerItem("Third Fragment", Icons.info)
];
#override
State<StatefulWidget> createState() {
return new HomePageState();
}
}
class HomePageState extends State<HomePage> {
int _selectedDrawerIndex = 0;
_getDrawerItemWidget(int pos) {
switch (pos) {
case 0:
return new FirstFragmen(pos);
case 1:
return new FirstFragmen(pos);
case 2:
return new FirstFragmen(pos);
default:
return new Text("Error");
}
}
_onSelectItem(int index) {
setState(() => _selectedDrawerIndex = index);
Navigator.of(context).pop(); // close the drawer
}
#override
Widget build(BuildContext context) {
List<Widget> drawerOptions = [];
for (var i = 0; i < widget.drawerItems.length; i++) {
var d = widget.drawerItems[i];
drawerOptions.add(
new ListTile(
leading: new Icon(d.icon),
title: new Text(d.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)
);
}
return new Scaffold(
appBar: new AppBar(
// here we display the title corresponding to the fragment
// you can instead choose to have a static title
title: new Text(widget.drawerItems[_selectedDrawerIndex].title),
),
drawer: new Drawer(
child: new Column(
children: <Widget>[
new UserAccountsDrawerHeader(
accountName: new Text("John Doe"), accountEmail: null),
new Column(children: drawerOptions)
],
),
),
body: _getDrawerItemWidget(_selectedDrawerIndex),
);
}
}
Here is Fragment First:
class FirstFragment extends StatefulWidget {
int pos;
FirstFragment(this.pos);
#override
_FirstFragmentState createState() => new _FirstFragmentState(pos);
}
class _FirstFragmentState extends State<FirstFragment> {
int pos;
_FirstFragmentState(this.pos);
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Center(
child: new Text("Hello Fragment $pos"), >printing 'pos' only. It remains
> same all time when new class is called.
);
}
}
if I am using stateless widget then its being updated, but stateful widget is not being updated. I've tried to debug using breakpoints but _FirstFragmentState class is called only once. Is there any way to redraw all widgets when its called second time.
The state is created once and then shared for multiple instances of your widget. Since you're taking pos in the state constructor, it's not being updated later when widgets change.
One way to solve this would be to remove the pos in your _FirstFragmentState, and reference the pos in FirstFragment directly. You can access it through the widget field of your state class.
class _FirstFragmentState extends State<FirstFragment> {
#override
Widget build(BuildContext context) {
return new Center(
child: new Text("Hello Fragment ${widget.pos}"), // -> use pos from FirstFragment
);
}
}