i previously asked a question about widgets not being updated here:
flutter slider not updating widget variables
i got a great answer which explained to me more about how states work and i experimented a little further and now have an issue where my widget inside a list is not being updated even though i update the state in a setstate.
The Widget in question not being updated is the TestBoxNumber widget in the testBoxList list after it has been added to the list. I realize that if i change the builder to return the widget itself rather than from the list it works, and i'm not sure why this is the case!
Once again any help would be greatly appreciated and i hope this helps someone facing the same issue as well :)
Main Page Code
class TestPage extends StatefulWidget {
static const id = "test_page";
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
List testBoxList = [];
List testSlideList = [];
List testParamList = [];
void updateFunc(ind, newVal) {
setState(() {
testParamList[ind] = newVal;
});
}
void addSlider() {
setState(() {
double slideValue = 0;
testParamList.add(slideValue);
int boxIndex = testParamList.length - 1;
testBoxList.add(TestBoxNumber(
numberDisplay: testParamList,
boxIndex: boxIndex,
));
testSlideList.add(TestSlider(
testValue: testParamList,
updateFunc: updateFunc,
boxIndex: boxIndex,
));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
addSlider();
},
),
body: Padding(
padding: const EdgeInsets.all(30.0),
child: ListView(
children: [
Text("Test Page"),
// Builder for viewers
ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: testBoxList.length,
itemBuilder: (BuildContext ctx, int index) {
return testBoxList[index];
// return Text(testParamList[index].toString());
// return TestBoxNumber(
// numberDisplay: testParamList, boxIndex: index);
},
),
// Builder for sliders
ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: testSlideList.length,
itemBuilder: (BuildContext ctx, int index) {
return testSlideList[index];
},
),
],
),
),
);
}
}
TestBoxNumber Widget
class TestBoxNumber extends StatelessWidget {
final List numberDisplay;
final int boxIndex;
TestBoxNumber({required this.numberDisplay, required this.boxIndex});
Widget build(BuildContext context) {
return Text(this.numberDisplay[this.boxIndex].toString());
}
}
Slider Widget
class TestSlider extends StatefulWidget {
List testValue;
dynamic updateFunc;
int boxIndex;
TestSlider({
required this.testValue,
required this.updateFunc,
required this.boxIndex,
});
#override
_TestSliderState createState() => _TestSliderState();
}
class _TestSliderState extends State<TestSlider> {
// double curValue = widget.testValue;
#override
Widget build(BuildContext context) {
double curValue = widget.testValue[widget.boxIndex];
return Slider(
activeColor: themeData.primaryColorLight,
value: curValue,
min: 0,
max: 100,
divisions: 50,
label: curValue.round().toString(),
onChanged: (double value) {
setState(() {
curValue = value;
});
widget.updateFunc(widget.boxIndex, value);
},
);
}
}
Me again )
Ok, so what is wrong right now is that you are using widgets, stored in the list instead of creating ones again:
You should not do this:
ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: testBoxList.length,
itemBuilder: (BuildContext ctx, int index) {
return testBoxList[index];
// return Text(testParamList[index].toString());
// return TestBoxNumber(
// numberDisplay: testParamList, boxIndex: index);
},
)
but return new TestBoxNumber widgets (you actually has it commented, not sure why you did that):
ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemCount: testBoxList.length,
itemBuilder: (BuildContext ctx, int index) {
return TestBoxNumber(numberDisplay: testParamList, boxIndex: index);
},
)
so you will render widgets from scratch instead of pulling it from memory (list) and causing some weird things. Flutter is pretty optimized for such re-rendering.
So summarizing all of above: just pass data into widgets in build method. Do not store widgets in memory to reuse later.
UPD: also you can just pass double (let's call it yourDoubleValue) into TestBoxNumber instead of list and index. And then use Text('$yourDoubleValue');
Related
I have a details page and it has a related items section
I was using get.put, but the page did not change by clicking on one of these elements and using Get.toNamed. I searched and found that Get.put should be replaced by Get.create, but the elements still did not change. Why??
Controller:
class EpisodeController extends GetxController
with StateMixin<List<TvEpisode>> {
final TvApiProvider tvApiProvider;
EpisodeController({required this.tvApiProvider});
final String _sequence = Get.parameters['sequence']!;
#override
void onInit() {
_getNextEpisode();
super.onInit();
}
void _getNextEpisode() async {
await tvApiProvider
.getNextEpisode(
sequence: _sequence)
.then((value) {
change(value, status: RxStatus.success());
}, onError: (err) {
change(null, status: RxStatus.error(err.toString()));
});
}
}
View:
class NextEpisode extends GetWidget<EpisodeController> {
const NextEpisode({super.key});
#override
Widget build(BuildContext context) {
return controller.obx(
(data) => SizedBox(
height: 375,
child: ListView.builder(
reverse: true,
scrollDirection: Axis.horizontal,
itemCount: data!.length,
itemBuilder: (context, index) {
return movieCard(
data[index],
data[index].id,
data[index].title,
data[index].seriesId!,
data[index].seriesTitle!);
},
),
),
}
The listView.builder that is inside of the first, when i scroll the screen delete the data.
I used .insert that adds another widget inside the list.
in the first ListView.builder the data does not dissapear.
this what i do.
this is the first listView that keeps the data.
class _TestsPage extends State<TestsPage> with TickerProviderStateMixin {
final _commentController = TextEditingController();
bool _isWriting = false;
final List<CommentaryBox> _commentariBox = [];
#override
Widget build(BuildContext context) {
final model = Provider.of<Model>(context);
return DraggableScrollableSheet(
expand: false,
maxChildSize: 0.8,
initialChildSize: 0.6,
minChildSize: 0.6,
builder: (BuildContext context, ScrollController controller) => Column(
children: [
Expanded(
child: ListView.builder(
controller: controller,
physics: const BouncingScrollPhysics(),
itemBuilder: (_, i) => _commentariBox[i],
itemCount: _commentariBox.length,
//
reverse: false,
),
),
second listView.builder that delete data.
Visibility(
visible: _showComments,
child: ExpansionTile(
// initiallyExpanded: true,
title: _deployText
? Text('see less commentaries')
: Text('see commentaries'),
onExpansionChanged: (bool expanded) {
setState(
() {
_deployText = expanded;
},
);
},
children: [
ListView.builder(
physics: BouncingScrollPhysics(),
shrinkWrap: true,
itemBuilder: (_, i) => responseBox[i],
itemCount: responseBox.length,
reverse: true,
),
],
),
),
the way how I insert data to the list is the same for both
_handleResponse(String reply) {
final model = Provider.of<Model>(context, listen: false);
if (reply.isEmpty) return;
respController.clear();
final newAnswer = ResponseWidget(
reply: reply,
animationController: AnimationController(
vsync: this,
duration: Duration(milliseconds: 400),
),
);
responseBox.insert(0, newAnswer);
newAnswer.animationController.forward();
setState(() {
model.showComments= true;
});
}
}
I found the solution!
I just had to add this in my appState this: AutomaticKeepAliveClientMixin
in the constructor this:
#override
Widget build(BuildContext context) {
super.build(context);
and add the implemetation:
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
The below code does not display any data when the bottomsheet loads. Once the bottomsheet is loaded if I do a save operation on the code editor it loads the data. What am I missing here?
I have a bottomsheet widget which is invoked using a button.
_showBottomSheet() {
showModalBottomSheet(
context: context,
builder: (context) {
return const Contacts();
},
);
}
The above code loads up the Contacts widget that has a Listview.builder in it which is below.
class Contacts extends StatefulWidget {
const Contacts({Key? key}) : super(key: key);
#override
_ContactsState createState() => _ContactsState();
}
class _ContactsState extends State<Contacts> {
List<PhoneBookContact> phoneBookContacts1 = [];
List<PhoneBookContact> phoneBookContacts2 = [];
#override
void initState() {
loadContacts();
super.initState();
}
Future loadContacts() async {
///somecode to gather data for the listview builder
///populates the phoneBookContacts1 & phoneBookContacts2 lists
}
#override
Widget build(BuildContext context) {
return Column(children: [
const Text('Contacts Set 1'),
displayPhoneBookContacts(phoneBookContacts1),
const Text('Contacts Set 2'),
displayPhoneBookContacts(phoneBookContacts2),
]);
}
Widget displayPhoneBookContacts(phoneBookContacts) {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: phoneBookContacts.length,
itemBuilder: (BuildContext context, int index) {
return Card(
margin: const EdgeInsets.all(10),
child: ListTile(
contentPadding: const EdgeInsets.all(10),
title: Column(
children: [
Text(phoneBookContacts[index].phoneBookContact.toString()),
const SizedBox(
height: 20,
),
ListView.separated(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
itemCount: phoneBookContacts[index].contactNumbers!.length,
separatorBuilder: (BuildContext context, int index) =>
const Divider(),
itemBuilder: (BuildContext context, int phoneIndex) {
return InkWell(
onTap: () {},
child: Row(
children: [
Text(phoneBookContacts[index]
.contactNumbers![phoneIndex]
.phone),
],
),
);
},
),
],
),
),
);
},
),
);
}
}
I don't prefer using FutureBuilder inside StatefulWidget., it will recall the API(future) on every setState. As for comment it is missing setState after initializing the data.
#override
void initState() {
super.initState();
loadContacts();
}
Future loadContacts() async {
///somecode to gather data for the listview builder
///populates the phoneBookContacts1 & phoneBookContacts2
if(mounted){
// if widget build then setState call.if not we don't need to call setState
// for every initstate data loading, we have to ensure it if widget is build or not. most of the case user close screen when data loading, then error happens
setState(() {});// make sure to call setState
}
}
Because function initState() don't await your loadContacts(), data loaded after function build().
You need use FutureBuilder class to rebuild ListView widget after load data
Example:
FutureBuilder(
future: loadContacts(),
builder:(context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
return Container(
child: ListView.builder(
itemCount: _faouriteList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Text('${_faouriteList[index].title}');
}
)
);
}
}
)
I want to implement pagination in GridView I use GridView.builder I want to download 10 by 10 items when the user reaches the last row
You can do this using a NotificationListener. As a simple demonstration it will increase the length of your GridView whenever it reaches end of page :
var items_number = 10 ;
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification){
if(scrollNotification.metrics.pixels == scrollNotification.metrics.maxScrollExtent){
setState(() {
items_number += 10 ;
});
}
},
child: GridView.builder(
itemCount: items_number,
itemBuilder: (context, index) {
//.... the reminder of your code
}
),
);
I also needed this but couldn't find any widget for the gridview pagination, so I tried to make a component based on #Mazin Ibrahim's answer below. It seems to be working but not sure if it is the right way to do this.
typedef Future<bool> OnNextPage(int nextPage);
class GridViewPagination extends StatefulWidget {
final int itemCount;
final double childAspectRatio;
final OnNextPage onNextPage;
final Function(BuildContext context, int position) itemBuilder;
final Widget Function(BuildContext context) progressBuilder;
GridViewPagination({
this.itemCount,
this.childAspectRatio,
this.itemBuilder,
this.onNextPage,
this.progressBuilder,
});
#override
_GridViewPaginationState createState() => _GridViewPaginationState();
}
class _GridViewPaginationState extends State<GridViewPagination> {
int currentPage = 1;
bool isLoading = false;
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification sn) {
if (!isLoading && sn is ScrollUpdateNotification && sn.metrics.pixels == sn.metrics.maxScrollExtent) {
setState(() {
this.isLoading = true;
});
widget.onNextPage?.call(currentPage++)?.then((bool isLoaded) {
setState(() {
this.isLoading = false;
});
});
}
return true;
},
child: CustomScrollView(
slivers: <Widget>[
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 1,
mainAxisSpacing: 1,
crossAxisCount: 2,
childAspectRatio: widget.childAspectRatio,
),
delegate: SliverChildBuilderDelegate(
widget.itemBuilder,
childCount: widget.itemCount,
addAutomaticKeepAlives: true,
addRepaintBoundaries: true,
addSemanticIndexes: true,
),
),
if (isLoading)
SliverToBoxAdapter(
child: widget.progressBuilder?.call(context) ?? _defaultLoading(),
),
],
),
);
}
Widget _defaultLoading() {
return Container(
padding: EdgeInsets.all(15),
alignment: Alignment.center,
child: CircularProgressIndicator(),
);
}
}
Example -
GridViewPagination(
itemCount: 10,
childAspectRatio: 1,
itemBuilder: _buildGridItem,
onNextPage: (int nextPage) {
return fetchData();
},
)
create a Scroll controller
ScrollController _scrollController = new ScrollController();
add a scroll event listener
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// Bottom poistion
}
});
}
Now just need to set the controller in your GridView, ListViewand ...
GridView.builder(
controller: _scrollController,
));
You can use this plugin here: Paging. Wrap your GridView inside of it and tell me if this works!
I have my widgets setup in the following hierarchy to pass data between the two tabs
DataShareWidget
TabBarView
InputManagment
InfiniteListView
The DataShareWidget extends InheritedWidget and contains the ShareData class which has a StreamController to send and receive data.
but on the receive side (InfiniteListView tab) am getting duplicate data.
I've printing out the raw data from InputManagment before entering the stream, but there does not appear to be any duplicate data, so it must be something with the stream.
Here the relevant code from the main file
class ShareData {
final StreamController _streamController = StreamController.broadcast();
Stream get stream => _streamController.stream;
Sink get sink => _streamController.sink;
}
class DataShareWidget extends InheritedWidget {
final ShareData data;
DataShareWidget({
Key key,
#required Widget child,
}) :assert(child != null),
data = ShareData(),
super(key: key, child: child);
static ShareData of (BuildContext context) => (context.inheritFromWidgetOfExactType(DataShareWidget) as DataShareWidget).data;
#override
bool updateShouldNotify(DataShareWidget old) => false;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("MyApp"),
bottom: TabBar(
tabs: Tabs,
controller: _tabController,
),
),
body: DataShareWidget(
child: TabBarView(
controller: _tabController,
children: [
InputManagment(),
InfiniteListView(),
],
),
),
);
}
In the data management file i have this line of code to add data
DataShareWidget.of(context).sink.add(inputData);
And here is the code for InfiniteListView
class _InfiniteScrollListViewState extends State<InfiniteScrollListView> with AutomaticKeepAliveClientMixin<InfiniteScrollListView>{
#override
bool get wantKeepAlive => true;
ScrollController _scrollController = ScrollController();
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
var _listViewData = new List();
_dataFormat(data){
var time = DateFormat('kk:mm:ss').format(DateTime.now());
var timeStampedData = time.toString() + "| " + data;
setState(() {_listViewData.add(timeStampedData); });
}
#override
Widget build(BuildContext context) {
DataShareWidget.of(context).stream.listen((data) => _dataFormat(data));
return ListView.builder(
itemCount: _listViewData.length,
controller: _scrollController,
itemBuilder: (context, index) {
return ListTile(
title: AutoSizeText(_listViewData[index], maxLines: 2),
dense: true,
);
},
);
}
}
EDIT: As per #jamesdlin suggestion i've refactored the code using StreamBuilder, and that appears to have solved the issue, here is the updated code below.
_dataFormat(data){
var time = DateFormat('kk:mm:ss').format(DateTime.now());
var timeStampedData = time.toString() + "| " + data;
_listViewData.add(timeStampedData);
}
#override
Widget build(BuildContext context) {
//_scrollToBottom();
return StreamBuilder(
stream: DataShareWidget.of(context).stream,
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.hasError){ return Text(snapshot.error);}
if(snapshot.hasData){
_dataFormat(snapshot.data);
return ListView.builder(
itemCount: _listViewData.length,
controller: _scrollController,
itemBuilder: (context, index) {
return ListTile(
title: AutoSizeText(_listViewData[index], maxLines: 2),
dense: true,
);
},
);
}
}
);
You call listen on the Stream every time _InfiniteScrollListViewState.build is called. That will result in your callback being invoked multiple times. You should listen to the Stream only once.
You also perhaps should consider using a StreamBuilder widget instead.