Flutter web - Geolocator not working when uploaded to server - flutter

everyone.
I'm trying to develop a PWA with flutter 2.2.1 that shows a map using Mapbox_gl and displays the user current location using Geolocator.
So far everything works as expected while debuging the app, but when I run:
flutter build
or
flutter build --release
and then run
firebase deploy
the site gets uploaded, the map shows as intended and it asks for permissions but the user's location is never shown and Google Chrome's Console throws this error:
Uncaught TypeError: m.gfR is not a function
at Object.avh (main.dart.js:20405)
at main.dart.js:65755
at aiD.a (main.dart.js:5853)
at aiD.$2 (main.dart.js:34394)
at ahm.$1 (main.dart.js:34386)
at Rx.o1 (main.dart.js:35356)
at adi.$0 (main.dart.js:34770)
at Object.tQ (main.dart.js:5975)
at a5.mn (main.dart.js:34687)
at ada.$0 (main.dart.js:34731)
Here's the code I'm using on flutter:
mapbox.dart
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:geolocator/geolocator.dart';
import 'package:kkc/main.dart';
import 'package:mapbox_gl/mapbox_gl.dart';
import 'package:kkc/services/location_service.dart';
class Mapbox extends StatefulWidget {
const Mapbox();
#override
State createState() => MapboxState();
}
class MapboxState extends State<Mapbox> {
final Random _rnd = new Random();
Position? _currentLocation;
LatLng _currentCoordinates = new LatLng(0,0);
final List<_PositionItem> _positionItems = <_PositionItem>[];
StreamSubscription<Position>? _positionStreamSubscription;
late MapboxMapController _mapController;
List<Marker> _markers = [];
List<_MarkerState> _markerStates = [];
CameraPosition _kInitialPosition = CameraPosition(
target: LatLng(19.4274418, -99.1682147),
zoom: 18.0,
tilt: 70,
);
void _addMarkerStates(_MarkerState markerState) {
_markerStates.add(markerState);
}
void _onMapCreated(MapboxMapController controller) {
_mapController = controller;
controller.addListener(() {
if (controller.isCameraMoving) {
_updateMarkerPosition();
}
});
}
void _onStyleLoadedCallback() {
_updateMarkerPosition();
}
void _onCameraIdleCallback() {
_updateMarkerPosition();
}
void _updateMarkerPosition() {
final coordinates = <LatLng>[];
for (final markerState in _markerStates) {
coordinates.add(markerState.getCoordinate());
}
_mapController.toScreenLocationBatch(coordinates).then((points) {
_markerStates.asMap().forEach((i, value) {
_markerStates[i].updatePosition(points[i]);
});
});
}
void _addMarker(Point<double> point, LatLng coordinates) {
setState(() {
_markers.add(Marker(_rnd.nextInt(100000).toString(), coordinates, point, _addMarkerStates));
});
}
#override
void initState() {
super.initState();
_getCurrentLocation();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: Stack(children: [
MapboxMap(
accessToken: Kukulcan.MAPBOX_ACCESS_TOKEN,
trackCameraPosition: true,
onMapCreated: _onMapCreated,
onCameraIdle: _onCameraIdleCallback,
onStyleLoadedCallback: _onStyleLoadedCallback,
initialCameraPosition: _kInitialPosition,
),
IgnorePointer(
ignoring: true,
child: Stack(
children: _markers,
))
]),
);
}
void _getCurrentLocation() async {
_currentLocation = await LocationService.startLocationService();
_currentCoordinates = new LatLng(_currentLocation!.latitude,_currentLocation!.longitude);
await _mapController.animateCamera(CameraUpdate.newLatLng(_currentCoordinates));
_addMarker(new Point(1, 1), _currentCoordinates);
if (_positionStreamSubscription == null) {
final positionStream = Geolocator.getPositionStream();
_positionStreamSubscription = positionStream.handleError((error) {
_positionStreamSubscription?.cancel();
_positionStreamSubscription = null;
}).listen((position) => setState(() => _positionItems.add(
_PositionItem(_PositionItemType.position, position.toString()))));
_positionStreamSubscription?.pause();
}
}
}
class Marker extends StatefulWidget {
final Point _initialPosition;
LatLng _coordinate;
final void Function(_MarkerState) _addMarkerState;
Marker(
String key, this._coordinate, this._initialPosition, this._addMarkerState)
: super(key: Key(key));
#override
State<StatefulWidget> createState() {
final state = _MarkerState(_initialPosition);
_addMarkerState(state);
return state;
}
}
class _MarkerState extends State with TickerProviderStateMixin {
final _iconSize = 80.0;
Point _position;
_MarkerState(this._position);
#override
Widget build(BuildContext context) {
var ratio = 1.0;
//web does not support Platform._operatingSystem
if (!kIsWeb) {
// iOS returns logical pixel while Android returns screen pixel
ratio = Platform.isIOS ? 1.0 : MediaQuery.of(context).devicePixelRatio;
}
return Positioned(
left: _position.x / ratio - _iconSize / 2,
top: _position.y / ratio - _iconSize / 2,
child: Image.asset('assets/img/pin.png', height: _iconSize));
}
void updatePosition(Point<num> point) {
setState(() {
_position = point;
});
}
LatLng getCoordinate() {
return (widget as Marker)._coordinate;
}
}
enum _PositionItemType {
permission,
position,
}
class _PositionItem {
_PositionItem(this.type, this.displayValue);
final _PositionItemType type;
final String displayValue;
}
Does anyone have an idea on what's the problem?
Cheers!

