Hello world !
absent lors du dernier coaching de groupe (25/01), j’ai pu le regarder en replay (c’est vraiment top comme fonctionnalité !!! merci @mbritto )
Dans les commentaires du replay, ‹ Kevin Y › et @Quentin, vous évoquez le package Flutter_map.
De mon côté, je suis en train de l’implémenter dans une application et voici mon avancement :
- Récupération de coordonnées dans Directus (ok)
- Affichage de la carte avec les marker des coordonnées (ok)
- Bouton pour se géolocaliser (ok)
- Boutons pour se positionner sur certains types de marker (ok)
Packages :
- Flutter
- latlong2
- geolocator
- flutter_map_marker_cluster
De mon point de vue, j’ai l’impression d’avoir fait beaucoup de bricolage et énormément de test de package (surtout pour la géolocalisation).
J’ai uniquement un bug que je n’ai pas encore résolu :
Quand je change d’écran avant d’avoir terminé l’affichage de la carte, j’ai le message suivant sur mes setState() :
FlutterError (setState() called after dispose(): _MapScreenState#8131c(lifecycle state: defunct, not mounted)
je me demande également si c’est normal de n’avoir aucun dispose() dans mon écran ?
(j’utilise les deux controllers : MapController et AnimationController)
Je vous propose mon écran et son viewModel si ca peut aider quelqu’un
map_screen.dart
import 'dart:async';
import 'package:bordered_text/bordered_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:pub_et_moi/ui/screens/map_viewmodel.dart';
import '../../data/model/mission.dart';
import '../../ressources/assets_manager.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
class MapScreen extends StatefulWidget {
final MapViewModel _viewModel;
const MapScreen(this._viewModel, {Key? key}) : super(key: key);
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> with TickerProviderStateMixin {
final MapController mapController = MapController();
final markersVestimentaires = <Marker>[];
int markersVestimentairesCount = 0;
bool _isEnableVestimentaires = true;
final markersVoitures = <Marker>[];
int markersVoituresCount = 0;
bool _isEnableVoitures = true;
final markersFlyers = <Marker>[];
int markersFlyersCount = 0;
bool _isEnableFlyers = true;
var localisationMarker = <Marker>[];
bool _isLoading = false;
List<Marker> myMarkers = [];
final minLatLng = LatLng(49.8566, 1.3522);
final maxLatLng = LatLng(58.3498, -10.2603);
/*
* Initialisation de la Classe
*/
@override
void initState() {
super.initState();
_getMissions();
}
/*
* Récupération des Missions
*/
_getMissions() async {
setState(() {
_isLoading = true;
});
final List<Mission> missions = await widget._viewModel.getMissions();
for (var element in missions) {
LatLng point = LatLng(element.coordonnees.coordinates[1],
element.coordonnees.coordinates[0]);
if (element.typeMission.libelle == Assets.strMissionsVestimentaires) {
markersVestimentaires.add(
addMarker(point, element, Assets.iconMissionsVestimentaires),
);
}
if (element.typeMission.libelle == Assets.strMissionsVoitures) {
markersVoitures.add(
addMarker(point, element, Assets.iconMissionsVoitures),
);
}
if (element.typeMission.libelle == Assets.strMissionsFlyers) {
markersFlyers.add(
addMarker(point, element, Assets.iconMissionsFlyers),
);
}
}
markersVestimentairesCount = markersVestimentaires.length;
markersVoituresCount = markersVoitures.length;
markersFlyersCount = markersFlyers.length;
myMarkers = [];
for (var element in markersVestimentaires) {
myMarkers.add(element);
}
for (var element in markersVoitures) {
myMarkers.add(element);
}
for (var element in markersFlyers) {
myMarkers.add(element);
}
_getCurrentLocation();
}
Marker addMarker(LatLng point, Mission element, Icon icone) {
return Marker(
width: 200,
height: 200,
point: point,
builder: (ctx) => GestureDetector(
onTap: () {
_showModal(context, element.titre, element.details);
},
child: Column(
children: [
FloatingActionButton.extended(
backgroundColor: Assets.couleur,
label: Text(element.titre),
icon: icone,
onPressed: () =>
_showModal(context, element.titre, element.details),
),
const Icon(Icons.push_pin),
],
),
),
);
}
/*
* Récupération de la Localisation
*/
void _getCurrentLocation() async {
setState(() {
_isLoading = true;
});
final bool hasPermission = await widget._viewModel.determinePermission();
if (!hasPermission) {
setState(() {
_isLoading = false;
});
return;
}
final Position myPosition = await widget._viewModel.determinePosition();
LatLng point = LatLng(myPosition.latitude, myPosition.longitude);
localisationMarker = [];
localisationMarker.add(
Marker(
width: 80,
height: 80,
point: point,
builder: (ctx) => GestureDetector(
child: Column(
children: const [
Icon(Icons.gps_fixed),
],
),
),
),
);
_animatedMapMove(LatLng(myPosition.latitude, myPosition.longitude), 8);
}
/*
* Animation de la MAP
*/
void _animatedMapMove(LatLng destLocation, double destZoom) {
setState(() {
_isLoading = true;
});
final latTween = Tween<double>(
begin: mapController.center.latitude, end: destLocation.latitude);
final lngTween = Tween<double>(
begin: mapController.center.longitude, end: destLocation.longitude);
final zoomTween = Tween<double>(begin: mapController.zoom, end: destZoom);
final controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> animation =
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn);
controller.addListener(() {
mapController.move(
LatLng(latTween.evaluate(animation), lngTween.evaluate(animation)),
zoomTween.evaluate(animation));
});
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.dispose();
} else if (status == AnimationStatus.dismissed) {
controller.dispose();
}
});
setState(() {
_isLoading = false;
});
controller.forward();
}
_onButtonVestimentaire() {
_isEnableVestimentaires = !_isEnableVestimentaires;
_onRefreshButton();
if (_isEnableVestimentaires) {
_animatedMapMove(
LatLng(markersVestimentaires[0].point.latitude,
markersVestimentaires[0].point.longitude),
8);
}
setState(() {});
}
_onButtonVoiture() {
_isEnableVoitures = !_isEnableVoitures;
_onRefreshButton();
if (_isEnableVoitures) {
_animatedMapMove(
LatLng(markersVoitures[0].point.latitude,
markersVoitures[0].point.longitude),
8);
}
setState(() {});
}
_onButtonFlyers() {
_isEnableFlyers = !_isEnableFlyers;
_onRefreshButton();
if (_isEnableFlyers) {
_animatedMapMove(
LatLng(markersFlyers[0].point.latitude,
markersFlyers[0].point.longitude),
8);
}
setState(() {});
}
_onRefreshButton() {
myMarkers = [];
if (_isEnableVestimentaires) {
myMarkers = myMarkers + markersVestimentaires;
}
if (_isEnableVoitures) {
myMarkers = myMarkers + markersVoitures;
}
if (_isEnableFlyers) {
myMarkers = myMarkers + markersFlyers;
}
}
@override
Widget build(BuildContext context) {
bool isLoading;
if (_isLoading) {
isLoading = true;
} else {
isLoading = false;
}
return Stack(
children: <Widget>[
Align(
alignment: Alignment.center,
child: FlutterMap(
mapController: mapController,
options: MapOptions(
interactiveFlags:
InteractiveFlag.pinchZoom | InteractiveFlag.drag,
center: LatLng(51.5, -0.09),
zoom: 5,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
),
MarkerLayer(markers: localisationMarker),
MarkerClusterLayerWidget(
options: MarkerClusterLayerOptions(
spiderfyCircleRadius: 80,
spiderfySpiralDistanceMultiplier: 2,
circleSpiralSwitchover: 12,
maxClusterRadius: 120,
rotate: true,
size: const Size(40, 40),
anchor: AnchorPos.align(AnchorAlign.center),
fitBoundsOptions: const FitBoundsOptions(
padding: EdgeInsets.all(50),
maxZoom: 15,
),
polygonOptions: const PolygonOptions(
borderColor: Assets.couleur,
color: Colors.black12,
borderStrokeWidth: 3),
markers: myMarkers,
builder: (context, markers) {
return FloatingActionButton(
onPressed: null,
child: Text(markers.length.toString()),
);
},
),
),
],
),
),
if (isLoading)
const Center(
child: CircularProgressIndicator(),
),
Align(
alignment: Alignment.topCenter,
child: Transform.scale(
scale: 0.7,
child: Container(
margin: const EdgeInsets.all(1),
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: const Color.fromARGB(100, 15, 170, 206),
border: Border.all(
color: Assets.couleur,
style: BorderStyle.solid,
width: 3,
),
borderRadius: BorderRadius.circular(30.0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FloatingActionButton.small(
onPressed: _onButtonVestimentaire,
backgroundColor: _isEnableVestimentaires
? Assets.couleur
: Colors.grey,
child: const Icon(
Icons.checkroom,
// size: 36.0,
),
),
BorderedText(
strokeWidth: 4,
strokeColor: Assets.couleur,
child: Text(
"Missions ${Assets.strMissionsVestimentaires} : $markersVestimentairesCount",
style: const TextStyle(
color: Colors.white,
fontSize: 21.0,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FloatingActionButton.small(
onPressed: _onButtonVoiture,
backgroundColor:
_isEnableVoitures ? Assets.couleur : Colors.grey,
child: const Icon(
Icons.directions_car,
// size: 36.0,
),
),
BorderedText(
strokeWidth: 4,
strokeColor: Assets.couleur,
child: Text(
"Missions ${Assets.strMissionsVoitures} : $markersVoituresCount",
style: const TextStyle(
color: Colors.white,
fontSize: 21.0,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FloatingActionButton.small(
onPressed: _onButtonFlyers,
backgroundColor:
_isEnableFlyers ? Assets.couleur : Colors.grey,
child: const Icon(
Icons.description,
// size: 36.0,
),
),
BorderedText(
strokeWidth: 4,
strokeColor: Assets.couleur,
child: Text(
"Missions ${Assets.strMissionsFlyers} : $markersFlyersCount",
style: const TextStyle(
color: Colors.white,
fontSize: 21.0,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
),
Align(
alignment: Alignment.bottomRight,
child: Container(
margin: const EdgeInsets.all(15.0),
child: FloatingActionButton(
onPressed: _getCurrentLocation,
backgroundColor: Assets.couleur,
child: const Icon(Icons.gps_fixed_outlined),
),
),
),
],
);
}
Future<dynamic> _showModal(BuildContext context, title, details) {
return showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(30.0),
),
),
backgroundColor: Assets.couleur,
builder: (context) {
return SizedBox(
height: 250.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(title),
Text(details),
ElevatedButton.icon(
onPressed: () {
//OnPressed Logic
},
icon: const Icon(Icons.check),
label: const Text("Accepter la mission")),
],
),
);
});
}
}
ma_viewmodel.dart
import 'package:geolocator/geolocator.dart';
import '../../data/model/mission.dart';
abstract class MapRouter {
recupMissions();
}
class MapViewModel {
final MapRouter _router;
MapViewModel(this._router);
Future<List<Mission>> getMissions() async {
final List<Mission> missions = await _router.recupMissions();
return missions;
}
Future<bool> determinePermission() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return false;
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return false;
}
}
if (permission == LocationPermission.deniedForever) {
return false;
}
return true;
}
Future<Position> determinePosition() async {
return await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
}
}