I'm using this lib for displaying a camera (https://pub.dev/packages/camera), however, I'd like to set a predefined zoom value for the camera before it starts, but I'm not able to identify where I can set it using this lib.
Does anyone know how to do it?
Here's the code I got so far
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
class CameraPage extends StatefulWidget {
#override
_CameraPageState createState() => _CameraPageState();
}
class _CameraPageState extends State<CameraPage> {
List<CameraDescription> cameras;
CameraController controller;
#override
void initState() {
// TODO: implement initState
super.initState();
buscarCameras();
}
Future<void> buscarCameras() async {
cameras = await availableCameras();
controller = CameraController(cameras[0], ResolutionPreset.medium);
controller.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
#override
Widget build(BuildContext context) {
if(controller == null || controller.value == null)
return Center(
child: CircularProgressIndicator(),
);
if (!controller.value.isInitialized) {
return Container();
}
return AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: CameraPreview(controller));
}
}
Flutter has added zoom support as of Camera version 0.6.2. You can use cameraController.setZoomLevel(4.0); in your code to adjust the zoom level.
There are also other helpful functions, such as cameraControl.getMaxZoomLevel(); to find the limits for the zoom level.
There isn't any documentation for this feature at time of writing, as it is very recent, but you can look through the code for the camera controller to see the available methods.
Create your zoom variable in state class:
class _ShowCameraState extends State<ShowCamera> {
CameraController _cameraController;
Future<void> _initializeControllerFuture;
double zoom = 0.0;
}
And then in a widget, create slider:
#override
Widget build(BuildContext context) {
return OrientationBuilder(
builder: (context, orientation) => Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return CameraPreview(_cameraController);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Slider(
activeColor: Colors.red,
value: zoom,
onChanged: (value) {
print(value);
// as slider values are in decimals, we multiply it by 10 because camera
//take values above 1.0 and below 8.0
//e.g:
//value=0.19 //according to slider
//value=value*10=> 0.19*10
//Now the updated value is:
//value=1.9
value = value * 10;
if (value <= 8.0 && value >= 1.0) {
//Here we set the zoom level when we move slider pointer
_cameraController.setZoomLevel(value);
}
//and to set slider pointer position visually, we divided the value by 10
//to give slider its original value.
setState(() => zoom = value / 10);
},
),
For those who need to do it now: Wrap your CameraPreview widget inside a GestureDetector and add the below code block inside it (you must initialize your CameraController before this).
onScaleUpdate: (details) async {
var maxZoomLevel = await camController.getMaxZoomLevel();
// just calling it dragIntensity for now, you can call it whatever you like.
var dragIntensity = details.scale;
if (dragIntensity < 1) {
// 1 is the minimum zoom level required by the camController's method, hence setting 1 if the user zooms out (less than one is given to details when you zoom-out/pinch-in).
camController.setZoomLevel(1);
} else if (dragIntensity > 1 && dragIntensity < maxZoomLevel) {
// self-explanatory, that if the maxZoomLevel exceeds, you will get an error (greater than one is given to details when you zoom-in/pinch-out).
camController.setZoomLevel(dragIntensity);
} else {
// if it does exceed, you can provide the maxZoomLevel instead of dragIntensity (this block is executed whenever you zoom-in/pinch-out more than the max zoom level).
camController.setZoomLevel(maxZoomLevel);
}
},
Very simple logic and gets the job done right away, but you can optimize it further for better user experience if you need.
Related
I implemented the shared preferences package in my Flutter app, with a list widget as radio button, that only save the language preference and not the checkmark.
So when i close the Language screen and come back, the language checkmark goes the the default one even if the language, saved in shared preferences is French or Italian.
This is my Language screen:
class LanguagesScreen extends StatefulWidget {
const LanguagesScreen({Key? key}) : super(key: key);
#override
State<LanguagesScreen> createState() => _LanguagesScreenState();
}
class Item {
final String prefix;
final String? helper;
const Item({required this.prefix, this.helper});
}
var items = [
Item(prefix: 'English', helper: 'English',), //value: 'English'
Item(prefix: 'Français', helper: 'French'),
Item(prefix: 'Italiano', helper: 'Italian'),
];
class _LanguagesScreenState extends State<LanguagesScreen> {
var _selectedIndex = 0;
final _userPref = UserPreferences();
var _selecLangIndex;
int index = 0;
final List<String> entries = <String>['English', 'French', 'Italian'];*/
//init shared preferences
#override
void initState() {
super .initState();
_populateField();
}
void _populateField() async {
var prefSettings = await _userPref.getPrefSettings();
setState((){
_selecLangIndex = prefSettings.language;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(...
),
body: CupertinoPageScaffold(
child: Container(
child: SingleChildScrollView(
child: CupertinoFormSection.insetGrouped(
children: [
...List.generate(items.length, (index) => GestureDetector(
onTap: () async {
setState(() => _selectedIndex = index);
if (index == 0){
await context.setLocale(Locale('en','US'));
_selecIndex = Language.English;
}
else if (index == 1){
await context.setLocale(Locale('fr','FR'));
_selecIndex = Language.French;
}
child: buildCupertinoFormRow(
items[index].prefix,
items[index].helper,
selected: _selectedIndex == index,
)
)),
TextButton(onPressed:
_saveSettings,
child: Text('save',
)
buildCupertinoFormRow(String prefix, String? helper, {bool selected = false,}) {
return CupertinoFormRow(
prefix: Text(prefix),
helper: helper != null
? Text(helper, style: Theme.of(context).textTheme.bodySmall,)
:null, child: selected ? const Icon(CupertinoIcons.check_mark,
color: Colors.blue, size: 20,) :Container(),
);
}
void _saveSettings() {
final newSettings = PrefSettings(language:_selecIndex);
_userPref.saveSettings(newSettings);
Navigator.pop(context);
}
}
this is the UserPreference:
class UserPreferences {
Future saveSettings(PrefSettings prefSettings) async {
final preferences = await SharedPreferences.getInstance();
await preferences.setInt('language' , prefSettings.language.index );
}
Future<PrefSettings> getPrefSettings() async {
final preferences = await SharedPreferences.getInstance();
final language = Language.values[preferences.getInt('language') ?? 0 ];
return PrefSettings(language: language);
}
}
enum Language { English, French, Italian}
class PrefSettings{
final Language language;
PrefSettings (
{required this.language});
}
I'm betting that the issue is in initState. You are calling _populateField, but it doesn't complete before building because it's an async method, and you can't await for it: so the widget gets build, loading the default position for the checkmark, and only after that _populateField completes...but then it's too late to show the saved data correctly.
In my experience, if I have not already instantiated a SharedPreferences object somewhere else in the code, I use this to load it:
class _LanguagesScreenState extends State<LanguagesScreen> {
[...]
#override
Widget build(BuildContext context) {
return FutureBuilder(
//you can put any async method here, just be
//sure that you use the type it returns later when using 'snapshot.data as T'
future: await SharedPreferences.getInstance(),
builder: (context, snapshot) {
//error handling
if (!snapshot.hasData || snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
var prefs= snapshot.data as SharedPreferences;
//now you have all the preferences available without need to await for them
return Scaffold((
[...]
);
EDIT
I started writing another comment, but there are so many options here that there wasn't enough space.
First, the code I posted should go in your _LanguagesScreenState build method. The FutureBuilder I suggested should wrap anything that depends on the Future you must wait for to complete. I put it up at the root, above Scaffold, but you can move it down the widgets' tree as you need, just remember that everything that needs to read the preferences has to be inside the FutureBuilder.
Second, regarding SharedPreferences.getInstance(), there are two ways: the first is declaring it as a global variable, and loading it even in the main method where everything starts. By doing this you'll be able to reference it from anywhere in your code, just be careful to save the changes everytime is needed. The second is to load it everytime you need, but you'll end up using a FutureBuilder a lot. I don't know if any of these two options is better than the other: the first might have problems if somehow the SharedPreferences object gets lost, while the second requires quite more code to work.
I am using the Fortune Wheel in this link. When I fling the wheel, it will spin and end but when it is done, I'm unable to fetch the value of the selection.
Here's what I have tried:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_fortune_wheel/flutter_fortune_wheel.dart';
class SpinawheelWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => SpinawheelState();
}
class SpinawheelState extends State<SpinawheelWidget> {
StreamController<int> selected = StreamController<int>();
#override
void initState() {
super.initState();
}
#override
void dispose() {
selected.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
final items = <String>[
'item1',
'item2',
'item3',
'item4',
'item5',
'item6',
];
return Column(
children: [
Expanded(
child: FortuneWheel(
physics: CircularPanPhysics(
duration: Duration(seconds: 1),
curve: Curves.decelerate,
),
onFling: () {
print('onFling');
selected.add(1);
},
onAnimationStart: () {
print('animation start');
},
onAnimationEnd: () {
print('animation end ${selected.stream}');
},
animateFirst: false,
selected: selected.stream,
items: [
for (var it in items) FortuneItem(child: Text(it)),
],
),
),
],
);
}
}
The print on onAnimationEnd only shows: animation end Instance of '_ControllerStream' but not the value. I am expect to get at least one of the item or the position of the item. Please help. Thanks!
Got it to work. Just need some changes.
Change your declaration of stream controller to broadcast
StreamController<int> _controller = StreamController.broadcast();
Then the trick. In some place of your widget tree (mine was just above the FortuneWheel in a Column) insert this StreamBuilder. This will show nothing, just handle some functions. In order to let us handle the snapshot result, we wil add the _text funciontion returning a widget. See bellow:
StreamBuilder(
stream: _controller.stream,
builder: (context, snapshot) => snapshot.hasData
? _text(snapshot)
: Container(),
)
And now, the function returning the widget _text.
Widget _text(var snapshot) {
//here you cand get and handle the result and do whathever.
print(snapshot.data);
int val = snapshot.data;
print(items[val]);
return Text(snapshot.data.toString()); //you dont need to return //anything here. Just replace the Text widget with SizedBox() to return //nothing.
}
As, the library is using Stream you will need to use stream method to get data from selection.One such method is called listen()
Rest you can check this : Medium Link - Streams In Flutter
var rendomval = Fortune.randomInt(0, items.length); setState(() { selected.add(rendomval); }); print(rendomval);
Declare the stream controller like this
StreamController<int> _controller = StreamController.broadcast();
then call the listen whatever you want , suggesting use listener inside initState function
_controller.stream.listen((value) {
print('Value from controller: $value');
})
I'm Building An Flutter Application which requires image changes after a period of time. I thought using while loop with a sleep method inside may solve the problem. But It didn't, Image is only getting change after the loop ends. Application UI also gets froze.
So, I used the async Task which I can't control with a Button.
Desired Output: Image should be changed after every 10 seconds and the user can pause or resume method execution.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Test(
),
),
)
);
}}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int imgnumber=1;
int varToCheckButtonPress = 0;
String BtnTxt = "START";
void inc(){
while(imgnumber<10)
{
print(imgnumber);
await Future.delayed(const Duration(seconds: 10));
setState(() {
imgnumber++;
});
}
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(flex: 1,
child: Container(
child: Image.asset('images/'+imgnumber.toString()+'.png'),
height: 500,
width:500,
color: Colors.green,
),
),
FlatButton(
child: Text(BtnTxt),
onPressed: (){
if (varToCheckButtonPress == 0) {
setState(() {
inc();
BtnTxt = 'PAUSE';
varToCheckButtonPress = 1;
});
} else if (varToCheckButtonPress == 1) {
setState(() {
BtnTxt = 'RESUME';
varToCheckButtonPress = 0;
});
}
},
)
],
);
}
}
I want the user to control the UI with a single button behave as START, PAUSE and RESUME.
Can we Use normal function To implement this functionality?
You should make use of Bloc pattern to manage your states, e.g: StreamBuilder, Providers, and make a timer to push new imageUrl to the sink and let the streamBuilder receive the latest imageUrl.
As for your button, all it controls is the timer. When u hit the play button, new imageUrl will keep pushing to the sink, while you press paused, simply stop the timer, and new image Url will not be pushing new imageUrl to the sink, and of course, reset the timer when you hit the stop button.
Here is a very detail Bloc pattern tutorial you can follow: Medium
The shortcut to achieve this is :
You can probably hold a function in async loop and call setState method on tap to change it's state.
For example :
call this function in desired location
while (_isPaused) {
await Future.delayed(Duration(milliseconds: 500));
}
and then call set state method from onTap, just like this
onTap:(){
setState((){
_isPaused? _isPaused=false: _isPaused=true;
});
}
I am coding a chess game in flutter.
and this is the relevant bits of my code :
class Rank extends StatelessWidget {
final _number;
Rank(this._number);
#override
Widget build(BuildContext context) {
var widgets = <Widget>[];
for (var j = 'a'.codeUnitAt(0); j <= 'h'.codeUnitAt(0); j++) {
widgets
.add(
DroppableBoardSquare(String.fromCharCode(j) + this._number.toString())
);
//
}
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgets);
}
}
class DroppableBoardSquare extends StatelessWidget {
final String _coordinate;
const DroppableBoardSquare(this._coordinate) ;
#override
Widget build(BuildContext context) {
return DragTarget(
builder:(BuildContext context, List candidate, List rejectedData){
return BoardSquare(_coordinate);
},
onAccept: (data ) {
print('Accepted');
},
onWillAccept: (data){
return true;
},
onLeave: (data) => print("leave"),);
}
}
class BoardSquare extends StatelessWidget {
final String _coordinate;
BoardSquare(this._coordinate);
#override
Widget build(BuildContext context) {
ChessBloc bloc = ChessBlocProvider.of(context);
return
StreamBuilder<chess.Chess>(
stream: bloc.chessState,
builder: (context, AsyncSnapshot<chess.Chess> chess) {
return DraggablePieceWidget(chess.data.get(_coordinate), _coordinate);
});
}
}
class DraggablePieceWidget extends StatelessWidget {
final chess.Piece _piece;
final String _coordinate;
DraggablePieceWidget(this._piece, String this._coordinate);
#override
Widget build(BuildContext context) {
return Draggable(
child: PieceWidget(_piece),
feedback: PieceWidget(_piece),
childWhenDragging: PieceWidget(null),
data: {"piece": _piece, "origin": _coordinate} ,
);
}
}
Now the problem is that I can drag the piece fine, but cannot drop them. None of the methods on DragTarget is getting called.
what I am doing wrong?
I developed a drag-n-drop photos grid, where you can drag photos to reorder them based on numeric indexes.
Essentially, I assume, it is the same thing as the chessboard concept you have.
The problem possibly occurs due to Draggable (DraggablePieceWidget) element being inside of DragTarget (DroppableBoardSquare).
In my app I made it the other way around - I placed DragTarget into Draggable.
Providing some pseudo-code as an example:
int _dragSelectedIndex;
int _draggingIndex;
// Basically this is what you'd use to build every chess item
Draggable(
maxSimultaneousDrags: 1,
data: index,
onDragStarted: () { _draggingIndex = index; print("Debug: drag started"); }, // Use setState for _draggingIndex, _dragSelectedIndex.
onDragEnd: (details) { onDragEnded(); _draggingIndex = null; print("Debug: drag ended; $details"); },
onDraggableCanceled: (_, __) { onDragEnded(); _draggingIndex = null; print("Debug: drag cancelled."); },
feedback: Material(type: MaterialType.transparency, child: Opacity(opacity: 0.85, child: Transform.scale(scale: 1.1, child: createDraggableBlock(index, includeTarget: false)))),
child: createDraggableBlock(index, includeTarget: true),
);
// This func is used in 2 places - Draggable's `child` & `feedback` props.
// Creating dynamic widgets through functions is a bad practice, switch to StatefulWidget if you'd like.
Widget createDraggableBlock(int index, { bool includeTarget = true }) {
if (includeTarget) {
return DragTarget(builder: (context, candidateData, rejectedData) {
if (_draggingIndex == index || candidateData.length > 0) {
return Container(); // Display empty widget in the originally selected cell, and in any cell that we drag the chess over.
}
// Display a chess, but wrapped in DragTarget widget. All chessboard cells will be displayed this way, except for the one you start dragging.
return ChessPiece(..., index: index);
}, onWillAccept: (int elemIndex) {
if (index == _draggingIndex) {
return false; // Do not accept the chess being dragged into it's own widget
}
setState(() { _dragSelectedIndex = index; });
return true;
}, onLeave: (int elemIndex) {
setState(() { _dragSelectedIndex = null; });
});
}
// Display a chess without DragTarget wrapper, e.g. for the draggable(feedback) widget
return ChessPiece(..., index: index);
}
onDragEnded() {
// Check whether _draggingIndex & _dragSelectedIndex are not null and act accordingly.
}
I assume if you change index system to custom objects that you have - this would work for you too.
Please let me know if this helped.
I'm trying to implement a infinite scroll functionality.
I tried using a ListView inside on a NotificationListener to detect scroll events, but I can't see an event that says if the scroll has reached the bottom of the view.
Which would be the best way to achieve this?
There are generally two ways of doing it.
1. Using ScrollController
// Create a variable
final _controller = ScrollController();
#override
void initState() {
super.initState();
// Setup the listener.
_controller.addListener(() {
if (_controller.position.atEdge) {
bool isTop = _controller.position.pixels == 0;
if (isTop) {
print('At the top');
} else {
print('At the bottom');
}
}
});
}
Usage:
ListView(controller: _controller) // Assign the controller.
2. Using NotificationListener
NotificationListener<ScrollEndNotification>(
onNotification: (scrollEnd) {
final metrics = scrollEnd.metrics;
if (metrics.atEdge) {
bool isTop = metrics.pixels == 0;
if (isTop) {
print('At the top');
} else {
print('At the bottom');
}
}
return true;
},
child: ListView.builder(
physics: ClampingScrollPhysics(),
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
itemCount: 20,
),
)
You can use a ListView.builder to create a scrolling list with unlimited items. Your itemBuilder will be called as needed when new cells are revealed.
If you want to be notified about scroll events so you can load more data off the network, you can pass a controller argument and use addListener to attach a listener to the ScrollController. The position of the ScrollController can be used to determine whether the scrolling is close to the bottom.
_scrollController = new ScrollController();
_scrollController.addListener(
() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = 200.0; // or something else..
if ( maxScroll - currentScroll <= delta) { // whatever you determine here
//.. load more
}
}
);
Collin's should be accepted answer....
I would like to add example for answer provided by collin jackson. Refer following snippet
var _scrollController = ScrollController();
_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
// Perform your task
}
});
This will be only triggered when last item is visible in the list.
A more simpler aproach is like this:
NotificationListener<ScrollEndNotification>(
onNotification: onNotification,
child: <a ListView or Wrap or whatever widget you need>
)
and create a method to detect the position:
bool onNotification(ScrollEndNotification t) {
if (t.metrics.pixels >0 && t.metrics.atEdge) {
log('I am at the end');
} else {
log('I am at the start')
}
return true;
}
t.metrics.pixel is 0 when the user is with the scrol at the top, as is more then 0 when the sure scrools.
t.metrics.atEdge is true when the user is either at the top with the scrol or at the end with the scrol
the log method is from package import 'dart:developer';
I feel like this answer is a complement to Esteban's one (with extension methods and a throttle), but it's a valid answer too, so here it is:
Dart recently (not sure) got a nice feature, method extensions, which allow us to write the onBottomReach method like a part of the ScrollController:
import 'dart:async';
import 'package:flutter/material.dart';
extension BottomReachExtension on ScrollController {
void onBottomReach(VoidCallback callback,
{double sensitivity = 200.0, Duration throttleDuration}) {
final duration = throttleDuration ?? Duration(milliseconds: 200);
Timer timer;
addListener(() {
if (timer != null) {
return;
}
// I used the timer to destroy the timer
timer = Timer(duration, () => timer = null);
// see Esteban Díaz answer
final maxScroll = position.maxScrollExtent;
final currentScroll = position.pixels;
if (maxScroll - currentScroll <= sensitivity) {
callback();
}
});
}
}
Here's a usage example:
// if you're declaring the extension in another file, don't forget to import it here.
class Screen extends StatefulWidget {
Screen({Key key}) : super(key: key);
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
ScrollController_scrollController;
#override
void initState() {
super.initState();
_scrollController = ScrollController()
..onBottomReach(() {
// your code goes here
}, sensitivity: 200.0, throttleDuration: Duration(milliseconds: 500));
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Note: if you're using method extensions, you need to configure some things, see "How to enable Dart Extension Methods"
final ScrollController controller = ScrollController();
void _listener() {
double maxPosition = controller.position.maxScrollExtent;
double currentPosition = controller.position.pixels;
/// You can change this value . It's a default value for the
/// test if the difference between the great value and the current value is smaller
/// or equal
double difference = 10.0;
/// bottom position
if ( maxPosition - currentPosition <= difference )
/// top position
else
if(mounted)
setState(() {});
}
#override
void initState() {
super.initState();
controller.addListener(_listener);
}
I used different approach for infinite scrolling. I used ChangeNotifier class for variable change listener.
If there is change in variable It triggers the event and eventually hit the API.
class DashboardAPINotifier extends ChangeNotifier {
bool _isLoading = false;
get getIsLoading => _isLoading;
set setLoading(bool isLoading) => _isLoading = isLoading;
}
Initialize DashboardAPINotifier class.
#override
void initState() {
super.initState();
_dashboardAPINotifier = DashboardAPINotifier();
_hitDashboardAPI(); // init state
_dashboardAPINotifier.addListener(() {
if (_dashboardAPINotifier.getIsLoading) {
print("loading is true");
widget._page++; // For API page
_hitDashboardAPI(); //Hit API
} else {
print("loading is false");
}
});
}
Now the best part is when you have to hit the API.
If you are using SliverList, Then at what point you have to hit the API.
SliverList(delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
Widget listTile = Container();
if (index == widget._propertyList.length - 1 &&
widget._propertyList.length <widget._totalItemCount) {
listTile = _reachedEnd();
} else {
listTile = getItem(widget._propertyList[index]);
}
return listTile;
},
childCount: (widget._propertyList != null)? widget._propertyList.length: 0,
addRepaintBoundaries: true,
addAutomaticKeepAlives: true,
),
)
_reachEnd() method take care to hit the api. It trigger the `_dashboardAPINotifier._loading`
// Function that initiates a refresh and returns a CircularProgressIndicator - Call when list reaches its end
Widget _reachedEnd() {
if (widget._propertyList.length < widget._totalItemCount) {
_dashboardAPINotifier.setLoading = true;
_dashboardAPINotifier.notifyListeners();
return const Padding(
padding: const EdgeInsets.all(20.0),
child: const Center(
child: const CircularProgressIndicator(),
),
);
} else {
_dashboardAPINotifier.setLoading = false;
_dashboardAPINotifier.notifyListeners();
print("No more data found");
Utils.getInstance().showSnackBar(_globalKey, "No more data found");
}
}
Note: After your API response you need to notify the listener,
setState(() {
_dashboardAPINotifier.setLoading = false;
_dashboardAPINotifier.notifyListeners();
}
You can use the package scroll_edge_listener.
It comes with an offset and debounce time configuration which is quite useful. Wrap your scroll view with a ScrollEdgeListener and attach a listener. That's it.
ScrollEdgeListener(
edge: ScrollEdge.end,
edgeOffset: 400,
continuous: false,
debounce: const Duration(milliseconds: 500),
dispatch: true,
listener: () {
debugPrint('listener called');
},
child: ListView(
children: const [
Placeholder(),
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),
You can use any one of below conditions :
NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrices = notification.metrics;
if (metrices.atEdge && metrices.pixels == 0) {
//you are at top of list
}
if (metrices.pixels == metrices.minScrollExtent) {
//you are at top of list
}
if (metrices.atEdge && metrices.pixels > 0) {
//you are at end of list
}
if (metrices.pixels >= metrices.maxScrollExtent) {
//you are at end of list
}
return false;
},
child: ListView.builder());