Anyway the solution i found is to use --no-sound-null-safety argument as stated by geolocat documentation
I quote:
NOTE: due to a bug in the dart:html library the web version of the Geolocator plugin does not work with sound null safety enabled and compiled in release mode. Running the App in release mode with sound null safety enabled results in a Uncaught TypeError (see issue #693). The current workaround would be to build your App with sound null safety disabled in release mode:

Related

Custom Google Maps Marker Flutter

Im having trouble using a custom marker in my GoogleMaps project with flutter. I get this error when the map screen loads
Exception has occurred.
LateError (LateInitializationError: Field 'myMarker' has not been initialized.)
I tried without using late and it says myMarker has to be initialized so I declared it as late and then initialized it in the initState. That didn't work so I tried with a nullable ? and that did not work either. Any help would be appreciated.
Thanks
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:location/location.dart';
import 'dart:math' as math;
import './main.dart' as main;
import './variables.dart' as variables;
import './methods.dart' as methods;
import './mapvariables.dart' as mapVar;
import './marker_information.dart' as markerInfo;
class MapScreen extends StatefulWidget {
const MapScreen({Key? key}) : super(key: key);
#override
_MapScreenState createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
Completer<GoogleMapController> _controllerGoogleMap = Completer();
late GoogleMapController newGoogleMapController;
Position? currentPosition;
var geoLocator = Geolocator();
final double dcheck = 0.00014128694207108202;
var location = new Location();
late BitmapDescriptor myMarker;
#override
void initState() {
super.initState();
setMarker();
}
void setMarker() async {
myMarker = await BitmapDescriptor.fromAssetImage(
ImageConfiguration(), 'loginlogo.png');
}
checkpermission_location() async {
var locationStatus = await Permission.location.status;
print(locationStatus);
if (!locationStatus.isGranted) {
print("gr");
print(await Permission.location.value);
await Permission.location.request();
checkpermission_location();
}
if (!locationStatus.isDenied) {
print('de');
await Permission.location.request();
checkLocation();
}
}
void checkClue(var x, var y, markerInfo.ClueLocation marker) {
double distance = methods.distance(marker.lat, marker.long, x, y);
log("distance: $distance");
if ((distance < dcheck)) {
variables.dialogVis = true;
if ((variables.dialogVis) && (marker.compl == false)) {
mapVar.showAlertDialog(context, marker);
variables.dialogVis = false;
marker.compl = true;
}
}
}
void checkLocation() {
location.onLocationChanged.listen((LocationData currentLocation) {
var lat = currentLocation.latitude;
var long = currentLocation.longitude;
checkClue(lat, long, markerInfo.newHamCollege);
checkClue(lat, long, markerInfo.coeFen);
checkClue(lat, long, markerInfo.mathematicalBridge);
checkClue(lat, long, markerInfo.graveYard);
checkClue(lat, long, markerInfo.archeologicalMuseum);
checkClue(lat, long, markerInfo.addenbrokesHospital);
checkClue(lat, long, markerInfo.stMarysBellTower);
checkClue(lat, long, markerInfo.trinityStreet);
checkClue(lat, long, markerInfo.viewOfTheBridgeOfSighs);
});
}
//Initial camera position when maps first load
static const _initalCameraPosition = CameraPosition(
target: LatLng(52.2053, 0.1218),
zoom: 11.5,
);
Marker makeMarker(markerInfo.ClueLocation marker) {
return (Marker(
markerId: MarkerId(marker.title),
infoWindow: InfoWindow(title: marker.title),
icon: myMarker,
position: LatLng(marker.lat, marker.long),
onTap: () {
if (marker.compl) {
mapVar.showAlertDialog(context, marker);
}
}));
}
//Google map widget
#override
Widget build(BuildContext context) {
//Checks if mapAcess is true
if (variables.mapAccess) {
var currentlocation = location.getLocation();
return Scaffold(
body: GoogleMap(
onMapCreated: (GoogleMapController controller) {
controller.setMapStyle(mapVar.mapStyle);
checkpermission_location();
_controllerGoogleMap.complete(controller);
newGoogleMapController = controller;
},
mapType: MapType.normal,
myLocationButtonEnabled: true,
zoomControlsEnabled: true,
myLocationEnabled: true,
zoomGesturesEnabled: true,
markers: {
//Markers located in the variables.dart file
makeMarker(markerInfo.newHamCollege),
makeMarker(markerInfo.coeFen),
makeMarker(markerInfo.mathematicalBridge),
makeMarker(markerInfo.graveYard),
makeMarker(markerInfo.archeologicalMuseum),
//6.??? Waiting for update from Konstantin
makeMarker(markerInfo.addenbrokesHospital),
makeMarker(markerInfo.stMarysBellTower),
makeMarker(markerInfo.trinityStreet),
makeMarker(markerInfo.viewOfTheBridgeOfSighs),
},
initialCameraPosition: _initalCameraPosition,
),
);
}
//Refuses access if 10 Digit key is not provided
return Scaffold(
body: Center(
child: Text('You do not have access to the map, please login')));
}
}
You pretty much have it the same way I do it; the way I've done it is as follows:
Set up the BitmapDescriptor as a nullable property at the top of your state class, as well as I create a markers Set:
Set<Marker>? _markers = <Marker>{};
BitmapDescriptor? myMarker
I create an async method that I run at the beginning of the build method (I do it in the build method because I need the build context sometimes), that asynchronously loads the bitmaps, as in:
void setMarkerIcon() async {
myMarker = await BitmapDescriptor.fromAssetImage(
const ImageConfiguration(size: Size(50, 50)),'loginlogo.png');
}
Then in the build method I just call it:
#override
Widget build(BuildContext context) {
// you can call it here
setMarkerIcons();
return Scaffold(
body: GoogleMap(
markers: _markers!,
onMapCreated: (GoogleMapController ctrl) {
// here after map is loaded, I generate the markers
generateMarkers();
}
)
);
BONUS:
Then as shown above, upon the map getting created, I can go ahead and use the custom markers, based on a list of locations, as such:
void generateMarkers() {
var localMarkers = <Marker>{};
for(var location in locationsList!) {
localMarkers.add(
Marker(
markerId: MarkerId(location.id!),
position: LatLng(location.lat!, location.lng!),
icon: myMarker
)
);
}
if (mounted) {
setState(() {
_markers = localMarkers;
});
}
}

Flutter Camera for Android will throw exception after some time

I am currently building this camera screen widget that will navigate to a different screen when a driver license barcode is detected.
My app flow is like this:
Screen 1 Widget -> Camera Screen Widget -> Screen 2 Widget (Screen 2 widget has a button to go back to Screen 1 Widget).
The flow will work fine close to 10 times but then it will always crash on the Camera Screen Widget.
Here's the exception being thrown:
I/Camera (24242): closeCaptureSession
E/AndroidRuntime(24242): FATAL EXCEPTION: pool-89-thread-1
E/AndroidRuntime(24242): Process: com.example.visitor_checkin_app, PID: 24242
E/AndroidRuntime(24242): java.lang.IllegalStateException: Session has been closed; further changes are illegal.
E/AndroidRuntime(24242): at android.hardware.camera2.impl.CameraCaptureSessionImpl.checkNotClosed(CameraCaptureSessionImpl.java:886)
E/AndroidRuntime(24242): at android.hardware.camera2.impl.CameraCaptureSessionImpl.setRepeatingRequest(CameraCaptureSessionImpl.java:303)
E/AndroidRuntime(24242): at io.flutter.plugins.camera.Camera.refreshPreviewCaptureSession(Camera.java:434)
E/AndroidRuntime(24242): at io.flutter.plugins.camera.Camera.access$600(Camera.java:83)
E/AndroidRuntime(24242): at io.flutter.plugins.camera.Camera$2.onConfigured(Camera.java:373)
E/AndroidRuntime(24242): at android.hardware.camera2.impl.CallbackProxies$SessionStateCallbackProxy.lambda$onConfigured$0$CallbackProxies$SessionStateCallbackProxy(CallbackProxies.java:53)
E/AndroidRuntime(24242): at android.hardware.camera2.impl.-$$Lambda$CallbackProxies$SessionStateCallbackProxy$soW0qC12Osypoky6AfL3P2-TeDw.run(Unknown Source:4)
E/AndroidRuntime(24242): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
E/AndroidRuntime(24242): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
E/AndroidRuntime(24242): at java.lang.Thread.run(Thread.java:923)
Flutter version: 2.5.1
Camera version: 0.9.4
Android minSDK version: 27
Below is the code:
import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_ml_kit/google_ml_kit.dart';
class CameraView extends StatefulWidget {
CameraView(
{Key? key,
this.initialDirection = CameraLensDirection.back})
: super(key: key);
static const CAMERAVIEW_ROUTE = "/cameraview";
final String title = "Barcode Scanner";
final CameraLensDirection initialDirection;
#override
_CameraViewState createState() => _CameraViewState();
}
class _CameraViewState extends State<CameraView> {
BarcodeScanner barcodeScanner = GoogleMlKit.vision.barcodeScanner(const [
BarcodeFormat.pdf417
]);
bool isBusy = false;
CameraController? _controller;
int _cameraIndex = 0;
#override
void initState() {
super.initState();
for (var i = 0; i < cameras.length; i++) {
if (cameras[i].lensDirection == widget.initialDirection) {
_cameraIndex = i;
break;
}
}
_startLiveFeed();
isBusy = false;
}
#override
void dispose() {
_stopLiveFeed();
barcodeScanner.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title)
),
body: _body()
);
}
Widget _body() {
Widget body = _liveFeedBody();
return body;
}
Widget _liveFeedBody() {
if (_controller?.value.isInitialized == false) {
return Container();
}
return Container(
color: Colors.black,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
CameraPreview(_controller!)
],
),
);
}
Future _startLiveFeed() async {
final camera = cameras[_cameraIndex];
_controller = CameraController(
camera,
ResolutionPreset.high,
enableAudio: false,
);
_controller?.initialize().then((_) {
if (!mounted) {
return;
}
_controller?.startImageStream(_processCreateInputImage);
if (mounted) {
setState(() {});
}
});
}
Future _stopLiveFeed() async {
await _controller?.stopImageStream();
await _controller?.dispose();
_controller = null;
}
Future _processBarCodeImage(InputImage inputImage) async {
final barcodes = await barcodeScanner.processImage(inputImage);
if (barcodes.length > 0) {
for (var i = 0; i < barcodes.length; ++i) {
Barcode bcode = barcodes[i];
if (bcode.type == BarcodeType.driverLicense) {
BarcodeDriverLicense bcodeDL = bcode.value as BarcodeDriverLicense;
String scannedName = bcodeDL.lastName! + ", " + bcodeDL.firstName!;
Navigator.of(context).popAndPushNamed(
AnotherScreen.ANOTHER_SCREEN_ROUTE,
arguments: ScannedInfoArguments(
scannedName));
break;
}
}
}
}
Future _processCreateInputImage(CameraImage image) async {
if (isBusy) return;
isBusy = true;
final WriteBuffer allBytes = WriteBuffer();
for (Plane plane in image.planes) {
allBytes.putUint8List(plane.bytes);
}
final bytes = allBytes.done().buffer.asUint8List();
final Size imageSize =
Size(image.width.toDouble(), image.height.toDouble());
final camera = cameras[_cameraIndex];
final imageRotation =
InputImageRotationMethods.fromRawValue(camera.sensorOrientation) ??
InputImageRotation.Rotation_0deg;
final inputImageFormat =
InputImageFormatMethods.fromRawValue(image.format.raw) ??
InputImageFormat.NV21;
final planeData = image.planes.map(
(Plane plane) {
return InputImagePlaneMetadata(
bytesPerRow: plane.bytesPerRow,
height: plane.height,
width: plane.width,
);
},
).toList();
final inputImageData = InputImageData(
size: imageSize,
imageRotation: imageRotation,
inputImageFormat: inputImageFormat,
planeData: planeData,
);
final inputImage =
InputImage.fromBytes(bytes: bytes, inputImageData: inputImageData);
await _processBarCodeImage(inputImage);
isBusy = false;
}
}
Looking forward for inputs and/or solutions to solve this.
Thank you

