Flutter ListView: widgets doesn't update - flutter

I have a listview that shows some items, inside a StatefulWidget :
ListView.separated(
shrinkWrap: true,
itemBuilder: (context, index) {
return RowItem(
item: rowsList[index],
index: index,
);
},
separatorBuilder: (context, index) {
return Divider(
height: 20,
color: Colors.transparent,
);
},
itemCount: rowsList.length,
),
RowItem is a StatefulWidget too.
Everithing works fine when i add and remove items from rowsList, but i'm facing problems when i try to update an existing RowItem. This is my function for update:
void updateRow(int rowIndex, MyItem item) {
setState(() {
rowsList[rowIndex] = item;
});
}
but i still see inside listview the old values for updated item. (MyItem is an object that extends Equatable).
What's wrong?

try RowItem widget try this
var item;
#override
void initState() {
item = widget.item;
super.initState();
}
#override
void didUpdateWidget(covariant SleepDeckStepper oldWidget) {
item = widget.item;
super.didUpdateWidget(oldWidget);
}
problem is item parameter is not updating for RowItem

Reposting my comment. You should use a Key on each RowItem.
More on them from Emily Fortuna here.

Related

Switching from ReorderableListView to ListView breaks Interaction of rows

I have a view which switches between a ListView and a ReorderableListView.
Widget _buildList(
BuildContext context,
List<ExerciseTemplate> exerciseTemplates,
EditWorkoutModel dao,
) {
if (_isInEditingMode) {
return ReorderableListView(
key: ObjectKey('reordeableListView'),
onReorder: ((oldIndex, newIndex) {
dao.reorderIndexes(
oldIndex,
(oldIndex < newIndex) ? newIndex - 1 : newIndex,
);
}),
padding: const EdgeInsets.only(bottom: 120),
children: [
for (var exerciseTemplate in exerciseTemplates)
Provider(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: CompactExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow'),
),
),
],
);
} else {
return ListView.builder(
key: ObjectKey('listView'),
itemCount: exerciseTemplates.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
final exerciseTemplate = exerciseTemplates[index];
return Provider(
// Key is needed here to properly handle deleted rows in the ui.
// Without the key, deleted rows are being shown.
key: ObjectKey(
'${exerciseTemplate.id}_exerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: ExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_exerciseTemplateRow'),
onDelete: () async {
await dao.deleteExercise(exerciseTemplate);
return true;
},
),
);
},
);
}
}
Both lists show the same data, but tapping a button, switches to a ReorderableListView which shows the data with different widgets. Tapping the button again switches back to the ListView.
However, switching forth and back results that I am not able to interact with elements within the row of the ListView. This issue appeared after I added a globalKey for each element in the ListView. I need this key, to properly handle deleting rows, so I can not just remove the key again.
How can I make it work, that I can interact with widgets within the row after I switched to the ReorderableListView and back to the ListView?
Copied from Provider document:
https://pub.dev/packages/provider
DON'T create your object from variables that can change over time.
In such a situation, your object would never update when the value changes.
int count;
Provider(
create: (_) => MyModel(count),
child: ...
)
If you want to pass variables that can change over time to your object, consider using ProxyProvider:
int count;
ProxyProvider0(
update: (_, __) => MyModel(count),
child: ...
)
It's ok to use Global key and switch between ListView and ReorderableListView, see example below:
https://dartpad.dev/?id=fd39a89b67448d86e682dd2c5ec77453
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool reOrder = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(reOrder ? "ReoderableListView" : "ListView"),
),
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
reOrder = !reOrder;
});
}),
body: MyListView(reOrder));
}
}
final data = List.generate(10, (index) => {"title": 'item $index', "value": false});
class MyListView extends StatefulWidget {
final bool reOrder;
const MyListView(this.reOrder, {super.key});
#override
State<MyListView> createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
#override
Widget build(BuildContext context) {
if (widget.reOrder) {
return ReorderableListView(
key: const ObjectKey('reordeableListView'),
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = data.removeAt(oldIndex);
data.insert(newIndex, item);
});
},
children: [
for (var item in data)
ListTile(
key: ObjectKey('${item["title"]}_compactExerciseTemplateRow_provider'),
title: Text(item["title"] as String),
trailing: Text((item["value"] as bool).toString()),
),
],
);
} else {
return ListView.builder(
key: const ObjectKey('listView'),
itemCount: data.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
key: ObjectKey('${data[index]["title"]}_exerciseTemplateRow_provider'),
title: Text(data[index]["title"] as String),
value: (data[index]["value"] as bool),
onChanged: (bool? value) {
setState(() => data[index]["value"] = value!);
},
);
},
);
}
}
}
So the issue was that I was using ObjectKey instead of ValueKey.
The difference between those two is that ObjectKey checks if the identity (the instance) is the same. ValueKey checks the underlying value with the == operator.
My guess here is that by using ObjectKey in my case, flutter is not able to properly replace the old widget with the new one, since the new widget always have a different key. By using ValueKey flutter can distinguish between old and new widgets. Widgets will be in my case replaced after I switch between the lists, because the row widget won't be visible and therefor disposed.
Because the widgets were not properly replaced, somehow the old widgets are still being rendered, but all gesture listeners were already disposed. Therefor no interaction was possible anymore.
These are just my assumption, let me know if I am completely wrong here.

