Current scroll offset inside a Flutter ListView, SliverList,etc - flutter

How do I get the current scroll offset inside a Flutter ListView, GridView, SliverList`, etc?

If you're inside the scroll view use Scrollable.of(context).position.pixels.
If you're outside, you probably want to hand a ScrollController in as the controller argument of the scroll view, then you can read controller.offset.
Alternatively, use scroll notifications with NotificationListener.
This was asked on Flutter Gitter, and answered:
https://gitter.im/flutter/flutter?at=591243f18a05641b1167be0e

For someone else, looking for code implementation, you can use ScrollController like this:
Using NotificationListener:
NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
print(scrollNotification.metrics.pixels); // <-- This is it.
return false;
},
child: ListView.builder(
itemCount: 200,
itemBuilder: (c, i) => Text('Item $i'),
),
)
Using ScrollController.offset
final ScrollController _controller = ScrollController();
#override
void initState() {
super.initState();
_controller.addListener(() {
print(_controller.offset); // <-- This is it.
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _controller,
itemCount: 200,
itemBuilder: (c, i) => Text('Item $i'),
),
);
}

Another approach is with NotificationListener
NotificationListener(
child: ListView(
controller: _scrollController,
children: ...
),
onNotification: (notification) {
if (notification is ScrollEndNotification) {
print(_scrollController.position.pixels);
}
return false;
},
)

Related

Hide card in column when scrolling

I have a two cards in a Column and I would like to hide one of them when a List that's under it is scrolled down and show it when scrolling back up.
Code looks like this:
final ScrollController scrollController = ScrollController();
#override
void initState() {
super.initState();
scrollController.addListener(() {
if (scrollController.position.pixels > 0 ||
scrollController.position.pixels <
scrollController.position.maxScrollExtent) {
scrollVisibility = false;
} else {
scrollVisibility = true;
}
setState(() {});
});
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
body: Column(
children: [
stats(),
ActivityWidget(controller: scrollController),
],
),
stats() {
return StreamBuilder ...
return Column(children: [
Card(),
Visibility(
visible: scrollVisibility,
child: Card(... // this is the one that I want hidden
])
}
The ActivityWidget returns this:
return Expanded(
child: Scrollbar(
controller: widget.scrollController,
child: ListView.builder(
controller: widget.scrollController,
itemCount: activityContent!.length,
itemBuilder: (context, i) { ...
In this case an exception is thrown:
The following assertion was thrown while notifying status listeners for AnimationController: The Scrollbar's ScrollController has no ScrollPosition attached. A Scrollbar cannot be painted without a ScrollPosition. The Scrollbar attempted to use the provided ScrollController. This ScrollController should be associated with the ScrollView that the Scrollbar is being applied to.When providing your own ScrollController, ensure both the Scrollbar and the Scrollable widget use the same one.
Another exception was thrown: The Scrollbar's ScrollController has no ScrollPosition attached.
I tried adding the controller to the ListView too, but then it wouldn't scroll.
Also tried adding the controller to the Scrollbar and the ListView but still didn't work.

Is there a way to close a modal bottom sheet in flutter that contains a listview?

So basically I've been trying for a couple of days to allow users to close a modal bottom sheet when they get to the top of the ListView, when swiping on the ListView. However, when they swipe on the list view the widgets register as if I'm just trying to scroll up on the ListView. Is there a physics type for the ListView to close a modal bottom sheet when at the top or a different way to set up an ignore pointer for this?
This is what I've come up with and I hope I explained myself well enough that this problem is understood.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ModalSheet extends StatefulWidget {
#override
_ModalSheetState createState() => _ModalSheetState();
}
ScrollController _scrollController = ScrollController();
bool close = false;
class _ModalSheetState extends State<ModalSheet> {
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels < 1) {
if (_scrollController.position.userScrollDirection ==
ScrollDirection.forward) {
setState(() {
close = true;
});
} else {
setState(() {
close = false;
});
}
} else {
setState(() {
close = false;
});
}
print(_scrollController.position.pixels);
print(_scrollController.position.userScrollDirection);
print(close);
// print(close);
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Flexible(
child: IgnorePointer(
ignoring: close,
child: ListView.builder(
controller: _scrollController,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(
title: Text('tile: ${index + 1}'),
);
},
),
),
),
],
),
);
}
}
EDIT: Thanks to #LearningJS888 for the package suggestion below to use this package! much appreciated
Can you elaborate what exactly you're trying to do?
A normal way of closing ModalBottomSheet is by Navigator.pop(context).

Flutter : PageController.page cannot be accessed before a PageView is built with it

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),
);
}
}

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();
}

how to access flutter bloc in the initState method?

In the code shown below , the dispatch event is called from within the build method after getting the BuildContext object. What if I wish to do is to dispatch an event during processing at the start of the page within the initState method itself ?
If I use didChangeDependencies method , then I am getting this error :
BlocProvider.of() called with a context that does not contain a Bloc of type FileManagerBloc. how to fix this?
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider<FileManagerBloc>(
builder: (context)=>FileManagerBloc(),
child: SafeArea(
child: Container(
child: Column(
children: <Widget>[
Container(color: Colors.blueGrey, child: TopMenuBar()),
Expanded(
child: BlocBuilder<FileManagerBloc,FileManagerState>(
builder: (context , state){
return GridView.count(
scrollDirection: Axis.vertical,
physics: ScrollPhysics(),
crossAxisCount: 3,
crossAxisSpacing: 10,
children: getFilesListWidget(context , state),
);
},
),
)
],
),
),
),
));
}
#override
void dispose() {
super.dispose();
}
#override
void didChangeDependencies() {
logger.i('Did change dependency Called');
final FileManagerBloc bloc = BlocProvider.of<FileManagerBloc>(context) ;
Messenger.sendGetHomeDir()
.then((path) async {
final files = await Messenger.sendListDir(path);
bloc.dispatch(SetCurrentWorkingDir(path)) ;
bloc.dispatch(UpdateFileSystemCacheMapping(path , files)) ;
});
}
The problem is that you are initializing the instance of FileManagerBloc inside the BlocProvider which is, of course inaccessible to the parent widget. I know that helps with automatic cleanup of the Bloc but if you want to access it inside initState or didChangeDependencies then you have to initialize it at the parent level like so,
FileManagerBloc _fileManagerBloc;
#override
void initState() {
super.initState();
_fileManagerBloc= FileManagerBloc();
_fileManagerBloc.dispatch(LoadEducation());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider<FileManagerBloc>(
builder: (context)=> _fileManagerBloc,
child: SafeArea(
child: Container(
child: Column(
children: <Widget>[
Container(color: Colors.blueGrey, child: TopMenuBar()),
Expanded(
child: BlocBuilder<FileManagerBloc,FileManagerState>(
builder: (context , state){
return GridView.count(
scrollDirection: Axis.vertical,
physics: ScrollPhysics(),
crossAxisCount: 3,
crossAxisSpacing: 10,
children: getFilesListWidget(context , state),
);
},
),
)
],
),
),
),
));
}
#override
void dispose() {
_fileManagerBloc.dispose();
super.dispose();
}
#override
void didChangeDependencies() {
logger.i('Did change dependency Called');
Messenger.sendGetHomeDir()
.then((path) async {
final files = await Messenger.sendListDir(path);
_fileManagerBloc.dispatch(SetCurrentWorkingDir(path)) ;
_fileManagerBloc.dispatch(UpdateFileSystemCacheMapping(path , files)) ;
});
}
alternatively, if FileManagerBloc was provided/initialized at a grandparent Widget then it could easily be accessible at this level through BlocProvider.of<CounterBloc>(context);
you can use it in didChangeDependencies method rather than initState.
Example
#override
void didChangeDependencies() {
final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
//do whatever you want with the bloc here.
super.didChangeDependencies();
}
Solution:
Step 1: need apply singleton pattern on Bloc class
class AuthBloc extends Bloc<AuthEvent, AuthState> {
static AuthBloc? _instance;
static AuthBloc get instance {
if (_instance == null) _instance = AuthBloc();
return _instance!;
}
....
....
Step 2: use AuthBloc.instance on main.dart for Provider
void main() async {
runApp(MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => AuthBloc.instance,
),
...
...
],
child: App(),
));
}
Now you can use Bloc without context
you can get state by AuthBloc.instance.state from initState or anywhere
you can add event from anywhere by AuthBloc.instance.add(..)
you also call BlocA from another BlocB very simple