flutter: how to make pull down to refresh flutter webview using the official webview_flutter package

I want to add refresher in flutter web view
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Stack(
children: <Widget>[
Container(
child: Center(
child: Text(_title),
),
),
],
)),
body: SafeArea(
child: WebView(
key: _key,
javascriptMode: JavascriptMode.unrestricted,
initialUrl: _url)),
);
}
}
#peter-koltai: Many thanks for this! I really appreciated your solution the height is working correctly, even the height is coming a bit late (page content is seen, but scrolling height not there), but there were other issues. (Sorry I can't vote you up)
Issues w/ SingleChildScrollView:
SingleChildScrollView has always the absolute height of the page e.g. if a text box was not expanded from the beginning (javascript), the scroll height exceeds the page height.
The WebView gets the whole scroll area height, but doesn't know the display size, so if a bottom or top modal sheet appears, they are not rendered correctly in the view area of the screen but in the absolute complete height of the scroll area, so then you have to scroll e.g. 6000px up and down.
The scroll position stays where you left somewhere in your previous absolute page height, if you browse further w/o a page refresh.
Complete code:
So the solution of #shalin-shah gave me this nice working solution:
I calculate the dragging down distance (>20% of screen height) if you start at the top=0 of the page which then shows the RefreshIndicator until onPageFinished.
webview.dart:
The RefreshIndicator gets a Completer if dragging down distance is reached and starts the reloading with the spinning, which is completed if page finishes loading.
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_web_refresh/pull_to_refresh.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyWebViewWidget extends StatefulWidget {
final String initialUrl;
const MyWebViewWidget({
Key? key,
required this.initialUrl,
}) : super(key: key);
#override
State<MyWebViewWidget> createState() => _MyWebViewWidgetState();
}
class _MyWebViewWidgetState extends State<MyWebViewWidget> with WidgetsBindingObserver {
late WebViewController _controller;
// Drag to refresh helpers
final DragGesturePullToRefresh pullToRefresh = DragGesturePullToRefresh();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
#override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
pullToRefresh.setRefreshDistance(MediaQuery.of(context).size.height);
}
#override
Widget build(context) {
return RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () {
Completer<void> completer = pullToRefresh.refresh();
_controller.reload();
return completer.future;
},
child: WebView(
initialUrl: widget.initialUrl,
javascriptMode: JavascriptMode.unrestricted,
zoomEnabled: true,
gestureNavigationEnabled: true,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
pullToRefresh.dragGestureRecognizer(_refreshIndicatorKey),
},
onWebViewCreated: (WebViewController webViewController) {
_controller = webViewController;
pullToRefresh.setController(_controller);
},
onPageStarted: (String url) { pullToRefresh.started(); },
onPageFinished: (finish) { pullToRefresh.finished(); },
onWebResourceError: (error) {
debugPrint(
'MyWebViewWidget:onWebResourceError(): ${error.description}');
pullToRefresh.finished();
},
),
);
}
}
pull_to_refresh.dart:
After drag start from top=0 of the page and is always downward, the moving distance is calculated, and when it exceeds 20% of the screen size the RefreshIndicator show() is called.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Fixed issue: https://github.com/flutter/flutter/issues/39389
class AllowVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
#override
//override rejectGesture here
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
class DragGesturePullToRefresh {
static const double EXCEEDS_LOADING_TIME = 3000;
static const double REFRESH_DISTANCE_MIN = .2;
late WebViewController _controller;
// loading
Completer<void> completer = Completer<void>();
int msLoading = 0;
bool isLoading = true;
// drag
bool dragStarted = false;
double dragDistance = 0;
double refreshDistance = 200;
Factory<OneSequenceGestureRecognizer> dragGestureRecognizer(final GlobalKey<RefreshIndicatorState> refreshIndicatorKey) {
return Factory<OneSequenceGestureRecognizer>(() => AllowVerticalDragGestureRecognizer()
// Got the original idea from https://stackoverflow.com/users/15862916/shalin-shah:
// https://stackoverflow.com/questions/57656045/pull-down-to-refresh-webview-page-in-flutter
..onDown = (DragDownDetails dragDownDetails) {
// if the page is still loading don't allow refreshing again
if (!isLoading ||
(msLoading > 0 && (DateTime.now().millisecondsSinceEpoch - msLoading) > EXCEEDS_LOADING_TIME)) {
_controller.getScrollY().then((scrollYPos) {
if (scrollYPos == 0) {
dragStarted = true;
dragDistance = 0;
}
});
}
}
..onUpdate = (DragUpdateDetails dragUpdateDetails) {
calculateDrag(refreshIndicatorKey, dragUpdateDetails.delta.dy);
}
..onEnd = (DragEndDetails dragEndDetails) { clearDrag(); }
..onCancel = () { clearDrag(); });
}
void setController(WebViewController controller){ _controller = controller; }
void setRefreshDistance(double height){ refreshDistance = height * REFRESH_DISTANCE_MIN; }
Completer<void> refresh() {
if (!completer.isCompleted) {
completer.complete();
}
completer = Completer<void>();
started();
return completer;
}
void started() {
msLoading = DateTime.now().millisecondsSinceEpoch;
isLoading = true;
}
void finished() {
msLoading = 0;
isLoading = false;
// hide the RefreshIndicator
if (!completer.isCompleted) {
completer.complete();
}
}
void clearDrag() {
dragStarted = false;
dragDistance = 0;
}
void calculateDrag(final GlobalKey<RefreshIndicatorState> refreshIndicatorKey, double dy) async {
if (dragStarted && dy >= 0) {
dragDistance += dy;
// Show the RefreshIndicator
if (dragDistance > refreshDistance) {
debugPrint(
'DragGesturePullToRefresh:refreshPage(): $dragDistance > $refreshDistance');
clearDrag();
unawaited(refreshIndicatorKey.currentState?.show());
}
/*
The web page scrolling is not blocked, when you start to drag down from the top position of
the page to start the refresh process, e.g. like in the chrome browser. So the refresh process
is stopped if you start to drag down from the page top position and then up before reaching
the distance to start the refresh process.
*/
} else {
clearDrag();
}
}
}
This fix was helpful for the gesture events flutter webview VerticalDragGestureRecognizer get no callback but only onDown and onCancel.
The complete code is on github too.
Gif, I am not allowed to post one...
Differences w/o SingleChildScrollView or to e.g. the chrome browser
=> Fixed: Go to update
The RefreshIndicator shows no initial animation by dragging it down until the distance is reached to start the refresh process. (Can be added differently)
The web page scrolling is not blocked, when you start to drag down from the top position of the page to start the refresh process, e.g. like in the chrome browser. So the refresh process is stopped if you start to drag down from the page's top position and then up before reaching the distance to start the refresh process. Check the method in refreshPage() in the pull_to_refresh.dart for my solution and the comment.
I find the differences irrelevant 🤷‍♀️ as the issues destroyed the browsing expierence.
Update
I changed using ScrollNotification which RefreshIndicator interprets right when FixedScrollMetrics are set. So we have the original animation like in SingleChildScrollView or e.g. chrome browser.
github
Complete code:
webview.dart:
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_refresh/pull_to_refresh.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyWebViewWidget extends StatefulWidget {
final String initialUrl;
const MyWebViewWidget({
Key? key,
required this.initialUrl,
}) : super(key: key);
#override
State<MyWebViewWidget> createState() => _MyWebViewWidgetState();
}
class _MyWebViewWidgetState extends State<MyWebViewWidget>
with WidgetsBindingObserver {
late WebViewController _controller;
late DragGesturePullToRefresh dragGesturePullToRefresh;
#override
void initState() {
super.initState();
dragGesturePullToRefresh = DragGesturePullToRefresh();
WidgetsBinding.instance!.addObserver(this);
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
#override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
dragGesturePullToRefresh.setHeight(MediaQuery.of(context).size.height);
}
#override
Widget build(context) {
return
// NotificationListener(
// onNotification: (scrollNotification) {
// debugPrint('MyWebViewWidget:NotificationListener(): $scrollNotification');
// return true;
// }, child:
RefreshIndicator(
onRefresh: () => dragGesturePullToRefresh.refresh(),
child: Builder(
builder: (context) => WebView(
initialUrl: widget.initialUrl,
javascriptMode: JavascriptMode.unrestricted,
zoomEnabled: true,
gestureNavigationEnabled: true,
gestureRecognizers: {Factory(() => dragGesturePullToRefresh)},
onWebViewCreated: (WebViewController webViewController) {
_controller = webViewController;
dragGesturePullToRefresh
.setContext(context)
.setController(_controller);
},
onPageStarted: (String url) { dragGesturePullToRefresh.started(); },
onPageFinished: (finish) { dragGesturePullToRefresh.finished();},
onWebResourceError: (error) {
debugPrint(
'MyWebViewWidget:onWebResourceError(): ${error.description}');
dragGesturePullToRefresh.finished();
},
),
),
);
}
}
pull_to_refresh.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Fixed issue: https://github.com/flutter/flutter/issues/39389
class DragGesturePullToRefresh extends VerticalDragGestureRecognizer {
static const double EXCEEDS_LOADING_TIME = 3000;
late BuildContext _context;
late WebViewController _controller;
// loading
Completer<void> completer = Completer<void>();
int msLoading = 0;
bool isLoading = true;
// drag
double height = 200;
bool dragStarted = false;
double dragDistance = 0;
#override
//override rejectGesture here
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
void _clearDrag() {
dragStarted = false;
dragDistance = 0;
}
DragGesturePullToRefresh setContext(BuildContext context) { _context = context; return this; }
DragGesturePullToRefresh setController(WebViewController controller) { _controller = controller; return this; }
void setHeight(double height) { this.height = height; }
Future refresh() {
if (!completer.isCompleted) {
completer.complete();
}
completer = Completer<void>();
started();
_controller.reload();
return completer.future;
}
void started() {
msLoading = DateTime.now().millisecondsSinceEpoch;
isLoading = true;
}
void finished() {
msLoading = 0;
isLoading = false;
// hide the RefreshIndicator
if (!completer.isCompleted) {
completer.complete();
}
}
FixedScrollMetrics _getMetrics(double minScrollExtent, double maxScrollExtent,
double pixels, double viewportDimension, AxisDirection axisDirection) {
return FixedScrollMetrics(
minScrollExtent: minScrollExtent,
maxScrollExtent: maxScrollExtent,
pixels: pixels,
viewportDimension: viewportDimension,
axisDirection: axisDirection);
}
DragGesturePullToRefresh() {
onStart = (DragStartDetails dragDetails) {
// debugPrint('MyWebViewWidget:onStart(): $dragDetails');
if (!isLoading ||
(msLoading > 0 && (DateTime.now().millisecondsSinceEpoch - msLoading) > EXCEEDS_LOADING_TIME)) {
_controller.getScrollY().then((scrollYPos) {
if (scrollYPos == 0) {
dragStarted = true;
dragDistance = 0;
ScrollStartNotification(
metrics: _getMetrics(0, height, 0, height, AxisDirection.down),
dragDetails: dragDetails,
context: _context)
.dispatch(_context);
}
});
}
};
onUpdate = (DragUpdateDetails dragDetails) {
if (dragStarted) {
double dy = dragDetails.delta.dy;
dragDistance += dy;
ScrollUpdateNotification(
metrics: _getMetrics(
dy > 0 ? 0 : dragDistance, height,
dy > 0 ? (-1) * dy : dragDistance, height,
dragDistance < 0 ? AxisDirection.up : AxisDirection.down),
context: _context,
scrollDelta: (-1) * dy)
.dispatch(_context);
if (dragDistance < 0) {
_clearDrag();
}
}
};
onEnd = (DragEndDetails dragDetails) {
ScrollEndNotification(
metrics: _getMetrics(0, height, dragDistance, height, AxisDirection.down),
context: _context)
.dispatch(_context);
_clearDrag();
};
onCancel = () {
ScrollUpdateNotification(
metrics: _getMetrics(0, height, 1, height, AxisDirection.up),
context: _context,
scrollDelta: 0)
.dispatch(_context);
_clearDrag();
};
}
}
It can be done, basic problem is that RefreshIndicator only works with a scrollable item as child, and WebView is not scrollable from Flutter's point of view (the loaded contents are scrollable). So you have to wrap it into some kind of scrollable, but there comes the other problem: you have to know the height to do so, and you still need the contents of WebView to be scrollable, so that you can scroll up and down on the loaded web page.
The solution includes the following steps, partially using accepted answer here.
Create a RefreshIndicator and add a SingleChildScrollView as its child.
Inside the scroll view add a Container to hold the WebView as child.
Set some initial height for the Container, for example the height of the screen.
After the page is loaded or refreshed, use JavaScript code to get the loaded document's height in the browser.
Resize the Container with the acquired height.
Watch for orientation change, because the same page will have different height in portrait and landscape, and refresh Container height accordingly.
This solution is not perfect. First, as you will see from print outputs in debug console, height settings occur not only when strictly necessary. Second, if the content of a web page changes so that the height of the loaded documents changes as well, without actually reloading the page, height will be not synced. (For example if you add rows to a table on a dynamic webpage.)
Complete code:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebView with RefreshIndicator',
home: Scaffold(
appBar: AppBar(title: Text('WebView with RefreshIndicator')),
body: SafeArea(
child: MyWebWiew(),
)),
);
}
}
class MyWebWiew extends StatefulWidget {
const MyWebWiew({Key? key}) : super(key: key);
#override
_MyWebWiewState createState() => _MyWebWiewState();
}
class _MyWebWiewState extends State<MyWebWiew> with WidgetsBindingObserver {
WebViewController? _webViewController;
// height of the WebView with the loaded content
double? _webViewHeight;
// is true while a page loading is in progress
bool _isPageLoading = true;
#override
void initState() {
super.initState();
// add listener to detect orientation change
WidgetsBinding.instance!.addObserver(this);
}
#override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
_setWebViewHeight();
}
#override
Widget build(BuildContext context) {
// on initial loading, get height using MediaQuery,
// this will be used until page is loaded
if (_webViewHeight == null) {
final initalWebViewHeight = MediaQuery.of(context).size.height;
print('WebView inital height set to: $initalWebViewHeight');
_webViewHeight = initalWebViewHeight;
}
return RefreshIndicator(
// reload page
onRefresh: () => _webViewController!.reload(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
height: _webViewHeight,
child: WebView(
javascriptMode: JavascriptMode.unrestricted,
initialUrl: 'https://flutter.dev',
onWebViewCreated: (WebViewController webViewController) {
_webViewController = webViewController;
},
onPageStarted: (String url) {
setState(() {
_isPageLoading = true;
});
},
onPageFinished: (String url) {
setState(() {
_isPageLoading = false;
});
// if page load is finished, set height
_setWebViewHeight();
})),
));
}
void _setWebViewHeight() {
// we don't update if WebView is not ready yet
// or page load is in progress
if (_webViewController == null || _isPageLoading) {
return;
}
// execute JavaScript code in the loaded page
// to get body height
_webViewController!
.evaluateJavascript('document.body.clientHeight')
.then((documentBodyHeight) {
// set height
setState(() {
print('WebView height set to: $documentBodyHeight');
_webViewHeight = double.parse(documentBodyHeight);
});
});
}
}

