https://github.com/cjk09083/ftweather
지난번 포스팅 까진 개발이 중단된 NaverMap (https://github.com/LBSTECH/naver_map_plugin) 플러그인을 사용중이였으나, 해당 플러그인에는 카메라 이동이 안되는 치명적인 오류가 있었다.
마침 찾아놨었던 새로운 플러그인 (https://pub.dev/packages/flutter_naver_map) v1.0이 출시되어 해당 라이브러리로 업데이트 하고 카메라 이동 기능을 추가하였다.
API 문서 : https://note11.dev/flutter_naver_map/
1. 플러그인 변경
pubspec.yaml에서 기존 플러그인을 제거하고 새로운 플러그인을 추가해준다.
dependencies:
flutter:
sdk: flutter
#naver_map_plugin:
# git: https://github.com/LBSTECH/naver_map_plugin.git <-- 제거
flutter_naver_map: ^1.0.0 # 추가
이후 Pub get을 해준다.
2. 코드 변경
플러그인 변경에 따라 NaverMap 관련 코드들을 수정해준다.
모든 수정된 부분을 설명하기엔 글이 길어지므로 중요한 수정부분만 언급하겠다.
# main.dart
아래와 같이 runApp() 전에 NaverMapsdk 초기화를 진행해준다. nMapKey는 기존에 사용하던 sdk key이다.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await nMapInit(); // NaverMaps 초기화
await fcmInit(); // firebase 초기화 및 fcm 관련 설정
await requestLocationPermission(); // 위치 권한 획득
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MapModel()),
],
child: MyApp(),
),
);
}
Future<void> nMapInit() async {
await NaverMapSdk.instance.initialize(
clientId: nMapKey,
onAuthFailed: (error) {
log('$TAG Auth failed: $error');
});
}
# MapWidget.dart
NaverMap 위젯을 바꿔주고, 초기화 option을 따로 생성해서 연결해준다.
기존 NaverMap 과 달리 맵을 클릭하면 자동으로 InfoWindow가 닫히지 않으므로, 수동으로 닫아주는 리스너를 추가한다.
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_naver_map/flutter_naver_map.dart';
import 'package:ftweather/provider/MapModel.dart';
import 'package:provider/provider.dart';
import '../main.dart';
class MapWidget extends StatelessWidget {
MapWidget({Key? key}) : super(key: key);
NaverMapViewOptions option = const NaverMapViewOptions(
initialCameraPosition: NCameraPosition(
target: NLatLng(37.5666805, 126.9784147),
zoom: 15,
bearing: 0,
tilt: 0
),
mapType: NMapType.basic,
locationButtonEnable: true,
);
@override
Widget build(BuildContext context) {
final model = Provider.of<MapModel>(context, listen: false);
return Expanded(
flex: 6,
child: Stack(
children: [
NaverMap(
options: option,
onMapReady: (controller) {
model.setController(controller);
},
onMapTapped: (point, latLng) {
log("$TAG onMapTapped point: $point, latLng: $latLng");
model.allOverlayClose();
},
onSymbolTapped: (symbol){
log("$TAG onSymbolTapped symbol: $symbol");
model.allOverlayClose();
},
),
Positioned(
bottom: 16,
right: 16,
child: FloatingActionButton(
onPressed: () {
// addMarker() 메서드를 호출하여 현재 위치에 마커 추가
model.addMarker(context);
},
child: const Icon(Icons.add),
),
),
],
),
);
}
}
# MapModel.dart
모든 'Marker' 클래스를 'NMarker'로 수정한다.
NMarker는 Marker와 달리 infoWindow 정보를 담고있지 못하므로
infoWindow 리스트를 따로 만들어서 관리하도록 추가한다.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_naver_map/flutter_naver_map.dart';
import 'dart:developer';
import 'package:shared_preferences/shared_preferences.dart';
import '../main.dart';
class MapModel extends ChangeNotifier {
// NaverMapController 변수
NaverMapController? _controller;
NaverMapController? get controller => _controller;
// 마커 목록 관리
final List<NMarker> _markers = [];
List<NMarker> get markers => _markers;
// 인포 목록 관리
final List<NInfoWindow> _infoWindows = [];
List<NInfoWindow> get infoWindows => _infoWindows;
// 인포 데이터 목록 관리
final List<InfoData> _infoList = [];
bool loadComp = false;
// NaverMapController 초기화 메서드
void setController(NaverMapController controller) {
_controller = controller;
_controller!.clearOverlays();
waitForLoadComp();
}
Future<void> waitForLoadComp() async {
while (!loadComp) {
await Future.delayed(const Duration(milliseconds: 100));
}
log("마커 추가 : ${_markers.length}");
_infoWindows.clear();
for (int i = 0; i < _markers.length; i++) {
NMarker marker = _markers[i];
_controller!.addOverlay(marker);
InfoData infoData = _infoList[i];
NInfoWindow onMarkerInfoWindow = NInfoWindow.onMarker(
id: infoData.id,
text: infoData.text,
);
_infoWindows.add(onMarkerInfoWindow);
marker.setOnTapListener((NMarker marker) {
allOverlayClose();
marker.openInfoWindow(onMarkerInfoWindow);
});
}
}
MapModel() {
initializeMarkers();
}
void initializeMarkers() async {
log("initializeMarkers");
SharedPreferences prefs = await SharedPreferences.getInstance();
String markerList = prefs.getString('marker') ?? '[]';
String infoList = prefs.getString('info') ?? '[]';
_markers.addAll(markerFromJson(markerList));
_infoList.addAll(infoFromJson(infoList));
loadComp = true;
notifyListeners();
}
// 현재 위치에 마커를 추가하는 메서드
Future<void> addMarker(BuildContext context) async {
// 로그 출력
log("$TAG : addMarker");
final TextEditingController nameController = TextEditingController();
final formKey = GlobalKey<FormState>();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Marker 추가"),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: "이름",
),
validator: (value) {
if (value == null || value.isEmpty) {
return "Name cannot be empty";
}
return null;
},
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
if (formKey.currentState!.validate()) {
Navigator.of(context).pop();
_createMarker(nameController.text);
}
},
child: const Text("추가하기"),
),
],
),
),
);
},
);
}
// 마커 생성 및 목록에 추가하는 메서드
void _createMarker(String name) async {
// NaverMapController가 초기화되었는지 확인
if (_controller != null) {
// 현재 카메라 위치 가져오기
NCameraPosition currentCameraPosition = await _controller!.getCameraPosition();
// 로그 출력
log("$TAG : Camera Pos $currentCameraPosition");
final now = DateTime.now();
final timeWithoutMicroseconds = DateTime(now.year, now.month, now.day, now.hour, now.minute, now.second);
final createdAt = timeWithoutMicroseconds.toString().replaceAll('.000', '');
String infoText = "$name\n"
"lat: ${currentCameraPosition.target.latitude.toStringAsFixed(7)}\n"
"lon: ${currentCameraPosition.target.longitude.toStringAsFixed(7)}\n"
"$createdAt";
// 현재 위치에 마커 추가
final marker = NMarker(
id: createdAt,
position: currentCameraPosition.target,
caption: NOverlayCaption(text: name),
);
final onMarkerInfoWindow = NInfoWindow.onMarker(
id: marker.info.id,
text: infoText
);
// Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
// log("$TAG : Real Pos $position");
marker.setOnTapListener((NMarker marker) {
allOverlayClose();
marker.openInfoWindow(onMarkerInfoWindow);
});
// 마커 목록에 추가
_markers.add(marker);
_infoList.add(InfoData(marker.info.id, infoText));
_infoWindows.add(onMarkerInfoWindow);
_controller!.addOverlay(marker);
// 마커 리스트 -> json String으로 변환
await saverMarkers();
}
// 마커가 추가되었음을 알림
notifyListeners();
}
Future<void> saverMarkers() async {
// 마커 리스트 -> json String으로 변환
String jsonMarker = markerToJson(_markers);
String jsonInfo = infoToJson(_infoList);
log('$TAG Markers to Json : $jsonMarker');
// jsonData 저장
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('marker', jsonMarker);
prefs.setString('info', jsonInfo);
}
Future<void> removeMarker(int index) async {
_markers.removeAt(index);
_infoList.removeAt(index);
_infoWindows.removeAt(index);
// 마커 리스트 -> json String으로 변환
await saverMarkers();
notifyListeners();
}
String infoToJson(List<InfoData> info) {
List<Map<String, dynamic>> infoList = info.map((info) {
return {
'id': info.id,
'name': info.text,
};
}).toList();
return json.encode(infoList);
}
String markerToJson(List<NMarker> markers) {
List<Map<String, dynamic>> markerList = markers.map((marker) {
return {
'id': marker.info.id,
'name': marker.caption!.text,
'latitude': marker.position.latitude,
'longitude': marker.position.longitude,
};
}).toList();
return json.encode(markerList);
}
List<NMarker> markerFromJson(String jsonString) {
List<dynamic> markerList = json.decode(jsonString);
List<NMarker> newMarkers = markerList.map((marker) {
NLatLng position = NLatLng(marker['latitude'], marker['longitude']);
NMarker newMarker = NMarker(
id: marker['id'],
position: position,
caption: NOverlayCaption(text: marker['name']??''),
);
return newMarker;
}).toList();
return newMarkers;
}
List<InfoData> infoFromJson(String jsonString) {
List<dynamic> infoList = json.decode(jsonString);
List<InfoData> newInfoList = infoList.map((info) {
return InfoData(info['id'], info['name'] );
}).toList();
return newInfoList;
}
void allOverlayClose(){
log('$TAG allOverlayClose ${_infoWindows.length}');
for(NInfoWindow window in _infoWindows){
if(window.isAdded) window.close();
}
}
}
class InfoData {
String id, text;
InfoData(this.id, this.text);
}
# MarkerList.dart
기존 'Marker'에서 info 정보를 가져오지 못하므로 'NMarker'에서 가져온 정보들로 대신 출력한다.
import 'package:flutter/material.dart';
import 'package:flutter_naver_map/flutter_naver_map.dart';
import 'package:ftweather/provider/MapModel.dart';
import 'package:provider/provider.dart';
class MarkerList extends StatelessWidget {
MarkerList({Key? key}) : super(key: key);
List<int> flexList = [20,40,30,15];
@override
Widget build(BuildContext context) {
final markers = Provider.of<MapModel>(context).markers;
return Expanded(
flex: 4,
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10),
child: Row(
children: [
buildHeader('이름', flex: flexList[0], size: 13, weight: FontWeight.bold),
buildDivider(thick: 1),
buildHeader('경도, 위도', flex: flexList[1], size: 13, weight: FontWeight.bold),
buildDivider(thick: 1),
buildHeader('등록일', flex: flexList[2], size: 13, weight: FontWeight.bold),
buildDivider(thick: 1),
buildHeader('삭제', flex: flexList[3], size: 13, weight: FontWeight.bold),
],
),
),
const Divider(color: Colors.black, thickness: 1,),
Expanded(
child: markerListView(markers),
),
],
),
);
}
ListView markerListView(List<NMarker> markers) {
return ListView.separated(
separatorBuilder: (context, index) => const Divider(color:Colors.black),
itemCount: markers.length,
itemBuilder: (BuildContext context, int index) {
NMarker marker = markers[index];
final name = marker.caption!.text;
final lat = marker.position.latitude.toStringAsFixed(6);
final lng = marker.position.longitude.toStringAsFixed(6);
final createdAt = marker.info.id;
return ListTile(
title: Row(
children: [
buildHeader(name, flex: flexList[0], ),
buildDivider(),
buildHeader('$lat,\n$lng', flex: flexList[1], ),
buildDivider(),
buildHeader(createdAt, flex: flexList[2], ),
buildDivider(),
Expanded(flex: flexList[3],
child: IconButton(
onPressed: () {
Provider.of<MapModel>(context, listen: false).removeMarker(index);
},
icon: const Icon(Icons.delete_forever_rounded),
iconSize: 30,
),
),
],
),
);
},
);
}
Widget buildHeader(String text,
{int flex = 1, double size = 12, weight = FontWeight.normal, color = Colors.black}) {
return Expanded(
flex: flex,
child: Text(text,textAlign: TextAlign.center,
style: TextStyle(color: color, fontSize: size, fontWeight: weight)
)
);
}
Widget buildDivider({double thick = 0.0}) {
return SizedBox(
height: 20,
child: VerticalDivider(
color: Colors.black,
width: 1,
thickness: thick,
),
);
}
}
3. 카메라 이동 기능 추가
ListView에서 마커 하나를 클릭시 지도에서도 해당 마커의 위치로 카메라를 이동시키는 기능을 추가하였다.
먼저 MarkerList.dart에서 ListView 부분을 아래와 같이 GestureDetector()로 감싸고 onTap() 리스너에 moveCamera를 추가한다.
ListView markerListView(List<NMarker> markers) {
return ListView.separated(
separatorBuilder: (context, index) => const Divider(color:Colors.black),
itemCount: markers.length,
itemBuilder: (BuildContext context, int index) {
NMarker marker = markers[index];
final name = marker.caption!.text;
final lat = marker.position.latitude.toStringAsFixed(6);
final lng = marker.position.longitude.toStringAsFixed(6);
final createdAt = marker.info.id;
return GestureDetector(
onTap: (){
Provider.of<MapModel>(context, listen: false).moveCamera(index);
},
child: ListTile(
title: Row(
children: [
buildHeader(name, flex: flexList[0], ),
buildDivider(),
buildHeader('$lat,\n$lng', flex: flexList[1], ),
buildDivider(),
buildHeader(createdAt, flex: flexList[2], ),
buildDivider(),
Expanded(flex: flexList[3],
child: IconButton(
onPressed: () {
Provider.of<MapModel>(context, listen: false).removeMarker(index);
},
icon: const Icon(Icons.delete_forever_rounded),
iconSize: 30,
),
),
],
),
),
);
},
);
}
이후 MapModel.dart에 아래와 같이 moveCamera 함수를 추가한다.
Future<void> moveCamera(int index) async {
if (_controller != null) {
NMarker marker = _markers[index];
NLatLng markerLatLng = NLatLng(
marker.position.latitude, marker.position.longitude);
log('$TAG moverCamera to : $markerLatLng');
final cameraUpdate = NCameraUpdate.scrollAndZoomTo(
target: markerLatLng,
);
await _controller!.updateCamera(cameraUpdate);
notifyListeners();
}
}
추가 결과 List에서 마커를 클릭하면 해당 마커로 CameraUpdate가 진행되는것을 확인할 수 있다.
'프로젝트들 > 날씨앱' 카테고리의 다른 글
[날씨앱] P10 Flutter ListView 드래그로 순서 변경 (0) | 2023.04.24 |
---|---|
[날씨앱] P9 Flutter NaverMap 클릭한 좌표에 마커 추가 (0) | 2023.04.21 |
[날씨앱] P7. Flutter Shared Preferences 추가 (0) | 2023.04.14 |
[날씨앱] P6. Flutter ListView 추가 (0) | 2023.04.14 |
[날씨앱] P5. Flutter file, func 분류 (0) | 2023.04.07 |