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(...);
}
Related
I'm creating an app gallery. and can not update the images, I am always coming across the first image although the pagecontroller is correct, please help me to solve this bug.
class _ViewerPageState extends State<ViewerPage> {
List<String> img = [];
int idx = 0;
late PageController pageController;
#override
void initState() {
super.initState();
pageController = PageController(initialPage: idx);
img = widget.media.map((e) => e.id).toList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
child: widget.medium.mediumType == MediumType.image
? PhotoViewGallery.builder(
pageController: pageController,
builder: (BuildContext context, int index) {
return PhotoViewGalleryPageOptions(
heroAttributes: PhotoViewHeroAttributes(
tag: Text(widget.medium.filename.toString())),
imageProvider: PhotoProvider(mediumId: img[index]));
},
itemCount: img.length,
onPageChanged: onPageChanged,
)
: VideoProvider(
mediumId: widget.medium.id,
),
),
);
}
void onPageChanged(int index) {
setState(() {
idx = index;
});
}
}
I also have this error when I press the return button after displaying the photos
'' Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method. ''
I have a pageview inside it a custom widget in which I'm passing the scrollController after initialising. And also have some function to manipulate scrollController from that widget but whenever it reaches to run manipulation part it gives me this error.
Unhandled Exception: 'package:flutter/src/widgets/scroll_controller.dart': Failed assertion: line 109 pos 12: '_positions.length == 1': ScrollController attached to multiple scroll views.
code
class WeekView<T> extends StatefulWidget {
#override
WeekViewState<T> createState() => WeekViewState<T>();
}
class WeekViewState<T> extends State<WeekView<T>> {
late ScrollController _scrollController;
late PageController _pageController;
#override
void initState() {
super.initState();
_pageController = PageController(initialPage: _currentIndex);
_scrollController = ScrollController();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (widget.enableScrollToEvent) {
if (widget.scrollToEvent == ScrollToEvent.currentTime &&
_controller.events.last.endTime != null) {
_controller.addListener(() {
scrollToCurrentTime(_controller.events.last.endTime!);
});
} else {
_controller.addListener(scrollToEvent);
}
}
}
#override
Widget build(BuildContext context) {
return PageView.builder(
itemCount: _totalWeeks,
controller: _pageController,
onPageChanged: _onPageChange,
itemBuilder: (_, index) { return InternalWeekViewPage<T>(
scrollController: _scrollController,
);
},
),
}
the function,
void scrollToEvent() {
if (_pageController.hasClients) {
_pageController
.animateToPage(
_pageController.initialPage +
((_controller.events.last.date.getDayDifference(DateTime.now())) /
7)
.floor(),
curve: widget.pageTransitionCurve,
duration: widget.pageTransitionDuration,
)
.then((value) {
if (_scrollController.hasClients) {//<<<<<<<<<<< scrollController
if (_controller.events.last.endTime != null) {
_scrollController.animateTo(
math.max(
_controller.events.last.endTime!.hour * _hourHeight -
_scrollController.position.viewportDimension +
_hourHeight,
0),
duration: widget.scrollTransitionDuration,
curve: widget.scrollToEventCurve,
);
}
}
});
}
}
the custom widget,
class InternalWeekViewPage<T> extends StatelessWidget {
const InternalWeekViewPage({
required this.scrollController,
});
#override
Widget build(BuildContext context) {
return Expanded(
child: SingleChildScrollView(
controller: scrollController,
child:(...)
),);
}
}
Note-: I have removed some unnecessary part for more readability
when I scroll manually and then I do the manipulation then it doesn't give me any error but doing it directly give me error.
so why I'm getting this error even though I'm using it only in one place can anyone help me
From this code
itemBuilder: (_, index) {
return InternalWeekViewPage<T>(
scrollController: _scrollController,
);
Item builder returning List of InternalWeekViewPage and having the same _scrollController.
Instead of passing _scrollController, make InternalWeekViewPage statefullWidgte and create and initialize on initState.
Does it solve your issue?
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.
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()),
);
}
}
I'm trying to avoid rebuilding FutureBuilder in flutter. I have tried solution suggested in below Q's.
How to parse JSON only once in Flutter
Flutter Switching to Tab Reloads Widgets and runs FutureBuilder
still my app fires API every time I navigate to that page. Please point me where I'm going wrong.
Util.dart
//function which call API endpoint and returns Future in list
class EmpNetworkUtils {
...
Future<List<Employees>> getEmployees(data) async {
List<Employees> emps = [];
final response = await http.get(host + '/emp', headers: { ... });
final responseJson = json.decode(response.body);
for (var empdata in responseJson) {
Employees emp = Employees( ... );
emps.add(emp);
}
return emps;
}
}
EmpDetails.dart
class _EmpPageState extends State<EmpPage>{
...
Future<List<Employees>>_getAllEmp;
#override
initState() {
_getAllEmp = _getAll();
super.initState();
}
Future <List<Employees>>_getAll() async {
_sharedPreferences = await _prefs;
String authToken = AuthSessionUtils.getToken(_sharedPreferences);
return await EmpNetworkUtils().getEmployees(authToken);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar( ... ),
body: Container(
child: FutureBuilder(
future: _getAllEmp,
builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
)))
}
}
Update:
I'm using bottomNavigationBar in my app, from which this page is loaded.
You are calling your getEmployees function in initState, which is meant to be called every time your widget is inserted into the tree. If you want to save the data after calling your function the first time, you will have to have a widget that persists.
An easy implementation would be using an InheritedWidget and a data class:
class InheritedEmployees extends InheritedWidget {
final EmployeeData employeeData;
InheritedEmployees({
Key key,
#required Widget child,
}) : assert(child != null),
employeeData = EmployeeData(),
super(key: key, child: child);
static EmployeeData of(BuildContext context) => (context.inheritFromWidgetOfExactType(InheritedEmployees) as InheritedEmployees).employeeData;
#override
bool updateShouldNotify(InheritedEmployees old) => false;
}
class EmployeeData {
List<Employees> _employees;
Future<List<Employees>> get employees async {
if (_employees != null) return _employees;
_sharedPreferences = await _prefs;
String authToken = AuthSessionUtils.getToken(_sharedPreferences);
return _employees = await EmpNetworkUtils().getEmployees(authToken);
}
}
Now, you would only have to place your InheritedEmployees somewhere that will not be disposed, e.g. about your home page, or if you want, even about your MaterialApp (runApp(InheritedEmployees(child: MaterialApp(..));). This way the data is only fetched once and cached after that. You could also look into AsyncMemoizer if that suits you better, but the example I provided should work fine.
Now, you will want to call this employees getter in didChangeDependencies because your _EmpPageState is dependent on InheritedEmployees and you need to look that up, which cannot happen in initState:
class _EmpPageState extends State<EmpPage>{
Future<List<Employees>>_getAllEmp;
#override
void didChangeDependencies() {
_getAllEmp = InheritedEmployees.of(context).employees;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar( ... ),
body: Container(
child: FutureBuilder(
future: _getAllEmp,
builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
)))
}
}
I mentioned that your State is now dependent on your InheritedWidget, but that does not really matter as updateShouldNotify always returns false (there are not going to be any additional builds).
I got another way to solve this issue and apply to my app also
Apply GetX controller to call API and render response data
Remove FutureBuilder to call API data
Apply GetX controller to call API data, Like
class NavMenuController extends GetxController {
Api api = new Api();
var cart = List<NavMenu>().obs;
#override
void onInit() {
// TODO: implement onInit
getNavMenuData();
super.onInit();
}
Future<List<NavMenu>> getNavMenuData() async {
var nav_data = await api.getNavMenus();
if(nav_data!=null) {
cart.value = nav_data;
}
return cart;
}
}
Call API using controller on initState() into desired class
class NavMenuDrawer extends StatefulWidget {
#override
_NavMenuDrawerState createState() => _NavMenuDrawerState();
}
class _NavMenuDrawerState extends State<NavMenuDrawer> {
final NavMenuController navMenuController = Get.put(NavMenuController());
#override
void initState() {
// TODO: implement initState
super.initState();
navMenuController.getNavMenuData();
}
Remove below FutureBuilder code for calling API, [if you use FutureBuilder/StreamBuilder whatever]
return FutureBuilder<List<NavMenu>>(
future: api.getNavMenus(),
builder: (context, snapshot) {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
title: Text("${snapshot.data[index].title}"),
Just use GetX controller to get data, like
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
itemCount: navMenuController.cart?.length ?? 0,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
title: Obx(() {
return Text(
"${navMenuController.cart.value[index].title}");
}),
Note : For more info you can search on how to apply GetX