Real-Time Updates with Streambuilder in Flutter and Google Maps (Firestore)

I have a sample app that I am building that fetches multiple markers from Firestore and displays them on Google Maps.
The issue is that the changes (markers added/deleted/updated) aren't displayed in real time since it only rebuilds in init state. I think that I could use Stream Builder to listen to the changes and update them in real time.
I am not sure how to do it with Google Maps and Firestore, since the syntax is kinda weird with snapshots, I tried many ways, but not successful. Here's my code:
#override
void initState() {
populateMarkers();
super.initState();
}
populateMarkers() {
FirebaseFirestore.instance.collection('marker').get().then((documents) {
if (documents.docs.isNotEmpty) {
for (int i = 0; i < documents.docs.length; i++) {
initMarker(
documents.docs[i].data(), documents.docs[i].id);
}
}
});
}
void initMarker(request, requestId) {
var markerIdVal = requestId;
final MarkerId markerId = MarkerId(markerIdVal);
//creating new markers
final Marker marker = Marker(
markerId: markerId,
position:
LatLng(request['location'].latitude, request['location'].longitude),
infoWindow:
InfoWindow(title: request['name'], snippet: request['description']),
onTap: () => print('Test'),
);
setState(() {
markers[markerId] = marker;
//print(markerId);
});
}
Widget loadMap() {
return GoogleMap(
markers: Set<Marker>.of(markers.values),
mapType: MapType.normal,
initialCameraPosition:
CameraPosition(target: LatLng(43.8031287, 20.453008), zoom: 12.0),
onMapCreated: onMapCreated,
// },
);
}
and in the buider, I just call loadMap() function as the body. As I mentioned, this works fine, but I would like to use Stream Builder for these functions to update in real time. Any ideas how to do it?
This thread has a similar issue, but I feel it's not the best way to do it:
Flutter - Provide Real-Time Updates to Google Maps Using StreamBuilder
Here is the full code in case someone wants to test it out:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MapScreen extends StatefulWidget {
#override
_MapScreenState createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
Map<MarkerId, Marker> markers = <MarkerId, Marker>{}; //--> google
GoogleMapController _controller;
#override
void initState() {
populateMarkers();
super.initState();
}
onMapCreated(GoogleMapController controller) async {
_controller = controller;
String value = await DefaultAssetBundle.of(context)
.loadString('assets/map_style.json');
_controller.setMapStyle(value);
}
populateMarkers() {
FirebaseFirestore.instance.collection('marker').get().then((documents) {
if (documents.docs.isNotEmpty) {
for (int i = 0; i < documents.docs.length; i++) {
initMarker(
documents.docs[i].data(), documents.docs[i].id); //maybe error
//documents.docs[i].data, documents.docs[i].id
}
}
});
}
void initMarker(request, requestId) {
var markerIdVal = requestId;
final MarkerId markerId = MarkerId(markerIdVal);
//creating new markers
final Marker marker = Marker(
markerId: markerId,
position:
LatLng(request['location'].latitude, request['location'].longitude),
infoWindow:
InfoWindow(title: request['name'], snippet: request['description']),
onTap: () => print('Test'),
);
setState(() {
markers[markerId] = marker;
//print(markerId);
});
}
Widget loadMap() {
return GoogleMap(
// markers: markers.values.toSet(),
markers: Set<Marker>.of(markers.values),
mapType: MapType.normal,
initialCameraPosition:
CameraPosition(target: LatLng(44.8031267, 20.432008), zoom: 12.0),
onMapCreated: onMapCreated,
cameraTargetBounds: CameraTargetBounds(LatLngBounds(
northeast: LatLng(44.8927468, 20.5509553),
southwest: LatLng(44.7465138, 20.2757283))),
mapToolbarEnabled: false,
// onMapCreated: (GoogleMapController controller) {
// _controller = controller;
// },
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
//-----------------------------------
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App'),
centerTitle: true,
backgroundColor: Colors.blue[700],
actions: [
// ElevatedButton(
// onPressed: () => refresh(),
// child: Text('REFRESH'),
// )
],
),
body: loadMap(),
);
}
}
So you want to subscribe to a stream coming from a firestore collection, and use this data to update markers on a map in real-time, is that right?
Please note that you absolutely need to separate your services from your widgets, or you'll end up quickly with a jumbled mess of a code.
In one file you have the class defining access to your database (such as API calls, or Firestore in this case). ANY write or read with Firestore will go through this service.
class FirestoreService {
FirestoreService._();
static final instance = FirestoreService._();
Stream<List<T>> collectionStream<T>({
required String path,
required T Function(Map<String, dynamic> data, String documentID) builder,
}) {
Query query = FirebaseFirestore.instance.collection(path);
final Stream<QuerySnapshot> snapshots = query.snapshots();
return snapshots.map((snapshot) {
final result = snapshot.docs
.map((snapshot) =>
builder(snapshot.data() as Map<String, dynamic>, snapshot.id))
.where((value) => value != null)
.toList();
return result;
});
}
}
In another file, you'll have the repository service, that contains the CRUD operations. Each operation makes a call to the DB service (defined in the previous step) and uses the objects's serialization (toJson, send to DB) or parsing (fromJson, get from DB) methods.
class MarkersRepo {
final _service = FirestoreService.instance;
Stream<List<Marker>> getMarkers() {
return _service.collectionStream(
path: 'marker',
builder:(json, docId){
return Marker.fromJson(json);
}
);
}
}
In another file, you define your Marker object model with the serialization and parsing methods. Please don't use strings to access directly the document properties in your code, as that is error-prone and again will cause messy code. Instead, you define those once and for all in the object model.
class Marker{
//properties of the object such as location, name and description
//toJson and fromJson methods
}
And finally in your widget file, as you noted yourself you are only reading the document once, in the initstate, so the view does not update. Instead, one simple option is to have a StreamBuilder inside your build method, extract the markers there and then display them:
Widget build(Buildcontext context){
return StreamBuilder(
stream: MarkersRepos().getMarkers(),
builder:(context, snapshot){
//check for connection state and errors, see streambuilder documentation
final List<Marker> markers = snapshot.data;
return loadMap(markers);
}
);
}
EDIT: added more details