Flutter Riverpod: Using ref.watch Outside of Build Function

In my Flutter app, I have a function gotoPage() that animates a PageView widget to a new page. I'd like this function to be called whenever newPageProvider is updated. How do I "activate" the ref.watch() inside the function gotoPage() when gotoPage() is not part of build() function?
providers.dart
final newPageProvider = StateProvider<int>((ref) => 0);
widget.dart
class _WidgetState extends ConsumerState<WidgetState> {
final pageController = PageController(
initialPage: 0,
);
#override
Widget build(BuildContext context) {
return PageView.builder(
controller: pageController,
itemCount: data.length,
itemBuilder: (context, index) {
return WidgetCard(poi: data[index], itemIndex: index);
},
);
}
void gotoPage() {
final index = ref.watch(newPageProvider);
pageController.animateToPage(
index,
);
}
}
I believe the answer is use ref.listen, not ref.watch (as shown below).
From the RiverPod user guide:
Similarly to ref.watch, it is possible to use ref.listen to observe a
provider.
The main difference between them is that, rather than
rebuilding the widget/provider if the listened provider changes, using
ref.listen will instead call a custom function.
The listen method should not be called asynchronously, like inside onPressed or
an ElevatedButton. Nor should it be used inside initState and other State life-cycles.
providers.dart
final newPageProvider = StateProvider<int>((ref) => 0);
widget.dart
class _WidgetState extends ConsumerState<WidgetState> {
final pageController = PageController(
initialPage: 0,
);
#override
Widget build(BuildContext context) {
ref.listen<int>(newPageProvider, (int previousIndex, int newIndex) {
_gotoPage(newIndex);
});
return PageView.builder(
controller: pageController,
itemCount: data.length,
itemBuilder: (context, index) {
return WidgetCard(poi: data[index], itemIndex: index);
},
);
}
void gotoPage(index) {
pageController.animateToPage(
index,
);
}
}
EDIT: Use ref.listen in build() instead.
Like so:
#override
Widget build(BuildContext context){
ref.listen(
newPageProvider,
(oldIndex, newIndex){
pageController.animateToPage(newIndex);
}
);
return Scaffold(...);
}

How to update `itemCount` of `ListView.builder` while building the list items in Flutter?

