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. ''
Related
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(...);
}
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'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()),
);
}
}
How to solve the exception -
Unhandled Exception: 'package:flutter/src/widgets/page_view.dart': Failed assertion: line 179 pos 7: 'positions.isNotEmpty': PageController.page cannot be accessed before a PageView is built with it.
Note:- I used it in two screens and when I switch between screen it shows the above exception.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _animateSlider());
}
void _animateSlider() {
Future.delayed(Duration(seconds: 2)).then(
(_) {
int nextPage = _controller.page.round() + 1;
if (nextPage == widget.slide.length) {
nextPage = 0;
}
_controller
.animateToPage(nextPage,
duration: Duration(milliseconds: 300), curve: Curves.linear)
.then(
(_) => _animateSlider(),
);
},
);
}
I think you can just use a Listener like this:
int _currentPage;
#override
void initState() {
super.initState();
_currentPage = 0;
_controller.addListener(() {
setState(() {
_currentPage = _controller.page.toInt();
});
});
}
I don't have enough information to see exactly where your problem is, but I just encountered a similar issue where I wanted to group a PageView and labels in the same widget and I wanted to mark active the current slide and the label so I was needing to access controler.page in order to do that. Here is my fix :
Fix for accessing page index before PageView widget is built using FutureBuilder widget
class Carousel extends StatelessWidget {
final PageController controller;
Carousel({this.controller});
/// Used to trigger an event when the widget has been built
Future<bool> initializeController() {
Completer<bool> completer = new Completer<bool>();
/// Callback called after widget has been fully built
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete(true);
});
return completer.future;
} // /initializeController()
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
// **** FIX **** //
FutureBuilder(
future: initializeController(),
builder: (BuildContext context, AsyncSnapshot<void> snap) {
if (!snap.hasData) {
// Just return a placeholder widget, here it's nothing but you have to return something to avoid errors
return SizedBox();
}
// Then, if the PageView is built, we return the labels buttons
return Column(
children: <Widget>[
CustomLabelButton(
child: Text('Label 1'),
isActive: controller.page.round() == 0,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 2'),
isActive: controller.page.round() == 1,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 3'),
isActive: controller.page.round() == 2,
onPressed: () {},
),
],
);
},
),
// **** /FIX **** //
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
CustomPage(),
CustomPage(),
CustomPage(),
],
),
],
);
}
}
Fix if you need the index directly in the PageView children
You can use a stateful widget instead :
class Carousel extends StatefulWidget {
Carousel();
#override
_HomeHorizontalCarouselState createState() => _CarouselState();
}
class _CarouselState extends State<Carousel> {
final PageController controller = PageController();
int currentIndex = 0;
#override
void initState() {
super.initState();
/// Attach a listener which will update the state and refresh the page index
controller.addListener(() {
if (controller.page.round() != currentIndex) {
setState(() {
currentIndex = controller.page.round();
});
}
});
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Column(
children: <Widget>[
CustomLabelButton(
child: Text('Label 1'),
isActive: currentIndex == 0,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 2'),
isActive: currentIndex == 1,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 3'),
isActive: currentIndex == 2,
onPressed: () {},
),
]
),
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
CustomPage(isActive: currentIndex == 0),
CustomPage(isActive: currentIndex == 1),
CustomPage(isActive: currentIndex == 2),
],
),
],
);
}
}
This means that you are trying to access PageController.page (It could be you or by a third party package like Page Indicator), however, at that time, Flutter hasn't yet rendered the PageView widget referencing the controller.
Best Solution: Use FutureBuilder with Future.value
Here we just wrap the code using the page property on the pageController into a future builder, such that it is rendered little after the PageView has been rendered.
We use Future.value(true) which will cause the Future to complete immediately but still wait enough for the next frame to complete successfully, so PageView will be already built before we reference it.
class Carousel extends StatelessWidget {
final PageController controller;
Carousel({this.controller});
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
FutureBuilder(
future: Future.value(true),
builder: (BuildContext context, AsyncSnapshot<void> snap) {
//If we do not have data as we wait for the future to complete,
//show any widget, eg. empty Container
if (!snap.hasData) {
return Container();
}
//Otherwise the future completed, so we can now safely use the controller.page
return Text(controller.controller.page.round().toString);
},
),
//This PageView will be built immediately before the widget above it, thanks to
// the FutureBuilder used above, so whenever the widget above is rendered, it will
//already use a controller with a built `PageView`
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
AnyWidgetOne(),
AnyWidgetTwo()
],
),
],
);
}
}
Alternatively
Alternatively, you could still use a FutureBuilder with a future that completes in addPostFrameCallback in initState lifehook as it also will complete the future after the current frame is rendered, which will have the same effect as the above solution. But I would highly recommend the first solution as it is straight-forward
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
//Future will be completed here
// e.g completer.complete(true);
});
use this widget and modify it as you want:
class IndicatorsPageView extends StatefulWidget {
const IndicatorsPageView({
Key? key,
required this.controller,
}) : super(key: key);
final PageController controller;
#override
State<IndicatorsPageView> createState() => _IndicatorsPageViewState();
}
class _IndicatorsPageViewState extends State<IndicatorsPageView> {
int _currentPage = 0;
#override
void initState() {
widget.controller.addListener(() {
setState(() {
_currentPage = widget.controller.page?.toInt() ?? 0;
});
});
super.initState();
}
#override
void dispose() {
widget.controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
3,
(index) => IndicatorPageview(isActive: _currentPage == index, index: index),
),
);
}
}
class IndicatorPageview extends StatelessWidget {
const IndicatorPageview({
Key? key,
required this.isActive,
required this.index,
}) : super(key: key);
final bool isActive;
final int index;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(right: 8),
width: 16,
height: 16,
decoration: BoxDecoration(color: isActive ?Colors.red : Colors.grey, shape: BoxShape.circle),
);
}
}
Want to start off by saying this is unrelated to what is being discussed here as I use the Bloc pattern.
I have a widget where I create a CustomListView with multiple SliverLists based on the items returned by the StreamBuilder on top of the CustomListView. Each SliverList is infinite in the sense that the childCount is set to null. This is for lazy loading purposes. The problem is that when I push to and pop back from a page, all the items of all the SliverLists are rebuild, which causes a delay, especially when I'm already pretty far down the list.
I thought perhaps this might be solvable with Keys, but this seems to be unrelated to that? I think the issue is that I'm rebuilding the list of SliverLists dynamically in the build method (see build() in _ItemsBrowserState). The solution that I can think of is storing these widgets inside the state, but that just seems like I'm treating the symptom rather than the cause? I feel the same way about using AutomaticKeepAliveClientMixin, but feel free to change my mind on this.
class ItemsBrowser extends StatefulWidget {
final RepositoryBloc repoBloc;
ItemsBrowser({Key key, #required this.repoBloc}) : super(key: key);
#override
_ItemsBrowserState createState() => _ItemsBrowserState();
}
class _ItemsBrowserState extends State<ItemsBrowser> {
ScrollController _scrollController;
ItemBrowsersBloc bloc;
List<ItemBrowserBloc> blocs = [];
int atBloc = 0;
bool _batchLoadListener(ScrollNotification scrollNotification) {
if (!(scrollNotification is ScrollUpdateNotification)) return false;
if (_scrollController.position.extentAfter > 500) return false;
if (atBloc == blocs.length) return false;
if (blocs[atBloc].isLoading.value) return false;
if (blocs[atBloc].wasLastPage) atBloc++;
if (atBloc < blocs.length) blocs[atBloc].loadNextBatch();
return false;
}
#override
void initState() {
super.initState();
bloc = ItemBrowsersBloc(widget.repoBloc);
bloc.collections.listen((collections) {
if (_scrollController.hasClients) _scrollController.jumpTo(0.0);
_disposeItemBlocs();
atBloc = 0;
blocs = [];
for (var i = 0; i < collections.length; i++) {
var itemBloc = ItemBrowserBloc(collections[i], initLoad: i == 0);
blocs.add(itemBloc);
}
});
_scrollController = ScrollController();
}
void _disposeItemBlocs() {
if (blocs != null) {
for (var b in blocs) {
b.dispose();
}
}
}
#override
void dispose() {
super.dispose();
bloc?.dispose();
_disposeItemBlocs();
}
#override
Widget build(BuildContext context) {
print('Building Item Browser');
return StreamBuilder<List<Collection>>(
stream: bloc.collections,
builder: (context, snapshot) {
if (!snapshot.hasData) return Container();
List<Widget> slivers = [];
for (var i = 0; i < snapshot.data.length; i++) {
slivers.add(ItemList(blocs[i], key: UniqueKey()));
slivers.add(_buildLoadingWidget(i));
}
slivers.add(const SliverToBoxAdapter(
child: const SizedBox(
height: 90,
)));
return NotificationListener<ScrollNotification>(
onNotification: _batchLoadListener,
child: CustomScrollView(
controller: _scrollController, slivers: slivers),
);
});
}
Widget _buildLoadingWidget(int index) {
return StreamBuilder(
stream: blocs[index].isLoading,
initialData: true,
builder: (context, snapshot) {
return SliverToBoxAdapter(
child: Container(
child: snapshot.data && !blocs[index].initLoaded
? Text(
'Loading more...',
style: TextStyle(color: Colors.grey.shade400),
)
: null,
),
);
},
);
}
}
class ItemList extends StatelessWidget {
final ItemBrowserBloc bloc;
ItemList(this.bloc, {Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: bloc.isLoading,
initialData: true,
builder: (context, snapshot) {
var isLoading = snapshot.data;
var isInitialLoad = isLoading && !bloc.initLoaded;
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
// Index: 0 1 2 3
// Return: Header Item Item null
print('INDEX $index');
if (index == 0) return _buildHeader();
if (index > bloc.items.value.length) return null;
// var itemIndex = (index - 1) % bloc.batchSize;
var itemIndex = index - 1;
var item = bloc.items.value[itemIndex];
return InkWell(
key: ValueKey<String>(item.key),
child: ItemTile(item),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => ItemPage(item)));
},
);
}, childCount: isInitialLoad ? 0 : null),
);
});
}
Widget _buildHeader() {
return Container();
}
}
Behaviour: I open the page and see the first list. In the logs I see 'INDEX 0', 'INDEX 1', .... 'INDEX 8' (see build() in ItemList), because Flutter lazily builds only the first 9 items. As I scroll down more items are build. I stop at 'INDEX 30' and tap on a item, which pushes a new page. Now the problem: The page loading takes a sec. The logs show 'INDEX 0' ... 'INDEX 30', i.e. all the items are rebuild, causing a delay. I pop the page, and again all items from 0 to 30 are rebuild, causing a delay.
As expected, If I scroll down to the second SliverList, the entirety of the first SliverList and the lazily build items of the second SliverList are all rebuild on push/pop.
Expected behavior: Only the surrounding items should be rebuild.
Ladies and gentleman, we got him:
slivers.add(ItemList(blocs[i], key: UniqueKey()));
Replacing the UniqueKey with a ValueKey (or removing it) removed the awful delay!