Flutter GoogleMaps onCameraMove and Future JSON - flutter

So i am using the googlemaps package and when a user zooms in/out i need to show/hide various markers based on distance. the markers data comes from a json future.
I can see it zoom in/out, but i cannot get it to loop through my amenities future. i know i am probably using the wrong thing(FutureBuilder) what should i be using instead? thanks
class _AlreadyHereState extends State<AlreadyHere>
with SingleTickerProviderStateMixin {
late Future<Amenities> amenities;
#override
void initState() {
super.initState();
amenities = AmenitiesData.getAmenities();
}
onCameraMove(CameraPosition position) {
double distance;
if (position.zoom >= 10.1) {
// Create Markers From Amenities
print("Add Marker - $position");
FutureBuilder(
future: amenities,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
snapshot.data!.amenities.forEach((el) {
distance = calculateDistance(position.target.latitude,
position.target.latitude, el.latitude, el.longitude);
if (distance <= 5) {
//Create Marker
} else {
// if marker exists remove marker
}
});
}
throw ('error');
});
} else if (position.zoom <= 10.0) {
print("Remove Marker - $position");
//Remove Markers
}
}

Related

Updating restaurant locations when map position moves

This code successfully shows nearby restaurants using Google Places API based on my current location. When I manually move the map position on my phone, though, new restaurant markers do not show in the new location of the map. Below is my code. I also have commented out something I was trying to do to update the markers to the new camera position. I used onCameraMove in the Google Maps. Any help with updating new restaurant markers based on the new camera position is greatly appreciated.
import 'package:family_farms_forever/utilities/secrets.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_webservice/places.dart';
import 'package:google_maps_flutter_platform_interface/src/types/marker_updates.dart';
void main() => runApp(const MyApp());
final places = GoogleMapsPlaces(apiKey: Secrets.iosApiKey);
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Market Map",
home: Scaffold(
// We'll change the AppBar title later
appBar: AppBar(title: const Text("My Location")),
body: const MyMarketMap()),
);
}
}
class MyMarketMap extends StatefulWidget {
const MyMarketMap({super.key});
#override
State<StatefulWidget> createState() {
return _MyMarketMapState();
}
}
class _MyMarketMapState extends State<MyMarketMap> {
late Future<Position> _currentLocation;
late final Set<Marker> _markers = {};
#override
void initState() {
super.initState();
_currentLocation = Geolocator.getCurrentPosition();
}
Future<void> _retrieveNearbyRestaurants(LatLng userLocation) async {
PlacesSearchResponse response = await places.searchNearbyWithRadius(
Location(lat: userLocation.latitude, lng: userLocation.longitude),
10000,
type: "restaurant");
Set<Marker> restaurantMarkers = response.results
.map((result) => Marker(
markerId: MarkerId(result.name),
// Use an icon with different colors to differentiate between current location
// and the restaurants
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueAzure),
infoWindow: InfoWindow(
title: result.name,
snippet:
"Ratings: ${result.rating?.toString() ?? "Not Rated"}"),
position: LatLng(
result.geometry!.location.lat, result.geometry!.location.lng)))
.toSet();
setState(() {
_markers.addAll(restaurantMarkers);
});
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _currentLocation,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
// The user location returned from the snapshot
Position snapshotData = snapshot.data;
LatLng userLocation =
LatLng(snapshotData.latitude, snapshotData.longitude);
if (_markers.isEmpty) {
_retrieveNearbyRestaurants(userLocation);
}
// void upDateMarkers() {
// Set<Marker> updatedMarkers =
// {}; //new markers with updated position go here
// updatedMarkers = {};
// /// Then call the SetState function.
// /// I called the MarkersUpdate class inside the setState function.
// /// You can do it your way but remember to call the setState function so that the updated markers reflect on your Flutter app.
// /// Ps: I did not try the second way where the MarkerUpdate is called outside the setState buttechnically it should work.
// setState(() {
// MarkerUpdates.from(Set<Marker>.from(_markers),
// Set<Marker>.from(updatedMarkers));
// _markers = updatedMarkers;
// _markers.addAll(updatedMarkers);
// //swap of markers so that on next marker update the previous marker would be the one which you updated now.
// // And even on the next app startup, it takes the updated markers to show on the map.
// });
// }
return GoogleMap(
myLocationEnabled: true,
zoomGesturesEnabled: true,
//onCameraMove: (position) => upDateMarkers(),
initialCameraPosition: CameraPosition(
target: userLocation,
zoom: 12,
),
markers: _markers
..add(Marker(
markerId: const MarkerId("User Location"),
infoWindow: const InfoWindow(title: "User Location"),
position: userLocation)),
);
} else {
return const Center(child: Text("Failed to get user location."));
}
}
// While the connection is not in the done state yet
return const Center(child: CircularProgressIndicator());
});
}
}
You have to use Stream for getting updates :
StreamBuilder<Position>(
stream: Geolocator.getPositionStream(
locationSettings: LocationSettings()
),
builder: (context, snapshot) {
... Do your Rest Coding Here...
You can control the behavior of the stream by specifying an instance of the [LocationSettings] class for the [locationSettings] parameter. Standard settings are:
LocationSettings.accuracy: allows controlling the precision of the position updates by supplying (defaults to "best");
LocationSettings.distanceFilter: allows controlling the minimum distance the device needs to move before the update is emitted (default value is 0 which indicates no filter is used);
LocationSettings.timeLimit: allows for setting a timeout interval. If between fetching locations the timeout interval is exceeded a [TimeoutException] will be thrown. By default no time limit is configured.

Is there any easy way to use a Future (which performs an http connection) inside a stateful widget without having it reconnect on every screen build?

Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies.
It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.

Data not updating to another widget flutter

I am trying to show the price of items in the cart but the total value should be shown in TextField. I am saving data to SQLite and retrieving then show to a widget, but when I try to access total_price to another widget it's not updating, but When I press hot reload again the data shows but not first time when I am opening the page
return FutureBuilder<List<CartModel>>(
future: fetchCartFromDatabase(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.length > 0) {
cartCount = snapshot.data.length;
for(int i = 0;i<snapshot.data.length;i++){
var price = snapshot.data[i].product_price.split("₹");
total_price+=double.parse(price[1]);
}
} else if (snapshot.hasData && snapshot.data.length == 0) {
return new Text("No Data found");
}
else
{
return new Container(
alignment: AlignmentDirectional.center,
child: new CircularProgressIndicator(),
);
}
);
value initialized
int cartCount = 0;
double total_price=0.0;
The FutureBuilder updates only its children. To update the value of another widget you must use setState.
The best way would be putting FutureBuilder in an upper level or using some sort of state manager, like provider.
To use setState you need to initialize you fetch from an initState of a stetefullWidget (or to call it from a function). This way you will not need a FutureBuilder and must refactor your code:
class YourWidget extends StatefulWidget {
#override
_YourWidgetState createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
double total_price = 0;
#override
void initState() {
super.initState();
fetchCartFromDatabase().then((value){
setState((){
for(int i = 0;i<value.length;i++){
var price = value[i].product_price.split("₹");
total_price+=double.parse(price[1]);
}
});
});
}
}
The addPostFrameCallback is not a good solution, since it updates the value only in the next frame. When the app grows it leads to lags.
To continue using the FutureBuilder, move your widget tree that needs to be updated to be inside of the FutureBuilder.

Zoom camera in Flutter

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.

How to check if scroll position is at top or bottom in ListView?

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