Flutter: Asking Location permission pauses all app execution, how to let it run asynchronously - ios

When my app requests location permission the entire app stops until the dialog is complete (e.g. if the permission dialog pops up during a page transition the transition will freeze mid transition until the dialog is resolved).
Literally it causes execution to pause.
Using: flutter_riverpod, location.
The offending code:
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/all.dart';
import 'package:location/location.dart';
class MapApiBloc extends StateNotifier<MapApiState> {
// Instantiating a location service
static Location _location = Location();
// This will subscribe to the user's location
StreamSubscription<LocationData> _streamSubscription;
// Permissions stuff
bool _serviceEnabled;
PermissionStatus _permissionGranted;
// Initial (empty) state
static const MapApiState _initialState = MapApiState(userLocation: null);
MapApiBloc() : super(_initialState) {
init();
}
// This runs when you initialize the class
init() async {
// Checks if user toggled on location service
_serviceEnabled = await _location.serviceEnabled();
if (!_serviceEnabled) {
_serviceEnabled = await _location.requestService();
if (!_serviceEnabled) {
return;
}
}
// Asks for permission
_permissionGranted = await _location.hasPermission();
if (_permissionGranted == PermissionStatus.denied) {
_permissionGranted = await _location.requestPermission();
if (_permissionGranted != PermissionStatus.granted) {
return;
}
}
// Starts the subscription
_streamSubscription = _location.onLocationChanged.listen((event) {
state = MapApiState(userLocation: event);
});
}
#override
void dispose() {
_streamSubscription.cancel();
super.dispose();
}
}
class MapApiState {
final LocationData userLocation;
const MapApiState({#required this.userLocation});
}
final mapApiProvider = StateNotifierProvider<MapApiBloc>((ref) {
return MapApiBloc();
});
UI Code:
class ViewNearbyMapPage extends StatefulWidget {
#override
_ViewNearbyMapPageState createState() => _ViewNearbyMapPageState();
}
class _ViewNearbyMapPageState extends State<ViewNearbyMapPage> {
Completer<GoogleMapController> _controller = Completer();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Search Sellers")),
body: Consumer(
builder: (context, watch, child) {
var location = watch(mapApiProvider.state);
if (location.userLocation?.latitude == null) {
return Center(child: CircularProgressIndicator());
}
CameraPosition _myPosition = CameraPosition(
target: LatLng(location.userLocation.latitude,
location.userLocation.longitude),
zoom: 14.4746,
);
return GoogleMap(
initialCameraPosition: _myPosition,
onMapCreated: (controller) {
_controller.complete(controller);
},
);
},
),
);
}
}

Related

StateError (Bad state: No element) on IOS only

This error does not occur on Android or web but only on IOS. It seem very trivial but I can't figure out what's wrong.
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:convert';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class ScanQrPage extends StatefulWidget {
#override
_ScanQrPageState createState() => _ScanQrPageState();
}
class _ScanQrPageState extends State<ScanQrPage> {
final qrKey = GlobalKey();
late QRViewController qrViewController;
late Barcode barcode;
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
#override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
Navigator.of(context).pop("");
return new Future(() => true);
},
child: Scaffold(
body: Stack(
children: [
buildQrView(context),
],
),
),
);
}
Widget buildQrView(BuildContext context) {
return QRView(
onQRViewCreated: onQRViewCreated,
key: qrKey,
overlay: QrScannerOverlayShape(
cutOutSize: MediaQuery.of(context).size.width * 0.8),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
if (!p) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('no Permission')),
);
}
}
#override
void dispose() {
qrViewController.dispose();
super.dispose();
}
void onQRViewCreated(QRViewController qrViewController) {
setState(() {
this.qrViewController = qrViewController;
});
qrViewController.scannedDataStream.listen((event) {
setState(() {
this.barcode = event;
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
String rawData = event.code;
Uri data = Uri.dataFromString(rawData);
String para1 = data.queryParameters["buy"] ??
""; //get parameter with attribute "para1"
Codec<String, String> stringToBase64 = utf8.fuse(base64);
if (para1 != "") {
placer = stringToBase64.decode(para1);
}
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Navigator.pop(context, placer);
});
});
});
}
}
I've tried all the solutions with the same error found on stackoverflow (addPostFrameCallback and Future(Duration.zero)) but none of them are exactly the same and does not seem to fix my problem.
I don't think I have having the same issue as any of the other questions.
The exception is happening on the Navigator.pop(context, placer);
Does anyone have any idea how to overcome this?
Why does this only happen on IOS?

Lost connection to device. Exited (sigterm) - Flutter Crash

My Flutter app is crashing on loading
It operates a FutureBuilder and I believe this to be where the issue comes from.
My app makes an API Call and returns the data to a map marker.
When i have the FutureBuilder return a list view it works fine.
However, when i change it to return a Stack containing my Map SDK and the buttons to call the API it crashes on start up.
Relevant code is below, thank you!
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
Future<Stations> stations;
BuildContext _context;
MapMarkerExample _mapMarkerExample;
#override
void initState() {
stations = API_Call().fetchStations();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Example 1'),
),
body: Container(
child: FutureBuilder<Stations>(
future: stations,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text("Error");
}
if (snapshot.connectionState == ConnectionState.done) {
return
Stack(
children: [
HereMap(onMapCreated: _onMapCreated),
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
button('Stations Near Me', _anchoredMapMarkersButtonClicked),
button('Clear', _clearButtonClicked),
],
),
],
),
],
);
}
return Text("Loading");
}
)
)
);
}
api_call.dart
class API_Call {
Future<Stations> fetchStations() async {
var client = http.Client();
final response = await client.get(
'https://transit.hereapi.com/v8/stations?in=x,-x&return=transport&apiKey=API_KEY');
if (response.statusCode == 200) {
return Stations.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load stations');
}
}
}
api_manager.dart
typedef ShowDialogFunction = void Function(String title, String message);
class MapMarkerExample{
void showAnchoredMapMarkers() {
print('step5');
GeoCoordinates geoCoordinates = _callGeoCoordinates();
// use the coords .. to add a marker
_addCircleMapMarker(geoCoordinates, 0);
_addPOIMapMarker(geoCoordinates, 1);
print('step6');
}
GeoCoordinates _callGeoCoordinates() {
print('step7');
var stations;
Future<Stations> fetchStations() async {
stations = await API_Call().fetchStations();
for (Station stations in stations) {
GeoCoordinates geoCoordinates = GeoCoordinates (stations.place.location.lat, stations.place.location.lng);
// use the coords .. to add a marker
_addCircleMapMarker(geoCoordinates, 0);
_addPOIMapMarker(geoCoordinates, 1);
}
}
}
HereMapController _hereMapController;
List<MapMarker> _mapMarkerList = [];
MapImage _poiMapImage;
MapImage _circleMapImage;
ShowDialogFunction _showDialog;
List<MapMarker3D> _mapMarker3DList = [];
MapMarkerExample(ShowDialogFunction showDialogCallback, HereMapController hereMapController) {
_showDialog = showDialogCallback;
_hereMapController = hereMapController;
double distanceToEarthInMeters = 8000;
_hereMapController.camera.lookAtPointWithDistance(
GeoCoordinates(x, -x), distanceToEarthInMeters);
// Setting a tap handler to pick markers from map.
_setTapGestureHandler();
_showDialog("Note", "Tap markers for more.");
}
void clearMap() {
for (var mapMarker in _mapMarkerList) {
_hereMapController.mapScene.removeMapMarker(mapMarker);
}
_mapMarkerList.clear();
for (var mapMarker3D in _mapMarker3DList) {
_hereMapController.mapScene.removeMapMarker3d(mapMarker3D);
}
_mapMarker3DList.clear();
}
Future<void> _addPOIMapMarker(GeoCoordinates geoCoordinates, int drawOrder) async {
// Reuse existing MapImage for new map markers.
if (_poiMapImage == null) {
Uint8List imagePixelData = await _loadFileAsUint8List('assets/poi.png');
_poiMapImage = MapImage.withPixelDataAndImageFormat(imagePixelData, ImageFormat.png);
}
Anchor2D anchor2D = Anchor2D.withHorizontalAndVertical(0.5, 1);
MapMarker mapMarker = MapMarker.withAnchor(geoCoordinates, _poiMapImage, anchor2D);
mapMarker.drawOrder = drawOrder;
Metadata metadata = new Metadata();
metadata.setString("key_poi", "Next Departures");
mapMarker.metadata = metadata;
_hereMapController.mapScene.addMapMarker(mapMarker);
_mapMarkerList.add(mapMarker);
}
Future<void> _addCircleMapMarker(GeoCoordinates geoCoordinates, int drawOrder) async {
// Reuse existing MapImage for new map markers.
if (_circleMapImage == null) {
Uint8List imagePixelData = await _loadFileAsUint8List('assets/circle.png');
_circleMapImage = MapImage.withPixelDataAndImageFormat(imagePixelData, ImageFormat.png);
}
MapMarker mapMarker = MapMarker(geoCoordinates, _circleMapImage);
mapMarker.drawOrder = drawOrder;
_hereMapController.mapScene.addMapMarker(mapMarker);
_mapMarkerList.add(mapMarker);
}
Future<Uint8List> _loadFileAsUint8List(String assetPathToFile) async {
// The path refers to the assets directory as specified in pubspec.yaml.
ByteData fileData = await rootBundle.load(assetPathToFile);
return Uint8List.view(fileData.buffer);
}
void _setTapGestureHandler() {
_hereMapController.gestures.tapListener = TapListener.fromLambdas(lambda_onTap: (Point2D touchPoint) {
_pickMapMarker(touchPoint);
});
}
void _pickMapMarker(Point2D touchPoint) {
double radiusInPixel = 2;
_hereMapController.pickMapItems(touchPoint, radiusInPixel, (pickMapItemsResult) {
// Note that 3D map markers can't be picked yet. Only marker, polgon and polyline map items are pickable.
List<MapMarker> mapMarkerList = pickMapItemsResult.markers;
if (mapMarkerList.length == 0) {
print("No map markers found.");
return;
}
});
}
}
In api_manager.dart, this looks very suspicous, and you aren't returning anything from this function, it could also explain the error saying future not complete
Future<Stations> fetchStations() async {
stations = await API_Call().fetchStations();
for (Station stations in stations) {
GeoCoordinates geoCoordinates = GeoCoordinates (stations.place.location.lat, stations.place.location.lng);
// use the coords .. to add a marker
_addPOIMapMarker(geoCoordinates, 1);
}
// GeoCoordinates geoCoordinates = stations.coordinates;
// _addPOIMapMarker(geoCoordinates, 1);
}
}
You have to return a Stations object from it, try after your for loop something like return stations;, it could fix your problem, if the error changes, it's also a good start.
Also change your line in future builder to this:
if (snapshot.connectionState == ConnectionState.done && snapshot.hasData)
And for the meantime, remove this _setTapGestureHandler(). The crash is most likely caused by some memory leak, and from the code posted, it could be explained by listeners.

Dart: How to properly dispatch bloc event in another bloc

I need to access AuthenticationBloc in my LoginBloc so I can fire the AuthenticationLogin() event if the login is successful. What I did so far is not working.
What I've done:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthenticationBloc authenticationBloc;
final AuthenticateCredentialsUsecase authenticateCredentialsUsecase;
//code
Stream<LoginState> mapEventToState(
LoginEvent event,
) async* {
//code
authenticationBloc.add(AuthenticationLogin());
yield LoginLoadSuccess();
//code
}
}
What I'm trying to accomplish:
class _AppViewState extends State<AppView> {
final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState;
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
builder: (context, child) {
return BlocListener<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
if (state is AuthenticationAuthenticated) {
_navigator.pushAndRemoveUntil<void>(
HomePage.route(),
(route) => false,
);
}
else if (state is AuthenticationUnauthenticated){
_navigator.pushAndRemoveUntil<void>(
LoginScreen.route(),
(route) => false,
);
}
},
child: child,
);
},
onGenerateRoute: (_) => SplashPage.route(),
);
}
}
As you can see, the user is currently in the LoginScreen, once the login is successful, I need to yield the AuthenticationAuthenticated() state in my AuthenticationBloc() so my users will be directed to the HomePage()
How can I yield the AuthenticationAuthenticated() state of the AuthenticationBloc() inside my LoginBloc() - since my login logic happens inside the LoginBloc.
I subscribed the AuthenticationBloc to the status stream of my AuthenticateCredentialsUsecase class.
When the AuthenticateCredentialsUsecase is called in my LoginBloc and the credentials are authenticated...
I then update the status stream - _controller.add(AuthenticationStatus.authenticated);
Which inturn will trigger the AuthenticationLogin event
inside the AuthenticationBloc
AuthenticationBloc
AuthenticationBloc({
#required CheckAuthenticationStatusUsecase checkAuthenticationStatus,
#required LogoutAuthenticatedUserUsecase logoutAuthenticatedUser,
#required AuthenticateCredentialsUsecase authenticateCredentials,
}) : assert(checkAuthenticationStatus != null),
assert(logoutAuthenticatedUser != null),
assert(authenticateCredentials != null),
checkAuthenticationStatusUsecase = checkAuthenticationStatus,
logoutAuthenticatedUserUsecase = logoutAuthenticatedUser,
authenticateCredentialsUsecase = authenticateCredentials,
super(AuthenticationInitial()) {
add(AuthenticationStatusRequested());
_loginStatusSubscription =
authenticateCredentialsUsecase.status.listen((event) {
if (event == AuthenticationStatus.authenticated) {
add(AuthenticationLogin());
}
});
}
AuthenticateCredentialsUsecase
final _controller = StreamController<AuthenticationStatus>();
Stream<AuthenticationStatus> get status async* {
yield AuthenticationStatus.unknown;
yield* _controller.stream;
}
void dispose() => _controller.close();
#override
Future<Either<Failure, AuthenticatedUser>> call(AuthenticationParams params) async {
final result = await repository.authenticateCredentials(params.userName, params.password);
if(result is Right){
_controller.add(AuthenticationStatus.authenticated);
}
return result;
}
Here is one way to do it. You must call BlocBuilder to handles building the widget in response to new states.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
debugShowCheckedModeBanner: false,
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
//If the login is successful, show homepage
if (state is AuthenticationAuthenticated) {
return HomePage();
}
//If the login failed, show login screen
if (state is AuthenticationUnauthenticated) {
return LoginScreen();
}
//If the login is in process, show loading indicator
if (state is AuthenticationInProgress) {
return LoadingIndicator();
}
return SplashScreen();
},
),
);
}
}
At first, the state is AuthenticationUnauthenticated and displays the login screen. If the login is successful then we display homepage, otherwise if it failed we will display LoginScreen.
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthenticationBloc authenticationBloc;
final AuthenticateCredentialsUsecase authenticateCredentialsUsecase;
//code
Stream<LoginState> mapEventToState(
LoginEvent event,
) async* {
if(event is LoginButtonPressed) {
// some logic code
// eg. : final response = UserRepository.login(username: event.username, password: event.password);
authenticationBloc.add(AuthenticationLogin());
//code
}
}
}
And here is the AuthenticationBloc () code which will handle the authentication.
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState>{
//code
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
if(event is AuthenticationLogin) {
yield AuthenticationInProgress();
//code
yield AuthenticationAuthenticated();
}
}
}

'StreamSubscription<LocationData>' cannot be assigned to 'StreamSubscription<Map<String, double>>'

Currently learning Flutter and got this error while trying to detect the location of my device:
A value of type 'StreamSubscription' can't be assigned
to a variable of type 'StreamSubscription>'
I was following an online tutorial but somehow got this error.
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:location/location.dart';
import 'dart:async';
import 'package:flutter/services.dart';
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<MainPage> {
Map<String,double> currentLocation = new Map();
StreamSubscription<Map<String,double>> locationSubscription;
var location = new Location();
String error;
void initState() {
super.initState();
currentLocation['latitude'] = 0.0;
currentLocation['longitude'] = 0.0;
initPlatformState();
locationSubscription =
location.onLocationChanged().listen((Map<String,double> result) {
setState(() {
currentLocation = result;
});
});
}
void initPlatformState() async{
Map<String,double> myLocation;
try {
myLocation = await location.getLocation();
error="";
} on PlatformException catch(e) {
if(e.code == 'PERMISSION_DENIED')
error = "permission denied";
else if(e.code == "PERMISSION_DENIED_NEVER_ASK")
error = "permission denied";
myLocation = null;
}
setState(() {
currentLocation = myLocation;
});
}
#override
Widget build (BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(""),
automaticallyImplyLeading: false,
),
body: Container(
child: FlutterMap(
options: MapOptions(
),
layers: [
TileLayerOptions(
),
]
),
)
);
}
}
I would be very grateful for any advice. Here is the video I followed: https://www.youtube.com/watch?v=K4nYTayjofY&t=321s
Looks like the tutorial was done using an older version of the location plugin, since v2.0.0 they changed the api to return structured data rather than a map.
https://github.com/Lyokone/flutterlocation/blob/master/CHANGELOG.md
So you'd need to either change all your Map<String, double> types to LocationData or set your plugin version to ^1.4.0.
I tried many ways till I found this way thanks to a kind person who helped on another flutter facebook group.Make sure in your pubspec.yaml you update location to the latest version
dependencies:
location: ^2.3.5
Then change it to the following code:
LocationData _currentLocation;
StreamSubscription<LocationData> _locationSubscription;
var _locationService = new Location();
String error;
void initState() {
super.initState();
initPlatformState();
_locationSubscription = _locationService
.onLocationChanged()
.listen((LocationData currentLocation) async {
setState(() {
_currentLocation = currentLocation;
});
});
}
void initPlatformState() async {
try {
_currentLocation = await _locationService.getLocation();
} on PlatformException catch (e) {
if (e.code == 'PERMISSION_DENIED') {
error = 'Permission denied';
}else if(e.code == "PERMISSION_DENIED_NEVER_ASK"){
error = 'Permission denied';
}
_currentLocation = null;
}
You may access longitude and latitude as
_currentLocation.longitude and _currentLocation.latitude
these will return double values. Also, there are more options available at
https://pub.dev/packages/location#-readme-tab-

Flutter Cancel Geolocator Listener

I am using Geolocator library to detect the location of the user. It works well however, I would like the listener to stop receiving any updates when the particular screen requesting the location updates is closed. I have no idea how to achieve this. Here is my code
class _CheckLoactionScreenState extends State<CheckLocationScreen>{
String _geoHash = "No Geo Hash";
String _placeId = "No Place Detected";
String _coordinates = "";
var geolocator = Geolocator();
var locationOptions = LocationOptions(accuracy: LocationAccuracy.high, distanceFilter: 10);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: _body(),
);
}
_body(){
return Center(
child: Text(_geoHash),
),
);
}
_getLocation(){
geolocator.getPositionStream(locationOptions).listen(
(Position position) {
//print("${position.toString()}");
var newGeoHash = Geohash.encode(position.latitude, position.longitude).substring(0,8);
if(newGeoHash != _geoHash){
setState((){
_geoHash = newGeoHash;
});
}
_coordinates = position.toString();
print(_geoHash);
});
}
#override
void initState() {
super.initState();
_getLocation();
}
#override
void dispose() {
print("**** dispose");
geolocator.getPositionStream(null).listen(null);
super.dispose();
}
}
I attempt to cancel the listener in the dispose method but the listener still persists.
StreamSubscription _getPositionSubscription;
_getLocation(){
_getPositionSubscription = geolocator.getPositionStream(locationOptions).listen(
...
}
#override
void dispose() {
_getPositionSubscription?.cancel();
super.dispose();
}

Resources