플러터(Flutter) 예제(Example) – Google Map API, Geolocator

구글 맵 API를 이용하기 위한 google_maps_flutter 플러그인과 GPS 활용을 위한 geolocator 플러그인을 사용하여 회사나 목적지 근처에 도착을 체크하는 예제를 만들어보자.

Geolocator 플러그인

Geolocator 플러그인의 주요 기능은 위치 서비스 권한을 얻거나 확인하는 것과 GPS 위치를 활용할 수 있는 것이다.

위치 서비스 권한

위치 서비스 권한이 켜져 있는지 확인하는 함수로 IsLocationServiceEnabled() 를 사용한다.

bool isLocationEnabled = await Geolocator.isLocationServiceEnabled();

앱의 위치 서비스 사용 권한 확인 함수는 checkPermission() 이다. 권한 확인 후 아직 권한을 받지 못하였다면 requestPermission() 함수로 권한을 요청한다.

LocationPermission chkPermission = await Geolocator.checkPermission();
LocationPermission reqPermission = await Geolocator.requestPermission();

LocationPermission

  • denied – 기본 상태(요청한 적이 없는 상태) 또는 거절 상태.
  • deniedForever – 완전 거절 상태로 이 상태에서는 기기의 설정 화면에서 직접 허가해 줘야 한다.
  • whileInUse – 앱이 사용 중일때만 허가된 상태.
  • always – 허가 상태. (항상)
  • unableToDetermine – 알 수 없음. (앱에서는 해당 사항이 없음)

현재 위치 스트리밍

Geolocator 플러그인의 getPositionStream() 함수로 현재 위치 변경 감지 시마다 Position 클래스 타입으로 얻어올 수 있다.

Geolocator.getPositionStream().listen((Position position) {  });

위치 간의 거리 구하는 방법

Geolocator 플러그인의 distanceBetween() 함수를 사용한다.

final distance = Geolocator.distanceBetween( startLat, startLng, endLat, endLng );

Google Map API Key 발급 방법

구글 클라우드에 접속하여 프로젝트를 하나 임의의 이름으로 생성한다.

구글-클라우드
구글-클라우드-프로젝트생성

상단 탐색창에 “Maps SDK for”를 입력하고 검색한다.

구글-클라우드-MapsSDK-검색

검색한 Maps SDK 활성화 과정을 마치고 아래 스크린샷과 같이 “키 및 사용자 인증 정보”로 가서 “키 표시” 버튼을 통해 키를 복사해 둔다.

구글-클라우드-맵-API-키

예제 구현

플러그인 추가 – google_maps_flutter, geolocator

pubspec.yaml

...
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8
  google_maps_flutter: ^2.9.0
  geolocator: ^13.0.1
...

위치 사용 권한과 구글 맵 API 키 등록

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <application
        ...
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="발급받은 구글 맵 API 키"/>
        ...

ios/Runner/AppDelegate.swift

import Flutter
import UIKit
import GoogleMaps

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMServices.provideAPIKey("발급받은 구글 맵 API 키")
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

info.plist

...
	<key>NSLocationWhenInUseUsageDesription</key>
	<string>위치 정보가 필요합니다.</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>위치 정보가 필요합니다.</string>
</dict>
</plist>

예제 코드

파일 구조

file-structure

이번 예제는 home_screen 단일 파일로 구현되었다.

main.dart

import 'package:flutter/material.dart';
import 'package:geo_chool_check/screen/home_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const HomeScreen(),
    );
  }
}

screen/home_screen.dart

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';