I have a ListView.builder that is building items from a list of items, _cache. When I scroll to the end of the list _cache, a function is called to extend _cache by pulling more data from elsewhere. However, that data is limited, and eventually I run out of items to extend _cache with. So I want ListView.builder to stop building items there.
I understand that ListView.builder has a property called itemsCount, but I don't see how I can add that property only when I run out of items to add to _cache. How do I achieve what I want?
You can use ScrollController to load more data when you reached the end of the list item when you scroll.
This is the simple example I have done in my application.
class OtherUserListScreen extends StatefulWidget {
const OtherUserListScreen({Key? key}) : super(key: key);
#override
State<OtherUserListScreen> createState() => _OtherUserListScreenState();
}
class _OtherUserListScreenState extends State<OtherUserListScreen> {
// create a _scrollController variable
late ScrollController _scrollController;
#override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_handleScroll);
}
#override
Widget build(BuildContext context) {
final _usersProvider = Provider.of<UsersProvider>(context);
return Scaffold(
body: ListView.builder(
itemBuilder: (context, index) {
if (index == _usersProvider.users.length) {
return MoreLoadingIndicator(
isMoreLoading: _usersProvider.isMoreLoading,
);
}
final user = _usersProvider.users[index];
return FadeIn(
duration: const Duration(milliseconds: 400),
delay: Duration(milliseconds: index * 100),
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PatientDetailScreen(
patientType: PatientType.OTHER,
user: user,
),
),
);
},
child: RecordUserDescription(
user: user,
showToolkit: false,
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
),
),
);
},
itemCount: _usersProvider.users.length,
),
);
}
_handleScroll() async {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
await context.read<UsersProvider>().loadMoreUsers(
context,
accountID: context.read<UserProvider>().user!.accountId!,
);
}
return;
}
}

Flutter/Dart - Get index number of PageView.Builder for use in a Text Widget?

I've got a PageView.builder within a StatelessWidget. I need to get the current index number of the currently viewed page to appear in a text widget in my build.
Was hoping I could simply use currentIndex.toString() as a variable in the text widget but Android Studio underlines it in red and warns me of undefined name currentIndex. How can I get the correct variable?
class StageBuilder extends StatelessWidget {
final List<SpeakContent> speakcrafts;
StageBuilder(this.speakcrafts);
final PageController controller = PageController(initialPage: 0);
#override
Widget build(context) {
return PageView.builder(
controller: controller,
itemCount: speakcrafts.length,
itemBuilder: (context, int currentIndex) {
return createViewItem(speakcrafts[currentIndex], context);
},
);
}
Widget createViewItem(SpeakContent speakcraft, BuildContext context) {
return Container(
child: Text(currentIndex.toString()),
)
}
}
You need to pass the currentIndex into your createViewItem
class StageBuilder extends StatelessWidget {
final List<SpeakContent> speakcrafts;
StageBuilder(this.speakcrafts);
final PageController controller = PageController(initialPage: 0);
#override
Widget build(context) {
return PageView.builder(
controller: controller,
itemCount: speakcrafts.length,
itemBuilder: (context, int currentIndex) {
return createViewItem(speakcrafts[currentIndex], context, currentIndex);
},
);
}
Widget createViewItem(SpeakContent speakcraft, BuildContext context, int currentIndex) {
return Container(
child: Text(currentIndex.toString()),
);
}
}

scrollcontroller not attached to any scroll views (Swiper)

I'm using a Swiper package to achieve a carousel effect on my images.
I'm trying to update the current index of my Swiper by passing a callback function to it's child.
but when I try to call the function, it returns this " scrollcontroller not attached " error.
I've added a SwiperController but still the same.
Here is my code:
SwiperController swiperController;
#override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
color: Colors.black,
child: Swiper(
controller: swiperController,
index: _index,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext c, int i) {
return StoriesPerUser(
storiesList: widget.storiesList,
selectedIndex: i,
updateFunction: callBack,
);
},
itemCount: widget.storiesList.length,
loop: false,
duration: 1000,
));
}
callBack() {
setState(() {
_index++;
});
}
Please help.
ANSWER:
If any of you guys want to use this package, and if you want a feature similar to mine, instead of updating the index, just use the one of the methods of SwiperController which is next().
this solved my problem:
callBack() {
setState(() {
swiperController.next();
});
}
Update:
It seems that SwiperController was not instantiated and initialised. You can do it by overriding the initState method:
#override
void initState() {
controller = SwiperController();
controller.length = 10
//controller.fillRange(0, 10, SwiperController());
super.initState();
}