프로젝트들/날씨앱

[날씨앱] P8 CameraMove 추가 & NaverMaps 플러그인 변경

Choi Jaekuk 2023. 4. 19. 18:03

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가 진행되는것을 확인할 수 있다.