I am trying to build my own custom pull to refresh indicator for ListView.
I am using a ChangeNotifier
whose offset variable gets updated by the ListView's scroll controller. My custom refresh indicator uses a Consumer to update its UI and also if it detects the scroll offset is less than a certain value, then it calls the onRefresh
function.
The onRefresh
function is supposed to clear all the existing items, then get new items and display them on the listview.
This is where the crash occurs. If I remove the code which clears the items, then it works. However, with it, the crash occurs with this error:
Exception has occurred.
FlutterError (setState() or markNeedsBuild() called during build.
This ViewController widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
ViewController
The widget which was currently being built when the offending call was made was:
Consumer<ScrollProvider>)
Here's my code:
viewcontroller.dart
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:notifydemo/refreshcontrol.dart';
import 'package:provider/provider.dart';
class ViewController extends StatefulWidget {
const ViewController({super.key});
@override
State<ViewController> createState() => _ViewControllerState();
}
class _ViewControllerState extends State<ViewController> {
final scrollController = ScrollController();
final scrollProvider = ScrollProvider();
final refreshIndicatorKey = GlobalKey<RefreshControlState>();
List<String> items = [];
@override
void initState() {
scrollController.addListener(scrollHandler);
WidgetsBinding.instance.addPostFrameCallback((_) {
refreshIndicatorKey.currentState?.show();
});
super.initState();
}
@override
void dispose() {
scrollController.removeListener(scrollHandler);
super.dispose();
}
void scrollHandler() {
scrollProvider.setCurrentOffset(scrollController.offset);
}
@override
Widget build(BuildContext context) {
print("Build");
return ChangeNotifierProvider(
create: ((_) => scrollProvider),
child: RefreshControl(
key: refreshIndicatorKey,
onRefresh: fetchData,
child: ListView.builder(
controller: scrollController,
itemBuilder: ((context, index) {
return Text(
"$index. ${items[index]}",
style: const TextStyle(fontSize: 40),
);
}),
itemCount: items.length,
),
),
);
}
Future fetchData() async {
setState(() {
items.clear();
});
final returned = await slowNumbers();
setState(() {
items.addAll(returned);
});
}
}
Future<List<String>> slowNumbers() async {
return Future.delayed(
const Duration(milliseconds: 2000),
() => List.generate(50, (index) => Random().nextInt(99999).toString()),
);
}
class ScrollProvider extends ChangeNotifier {
double? currentOffset;
void setCurrentOffset(valueToSet) {
if (currentOffset != valueToSet) {
currentOffset = valueToSet;
print("currentOffset: $currentOffset");
notifyListeners();
}
}
}
refreshcontrol.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'viewcontroller.dart';
class RefreshControl extends StatefulWidget {
const RefreshControl({super.key, required this.onRefresh, required this.child});
final Widget child;
final Future<void> Function() onRefresh;
@override
State<RefreshControl> createState() => RefreshControlState();
}
class RefreshControlState extends State<RefreshControl> {
bool isRefreshing = false;
void show() {
widget.onRefresh();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
Positioned(
left: 0,
top: 50,
right: 0,
child: CupertinoButton(
onPressed: show,
child: Consumer<ScrollProvider>(
builder: ((context, provider, child) {
if (!isRefreshing && (provider.currentOffset ?? 0) < -100) {
isRefreshing = true;
show();
}
return Text(
// "Offset: ${scrollProvider.currentOffset?.toInt()}",
// "Offset: ${Provider.of<ScrollProvider>(context, listen: false).currentOffset?.toInt()}",
"Offset: ${provider.currentOffset?.toInt()}",
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
backgroundColor: Colors.black,
),
);
}),
),
),
),
],
);
}
}
Try to call
setState
only once infetchData
: