feat: harden geofence and distance diagnostics

This commit is contained in:
Artem Kokos
2026-05-01 09:13:23 +07:00
parent 872ddf9513
commit 91a494adf5
20 changed files with 1639 additions and 260 deletions

View File

@@ -5,15 +5,28 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
enum UserLocationIssue {
servicesDisabled,
permissionDenied,
permissionDeniedForever,
unavailable,
}
/// Состояние геолокации: позиция или причина отсутствия.
/// Запрашивается один раз, кешируется до перезапуска провайдера.
class UserLocation {
final Position? position;
final String? error; // null -- всё ок, иначе причина
final UserLocationIssue? issue;
final DateTime? updatedAt;
const UserLocation({this.position, this.error});
const UserLocation({this.position, this.error, this.issue, this.updatedAt});
bool get hasPosition => position != null;
bool get needsAppSettings =>
issue == UserLocationIssue.permissionDeniedForever;
bool get needsLocationSettings => issue == UserLocationIssue.servicesDisabled;
bool get canRequestPermission => issue == UserLocationIssue.permissionDenied;
/// Расстояние в км до точки. Возвращает null если нет позиции
/// или у цели нет координат.
@@ -50,30 +63,7 @@ class UserLocationNotifier extends Notifier<UserLocation> {
/// и отдаёт lastKnown мгновенно (если есть).
Future<void> fetch() async {
if (state.hasPosition) return;
final err = await _ensurePermission();
if (err != null) {
state = UserLocation(error: err);
return;
}
try {
final last = await Geolocator.getLastKnownPosition();
if (last != null) {
state = UserLocation(position: last);
return;
}
final pos = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.low,
timeLimit: Duration(seconds: 10),
),
);
state = UserLocation(position: pos);
} catch (e) {
state = UserLocation(error: 'Ошибка: $e');
}
await refresh();
}
/// Начать непрерывное отслеживание. Вызывать из initState экрана.
@@ -83,9 +73,9 @@ class UserLocationNotifier extends Notifier<UserLocation> {
_watchers++;
if (_sub != null) return;
final err = await _ensurePermission();
if (err != null) {
state = UserLocation(error: err);
final permissionState = await _ensurePermission();
if (!permissionState.isGranted) {
state = permissionState.toLocation();
return;
}
@@ -93,7 +83,7 @@ class UserLocationNotifier extends Notifier<UserLocation> {
try {
final last = await Geolocator.getLastKnownPosition();
if (last != null) {
state = UserLocation(position: last);
state = _fromPosition(last);
}
} catch (_) {}
}
@@ -104,9 +94,14 @@ class UserLocationNotifier extends Notifier<UserLocation> {
);
_sub = Geolocator.getPositionStream(locationSettings: settings).listen(
(pos) => state = UserLocation(position: pos),
(pos) => state = _fromPosition(pos),
onError: (e) {
debugPrint('Ошибка стрима геолокации: $e');
state = UserLocation(
error: 'Не удалось отслеживать позицию: $e',
issue: UserLocationIssue.unavailable,
updatedAt: state.updatedAt,
);
},
);
}
@@ -120,11 +115,61 @@ class UserLocationNotifier extends Notifier<UserLocation> {
}
}
Future<void> refresh() async {
final permissionState = await _ensurePermission();
if (!permissionState.isGranted) {
state = permissionState.toLocation();
return;
}
try {
final last = await Geolocator.getLastKnownPosition();
if (last != null) {
state = _fromPosition(last);
return;
}
final pos = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.low,
timeLimit: Duration(seconds: 10),
),
);
state = _fromPosition(pos);
} catch (e) {
state = UserLocation(
error: 'Не удалось получить позицию: $e',
issue: UserLocationIssue.unavailable,
updatedAt: state.updatedAt,
);
}
}
Future<void> requestPermission() async {
await Geolocator.requestPermission();
if (_watchers > 0 && _sub == null) {
await startWatching();
return;
}
await refresh();
}
Future<void> openAppSettings() async {
await Geolocator.openAppSettings();
}
Future<void> openLocationSettings() async {
await Geolocator.openLocationSettings();
}
/// Проверить сервис и пермишены. Возвращает null если всё ок,
/// иначе строку с причиной ошибки.
Future<String?> _ensurePermission() async {
Future<_LocationPermissionState> _ensurePermission() async {
if (!await Geolocator.isLocationServiceEnabled()) {
return 'Геолокация выключена';
return const _LocationPermissionState(
issue: UserLocationIssue.servicesDisabled,
message: 'Геолокация выключена',
);
}
var perm = await Geolocator.checkPermission();
@@ -132,12 +177,35 @@ class UserLocationNotifier extends Notifier<UserLocation> {
perm = await Geolocator.requestPermission();
}
if (perm == LocationPermission.denied) {
return 'Нет разрешения';
return const _LocationPermissionState(
issue: UserLocationIssue.permissionDenied,
message: 'Нет разрешения на геолокацию',
);
}
if (perm == LocationPermission.deniedForever) {
return 'Разрешение запрещено навсегда';
return const _LocationPermissionState(
issue: UserLocationIssue.permissionDeniedForever,
message: 'Доступ к геолокации запрещён навсегда',
);
}
return null;
return const _LocationPermissionState();
}
UserLocation _fromPosition(Position position) {
return UserLocation(position: position, updatedAt: position.timestamp);
}
}
class _LocationPermissionState {
final UserLocationIssue? issue;
final String? message;
const _LocationPermissionState({this.issue, this.message});
bool get isGranted => issue == null;
UserLocation toLocation() {
return UserLocation(error: message, issue: issue);
}
}