[Flutter] Kakao map API와 WebView

이제일·2021년 11월 30일
3

Flutter

목록 보기
6/6
post-thumbnail

Kakao map API

플루터 프로젝트를 진행 중 지도를 사용할 일이 생겼다.

플루터의 경우 google maps를 이용할 경우 플러그인을 직접 제공하는 등 이용의 편리함이 있다.

하지만 국내 지도에는 특화되어있지 않은점과 가격등을 고려하여 kakao의 지도 서비스를 이용하기로 하고 이에 대해 써보려 한다.

참고로 가격 비교는 아래의 그림과 같다.

Map API 가이드

kakao map guide page에서 충분히 설명이 잘되어있기에 추가적으로 언급하지는 않겠습니다.

플랫폼 설정
웹뷰로 보여질 것이기에 접속할 도메인을 등록하여 api를 이용해야한다.
이때 등록을 안하고 직접 html을 생성하여 요청할 수 있지만, 정적인 지도만 생성가능하고, 라이브러리의 사용이 불가하다.

다음의 경우는 해당 상황에서 주소를 좌표로 변환하는 라이브러리를 사용했을 때 나온 에러이다.


WebView

kakao map의 경우 flutter를 직접적으로 지원해주지않는다.
이를 위해서 먼저 webview를 사용하는 방법을 소개해보도록 한다.

install

pub dev에서 웹뷰를 띄우고, 통신하는 라이브러리를 설치한다.

안드로이드 세팅
안드로이드 폴더의 manifest 파일에서 min sdk 설정을 19로 한다.(readme 참조)

android {
     defaultConfig {
         minSdkVersion 19
     }
 }

permission

네트워크를 사용하기 위해서 권한을 설정한다.
낮은 보안 수준의 권한이라 특별한 요청은 필요하지않다.

  • Android
    AndroidManifest.xml 파일에 다음과 같이 설정한다.
    <uses-permission android:name="android.permission.INTERNET"/>

  • IOS
    info.plist에 다음과 같이 설정한다.

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key> <true/>
        <key>NSAllowsArbitraryLoadsInWebContent</key> <true/>
    </dict>
    
 <key>io.flutter.embedded_views_preview</key> <true/>
 	<key>YES</key>

웹뷰 예제

준비는 다 되었으니 간단한 지도를 띄어보자.

import 'package:webview_flutter/webview_flutter.dart';

...

class MapTest extends StatelessWidget {
  String url = "";  // 띄울 웹 페이지의 주소 
  Set<JavascriptChannel>? channel; // 아래 설명 참조
  WebViewController? controller;   // 아래 설명 참조

  
  Widget build(BuildContext context) {
    return WebView(
        initialUrl: url,
        onWebViewCreated: (controller) { 
          this.controller = controller;
        },
        javascriptChannels: channel,
        javascriptMode: JavascriptMode.unrestricted // 자바스크립트 허용
    );
  }
}

페이지 통신

지도의 특성상 마커의 클릭, 추가등의 이벤트등을 페이지와 소통해야한다. 이를 위해서 다음의 두가지를 이용할 수 있다.

controller

자바스크립트 함수 실행 등 이벤트를 보낼 수 있다.
controller.runJavascript('alert("$url")');

channel

리스너를 등록하여 페이지의 이벤트를 플루터로 수신받을 수 있게한다.

dart file

 Set<JavascriptChannel>? channel = JavascriptChannel(
	name: 'onClickMarker', onMessageReceived: (message){
		Fluttertoast.showToast(msg: message.message);
      });

javascript file

marker = new kakao.maps.Marker({
  map: map,
  position: new kakao.maps.LatLng(lat,lng),
  image: new kakao.maps.MarkerImage(imageUrl, new kakao.maps.Size(60, 54)),
});

kakao.maps.event.addListener(marker, 'click', function(){
  	// javascript channel로 메시지를 보내는 함수
	onClickMarker.postMessage('marker is clicked');
});

해상도 문제

홈페이지의 경우는 픽셀단위를 사용하고 플루터의 위젯은 Logical Pixel 단위를 사용하기에 위젯에 설정한 크기와 웹에서 불러온 페이지의 크기가 다를 수 있다.

이 때문에 지도 크기가 작게 나오는데 이를 위해 위젯을 확대하고, 튀어나온 만큼 자르기를 사용한다.

class MapTest extends StatelessWidget {
  String url = "";
  Set<JavascriptChannel>? channel;
  WebViewController? controller;

  
  Widget build(BuildContext context) {
    double ratio = MediaQuery.of(context).devicePixelRatio;
    return ClipRect( // 자르기
        child: Transform.scale( // 확대
        scale: ratio,
        child: WebView(
        initialUrl: url,
        onWebViewCreated: (controller) {
          this.controller = controller;
        },
        javascriptChannels: channel,
        javascriptMode: JavascriptMode.unrestricted,
      )
    ));
  }
}