Flutter, DateTime.now() is not working in my code

I've been developing an application for both iOS and Android.
The code below makes real-time screen which shows current time.
1.I confirmed that DateTime.now() is working on both OS.
2.Also i confirmed that it is working at the actual Android device and Android emulator.
But everytime when i am trying to test on iOS(both emulator and actual device), _timeString always get a null.
What is wrong with this code? I don't get it.
Here is my environment in advance for solving my question.
VMware workstation 15 Player, Xcode 10.3, Android Studio 3.4.2
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:intl/intl.dart';
class TableLayout extends StatefulWidget {
#override
_TableLayoutState createState() => _TableLayoutState();
}
class _TableLayoutState extends State<TableLayout> {
String _timeString;
String _formatDateTime(DateTime dateTime) {
return DateFormat('MM/dd/yyyy HH:mm:ss').format(dateTime);
}
#override
void initState() {
_timeString = _formatDateTime(DateTime.now());
Timer.periodic(Duration(milliseconds: 1000), (Timer t) => _getTime());
super.initState();
}
void _getTime() {
DateTime _now = DateTime.now();
final String formattedDateTime = _formatDateTime(_now);
if (this.mounted) {
setState(() {
_timeString = formattedDateTime;
});
}
}
#override
Widget build(BuildContext context) {
return
Scaffold(
body:
Container(
child:
Text("\n\n\n\n${_timeString}")
)
);
}
}
Try this by changing date format
To
MM/dd/yyyy hh:mm:ss
I ran your code without any change. (Added a entry point however)
and it works as expected.
Any additional information regarding the intl package will be helpful. Here is my entire app and the result of that below.
My dependency is intl: ^0.15.8.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:intl/intl.dart';
void main()
{
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
home: TableLayout(),
);
}
}
class TableLayout extends StatefulWidget {
#override
_TableLayoutState createState() => _TableLayoutState();
}
class _TableLayoutState extends State<TableLayout> {
String _timeString;
String _formatDateTime(DateTime dateTime) {
return DateFormat('MM/dd/yyyy HH:mm:ss').format(dateTime);
}
#override
void initState() {
_timeString = _formatDateTime(DateTime.now());
Timer.periodic(Duration(milliseconds: 1000), (Timer t) => _getTime());
super.initState();
}
void _getTime() {
DateTime _now = DateTime.now();
final String formattedDateTime = _formatDateTime(_now);
if (this.mounted) {
setState(() {
_timeString = formattedDateTime;
});
}
}
#override
Widget build(BuildContext context) {
return
Scaffold(
body:
Container(
child:
Text("\n\n\n\n${_timeString}")
)
);
}
}
App running on IOS simulator