class HomeScreen extends StatelessWidget {
  static final LatLng companyLatLng = LatLng(37.5649151, 126.9870850);1
  static final Marker marker = Marker(
    markerId: MarkerId('company'),
    position: companyLatLng,
  );
  static final Circle circle = Circle(
    circleId: CircleId('checkCircle'),
    center: companyLatLng,
    fillColor: Colors.blue.withValues(alpha: 0.5),
    radius: 100,
    // 반지름 (미터 단위)
    strokeColor: Colors.blue,
    strokeWidth: 1,
  );

  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: renderAppBar(),
      body: FutureBuilder(
        future: checkPermission(),
        builder: (context, snapshot) {
          if (snapshot.hasData &&
              snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          if (snapshot.data == "위치 권한이 허용 되었습니다.") {
            return Column(
              children: [
                Expanded(
                  flex: 2,
                  child: GoogleMap2(
                    initialCameraPosition: CameraPosition(
                      target: companyLatLng,
                      zoom: 16,
                    ),
                    myLocationEnabled: true3,
                    myLocationButtonEnabled: true,
                    markers: {marker}4,
                    circles: {circle},
                  ),
                ),
                Expanded(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(
                        Icons.timelapse_outlined,
                        color: Colors.blue,
                        size: 50.0,
                      ),
                      const SizedBox(height: 20.0),
                      ElevatedButton(
                        onPressed: () async {
                          final curPos = await Geolocator.getCurrentPosition();
                          final distance = Geolocator.distanceBetween(
                            curPos.latitude,
                            curPos.longitude,
                            companyLatLng.latitude,
                            companyLatLng.longitude,
                          );

                          bool canCheck = distance < 100;5
                          showDialog(
                            context: context,
                            builder: (_) {
                              return AlertDialog(
                                title: Text('Check'),
                                content: Text(
                                  canCheck
                                      ? 'Could you check now?'
                                      : "You can't check where current position",
                                ),
                                actions: [
                                  TextButton(
                                    onPressed: () {
                                      Navigator.of(context).pop(false);
                                    },
                                    child: Text('Cancel'),
                                  ),
                                  if (canCheck)
                                    TextButton(
                                      onPressed: () {
                                        Navigator.of(context).pop(true);
                                      },
                                      child: Text('Do Check'),
                                    ),
                                ],
                              );
                            },
                          );
                        },
                        child: Text('Check!'),
                      ),
                    ],
                  ),
                ),
              ],
            );
          }

          return Center(child: Text(snapshot.data.toString()));
        },
      ),
    );
  }

  AppBar renderAppBar() {
    return AppBar(
      centerTitle: true,
      title: Text(
        '출첵',
        style: TextStyle(color: Colors.blue, fontWeight: FontWeight.w700),
      ),
      backgroundColor: Colors.white,
    );
  }

  Future<String> checkPermission() async {
    // 위치 서비스 활성화 여부
    final isLocationEnabled = await Geolocator.isLocationServiceEnabled();

    if (!isLocationEnabled) {
      return '위치 서비스를 활성화해주세요.';
    }

    // 위치 권한 확인
    LocationPermission checkPermission = await Geolocator.checkPermission();

    // 위치 권한 거절 상태
    if (checkPermission == LocationPermission.denied) {
      // 위치 권한 요청
      checkPermission = await Geolocator.requestPermission();

      if (checkPermission == LocationPermission.denied) {
        return '위치 권한을 허용해 주세요.';
      }
    }

    // 위치 권한 재요청 불가
    if (checkPermission == LocationPermission.deniedForever) {
      return "앱의 위치 권한 설정을 허용해 주세요.";
    }

    return "위치 권한이 허용 되었습니다.";
  }
}
  1. LatLng 클래스는 위도와 경도로 위치를 표현하는 클래스이다. 첫 번째 매개변수 latitude에 위도를 두 번째 매개변수 longitude에 경도를 입력해 준다. ↩︎
  2. GoogleMap 위젯은 필수 입력인 initialCameraPosition 인자가 있다. 여기에는 CameraPosition 클래스를 입력해 준다. CameraPosition의 target 인자는 지도의 중심이 될 위치를 LatLng로 입력할 수 있다. ↩︎
  3. 현재 위치를 지도에 표시하는 옵션으로 GoogleMap 위젯에 myLocationEnabled 인자가 있다. 기본 값이 false이니 true로 입력해 주었다. ↩︎
  4. Marker 클래스를 사용하여 각 마커별 ID와 함께 위치를 입력해 준다. 그 후 GoogleMap 위젯의 markers 인자에 Set 형태로 원하는 만큼의 Marker를 넣어준다. ↩︎
  5. 목표 지점과 100미터 반경 안에 있는 경우 체크 가능하도록 팝업 화면의 버튼을 활성화 해 주었다. ↩︎

참고

플러터(flutter) 윈도우(Windows)에 개발환경 구축

댓글 남기기