Geolocator

현재 위치를 가져오는 geolocator 라이브러리를 통해 현재 위치를 가져올 수 있다.
이를 이용해 지도에 현재 위치를 표시해보자.

pub dev page 참조

compile failed
7.0.0 버전 이상 부터는 null safety 세이프티의 지원으로 인해 cannot find symbol과 같은 에러가 뜰 수 있다.
해당의 경우 안드로이드의 Mankfest 파일에서 compile sdk를 다음과 같이 설정한다.

android {
    compileSdkVersion 31
    defaultConfig {
        minSdkVersion 21 
        targetSdkVersion 29
        ...
    }
...
}

stackoverflow 참조

permission

GPS를 사용하기 위해 권한을 설정한다.

  • Android
    AndroidManifest.xml 파일에 다음과 같이 설정한다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  • IOS
    info.plist에 다음과 같이 설정한다.
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Used to Location&apos;s Info always for Using Beacon.</string>

<key>NSLocationAlwaysUsageDescription</key>
<string>Used to Location&apos;s Info always for Using Beacon.</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to Location&apos;s Info for Using Beacon when In Use.</string>

현재 위치 가져오기

pub dev의 readme에 다음과 같은 예제가 있다.

import 'package:geolocator/geolocator.dart';

/// Determine the current position of the device.
///
/// When the location services are not enabled or permissions
/// are denied the `Future` will return an error.
Future<Position> _determinePosition() async {
  bool serviceEnabled;
  LocationPermission permission;

  // Test if location services are enabled.
  serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    // Location services are not enabled don't continue
    // accessing the position and request users of the 
    // App to enable the location services.
    return Future.error('Location services are disabled.');
  }

  permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      // Permissions are denied, next time you could try
      // requesting permissions again (this is also where
      // Android's shouldShowRequestPermissionRationale 
      // returned true. According to Android guidelines
      // your App should show an explanatory UI now.
      return Future.error('Location permissions are denied');
    }
  }
  
  if (permission == LocationPermission.deniedForever) {
    // Permissions are denied forever, handle appropriately. 
    return Future.error(
      'Location permissions are permanently denied, we cannot request permissions.');
  } 

  // When we reach here, permissions are granted and we can
  // continue accessing the position of the device.
  return await Geolocator.getCurrentPosition();
}

실행시 권한 요청등 자동으로 이루어지고 현재 위치를 가져온다.


완성된 예제

중요한 부분을 제외하고 생략된 부분이 있습니다.

class MapTestAPP extends StatelessWidget{
  WebViewController? controller;
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(children: [
        Container(height:600,child:MapTest(controller)),
        ElevatedButton(onPressed: (){createCurrentMarker();}, child: const Text('현재위치 표시'))
      ])
    );
  }
  void createCurrentMarker() {
    StaticFunctions.getCurrentLocation().then((value) => {
      if(value != null) {
        controller!.runJavascript('createCurrentMarker(${value[0]},${value[1]})')
      }
    });
  }
}
class MapTest extends StatelessWidget {
  String url = "";
  Set<JavascriptChannel>? channel;
  WebViewController? controller;
  
  MapTest(this.controller){
    channel = {JavascriptChannel(
        name: 'onClickMarker', onMessageReceived: (message){
          Fluttertoast.showToast(msg: message.message);
        })
    };
  }
  
  
  Widget build(BuildContext context) {
    double ratio = MediaQuery.of(context).devicePixelRatio;
    return ClipRect(
        child: Transform.scale(
        scale: ratio,
        child: WebView(
        initialUrl: url,
        onWebViewCreated: (controller) {
          this.controller = controller;
        },
        javascriptChannels: channel,
        javascriptMode: JavascriptMode.unrestricted,
      )
    ));
  }
}

javascript

...

function createCurrentMarker(lat,lng) {
    if(currentMarker){
        currentMarker.setPosition(new kakao.maps.LatLng(lat, lng));
    }else{
        currentMarker = new kakao.maps.Marker({
            map: map,
            position: new kakao.maps.LatLng(lat, lng),
            image: new kakao.maps.MarkerImage(imageUrl, new kakao.maps.Size(45, 41)),
        });
    }
    kakao.maps.event.addListener(currentMarker, 'click', function(){
            onClickMarker.postMessage('marker is clicked');
      });
    map.panTo(new kakao.maps.LatLng(lat, lng));
}
profile
세상 제일 이제일

0개의 댓글