
Google Maps API 키 발급
Google Cloud Console에서 API 키를 발급받는 과정은 다음과 같습니다.
- Google Cloud Console(https://console.cloud.google.com/)에 접속하여 로그인합니다.
- 새 프로젝트를 생성하고, Maps SDK for Android와 Maps SDK for iOS를 활성화합니다.
- 사용자 인증 정보 메뉴에서 API 키를 생성하고 복사해둡니다.
1. Google Maps API
- google_maps_flutter 패키지: 이 패키지는 Flutter에서 Google Maps Platform을 편리하게 사용할 수 있도록 도와주는 '다리(Bridge)' 역할을 합니다.
- 작동 방식: 이 패키지는 Dart 코드를 Android의 네이티브 Google Maps SDK for Android와 iOS의 네이티브 Google Maps SDK for iOS로 변환하여 실행합니다.
2. AGP 오류가 발생한다면 수정하기
gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
settings.gradle
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.5.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
}
AndroidManifest.xml
<application
android:label="dem_google_map"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCX0k01nqnZyCU9EKJ8fhtYVnkyt6YizgY"/>
yml
cupertino_icons: ^1.0.8
google_maps_flutter: ^2.5.0 # 구글 맵
shared_preferences: ^2.2.2 # 로컬 저장
models/saved_marker.dart
// 저장시킬 마커 정보를 담는 클래스 (추후 확장 가능)
class SavedMaker {
final String id;
final String name;
final double latitude;
final double longitude;
// _ <-- private <-- 외부에서 생성자 호출 방법이 없다.
SavedMaker._internal({
required this.id,
required this.name,
required this.latitude,
required this.longitude,
});
// 1. static 저장소(캐시) 준비
static final Map<String, SavedMaker> _cache = <String, SavedMaker>{};
// 팩토링 생성자
factory SavedMaker.fromMap(Map<String, dynamic> map) {
String id = map["id"] as String;
if (_cache.containsKey(id)) {
print('캐시에 저장된 객체 리턴 및 활용');
// map 구조에 있는 객체를 반환
return _cache[id]!;
}
final newMarker = SavedMaker._internal(
id: id,
name: map["name"],
latitude: map["latitude"],
longitude: map["longitude"],
);
_cache[id] = newMarker;
return newMarker;
}
// SavedMarker 를 SharedPreferences에 저장할 형태로 사용하기 위해 설계
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'latitude': latitude,
'longitude': longitude,
};
}
// Map<String, dynamic> map = SavedMarker().toMap();
}
screens/map_screen.dart
import 'package:class_google_map_v1/models/saved_maker.dart';
import 'package:class_google_map_v1/screens/marker_list_screen.dart';
import 'package:class_google_map_v1/services/storage_service.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
// 구글맵 사용해보기 sdk 버전
// 1단계 - 지도를 조작하기 위한 리모컨 필요 - GoogleMapController
GoogleMapController? _controller;
// 특정 위치로 부드럽게 이동 처리
// 현재 줌 레벨 조정 1 ~ 20 (확대, 축소)
// 1 - 전세계 지도. 15 - 도시 레벨
// 화면의 중심 좌표 가져오기
// 2 - 중심 좌표 객체 생성
static const LatLng _initCenter = LatLng(35.162439, 129.062293);
Set<Marker> _markerSet = {};
final StorageService _storageService = StorageService();
// SP에 저장되어 있던 데이터를 담을 자료구조
List<SavedMaker> _savedMarkers = [];
@override
void initState() {
super.initState();
_loadMarkers();
}
/// 저장된 마커들을 불러오기
Future<void> _loadMarkers() async {
try {
final List<SavedMaker> loadedMakers = await _storageService.loadMarkers();
setState(() {
_savedMarkers = loadedMakers;
_markerSet.clear(); // 기존에 있던 마커를 다 제거
for (SavedMaker m in loadedMakers) {
_markerSet.add(
Marker(
markerId: MarkerId(m.id),
position: LatLng(m.latitude, m.longitude),
infoWindow: InfoWindow(
title: m.name,
snippet: "나의 장소",
),
),
);
}
});
} catch (e) {
_showMessage('마커를 불러오는데 실패');
}
}
// 스낵바 메세지 만들어보기
void _showMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 1),
),
);
}
void _onMapTapped(LatLng position) {
final nameController = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('새 장소 추가'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: InputDecoration(
labelText: '장소 이름',
hintText: '장소 이름을 입력하세요',
border: OutlineInputBorder(),
),
autofocus: true,
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('취소'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_addMarker(position, nameController.text);
},
child: Text('추가'),
)
],
);
},
);
}
void _addMarker(LatLng position, String name) {
// 고유 ID 생성
final markerId = DateTime.now().millisecond.toString();
// 새로운 SavedMarker 객체 생성
final newMarker = SavedMaker.fromMap(
{
'id': markerId,
'name': name,
'latitude': position.latitude,
'longitude': position.longitude,
},
);
setState(() {
// 새롭게 생성한 객체를 마커 List 자료구조에 넣기
_savedMarkers.add(newMarker);
// 지도에 마커 추가
_markerSet.add(
Marker(
markerId: MarkerId(markerId),
position: position,
infoWindow: InfoWindow(
title: name,
snippet: '저장된 장소',
)),
);
});
// 로컬에 저장
_saveMarkers();
}
Future<void> _saveMarkers() async {
try {
final success = await _storageService.saveMakers(_savedMarkers);
if (success) {
_showMessage("마커 저장 완료");
} else {
_showMessage("마커 저장 실패");
}
} catch (e) {
_showMessage("마커 저장 실패");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue[400],
foregroundColor: Colors.white,
title: Text(
'처음 만나는 구글 맵',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
actions: [
IconButton(
onPressed: () {
_showMarkerList();
},
icon: Icon(Icons.list),
),
IconButton(
onPressed: () async {
await StorageService().clearAllMakers();
setState(() {
_markerSet = {};
_savedMarkers = [];
});
},
icon: Icon(Icons.delete_forever),
),
],
),
// 3 - GoogleMap 위젯 사용
// GoogleMap - 지도의 모든 것을 담을 수 있는 그릇
body: GoogleMap(
onMapCreated: (controller) {
// controller 새롭게 생성된 지도 안에 생성한 GoogleController
// 내 코드에서 사용하기 위해 멤버 변수로 선언
// 주소값을 연결 시켜 두었다.
_controller = controller;
},
initialCameraPosition: CameraPosition(
target: _initCenter,
zoom: 15,
),
markers: _markerSet,
onTap: _onMapTapped,
),
bottomNavigationBar: Container(
color: Colors.blue[200],
padding: EdgeInsets.all(16.0),
child: Text(
'마커된 장소 ${_savedMarkers.length} 개',
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
),
);
}
// 마커 목록 화면 표시 메서드
void _showMarkerList() async {
if (_savedMarkers.isEmpty) {
_showMessage('저장된 장소가 없습니다');
}
/// 중요!! 화면 이동 후 코드가 여기서 멈춰져 있다. await
// 저장된 마커들이 있다면 화면을 stack 구조로 올릴 예정
// <> 타입 안정성을 위해 제네릭 사용
final result =
await Navigator.of(context).push<SavedMaker>(MaterialPageRoute(
builder: (context) {
return MarkerListScreen();
},
));
if (result != null) {
// 리스트 화면에서 선택된 마커로 포커스를 이동하는 기능
await _moveToMarker(result);
}
// 목록에서 돌아온 후 마커를 다시 로드 (새로고침 - 삭제된 마커가 있다면 반영)
await _loadMarkers();
}
// 메서드 추가
/// 코드로 특정 마커 위치로 이동하는 기능
Future<void> _moveToMarker(SavedMaker marker) async {
// 구글 맵에서 특정 포지션으로 이동시키는 기능은 _controller 담당
if (_controller != null) {
_controller!.animateCamera(
CameraUpdate.newLatLngZoom(
LatLng(marker.latitude, marker.longitude), 16.0),
);
}
}
}
screens/market_list_screen.dart
import 'package:class_google_map_v1/models/saved_maker.dart';
import 'package:class_google_map_v1/services/storage_service.dart';
import 'package:flutter/material.dart';
class MarkerListScreen extends StatefulWidget {
const MarkerListScreen({super.key});
@override
State<MarkerListScreen> createState() => _MarkerListScreenState();
}
class _MarkerListScreenState extends State<MarkerListScreen> {
// 데이터가 필요
List<SavedMaker> _markers = [];
final StorageService _storageService = StorageService();
@override
void initState() {
super.initState();
// 저장된 마커 불러 오기
_loadMarkers();
}
// 1.
Future<void> _loadMarkers() async {
final markers = await _storageService.loadMarkers();
setState(() {
_markers = markers;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('저장된 장소 ${_markers.length} 개'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: ListView.builder(
itemCount: _markers.length,
itemBuilder: (context, index) {
final marker = _markers[index];
return Card(
margin: const EdgeInsets.all(12),
child: ListTile(
leading: Icon(Icons.place, color: Colors.blue),
title: Text(marker.name),
subtitle: Text('위도:${marker.latitude}\n경도:${marker.longitude}'),
trailing: IconButton(
onPressed: () async {
print('삭제');
final success = await _storageService.deleteMarker(marker.id);
if (success) {
_loadMarkers();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'${marker.id}가 삭제되었습니다',
)));
}
},
icon: Icon(
Icons.delete,
color: Colors.red,
),
),
onTap: () {
print('선택된 마커로 자동 이동 처리');
// 화면 스택 구조 구글맵에서 --> 저장된 마커 리스트가 올라온 상태
// 화면을 제거하고 선택된 마커값을 뒤 화면에 전달 시키는 기능
Navigator.of(context).pop(marker);
},
),
);
},
),
);
}
}
services/storage_service.dart
// SharedPreferences 를 가공해서 관리하는 서비스 클래스
import 'dart:convert';
import 'package:class_google_map_v1/models/saved_maker.dart';
import 'package:shared_preferences/shared_preferences.dart';
class StorageService {
// SharedPreferences - 저장 키
static const String _markerKey = "saved_markers";
// C R U D
/// SavedMarker 객체를 저장하는 기능 설계
Future<bool> saveMakers(List<SavedMaker> markers) async {
try {
final prefs = await SharedPreferences.getInstance();
// [SavedMarker(), SavedMarker(), SavedMarker()]
// dart 에서 json 형식을 만들 때 map 구조에서 변경하는 것이 가장 편하다.
// {'key1' : 1, 'key2' : "abc"} --> jsonEncode(map) --> {"key1" : 1, "key2" : "abc"}
// 자료구조에서 map 메서드는 (내부 형태를 다른 형태로 바꿀 때 편하게 사용하는 메서드)
final markersMapList = markers.map((e) => e.toMap()).toList();
// {'id' : "1"}, {'id' : "3"}]; --> List<Map<>>
// List<Map> 형태를 json 형식으로 변환
final jsonString = jsonEncode(markersMapList);
return await prefs.setString(_markerKey, jsonString); // true
} catch (e) {
return false;
}
}
/// 저장된 마커 불러오기
Future<List<SavedMaker>> loadMarkers() async {
try {
final prefs = await SharedPreferences.getInstance();
// 저장되어 있는 JSON 형식의 문자열 가져오기
final markerJson = prefs.getString(_markerKey);
if (markerJson != null) {
final List<dynamic> markersList = jsonDecode(markerJson);
// [{'id' : "1"}, {'id' : "2"}]
// 내부에 데이터 타입(형태를 변경할 때 쓰는 것)
// e --> {'id' : "1"}
return markersList.map((e) => SavedMaker.fromMap(e)).toList();
// [ SavedMarker(), SavedMarker() ] 변경 됨
}
return [];
} catch (e) {
return [];
}
}
/// 특정 id의 마커 삭제
Future<bool> deleteMarker(String markerId) async {
try {
final markers = await loadMarkers();
final updatedMarkers =
// list.where()
// --> 조건을 만족하는 요소들로만 새로운 컬렉션을 만들어주는 메서드
markers.where((marker) => marker.id != markerId).toList();
return await saveMakers(updatedMarkers);
} catch (e) {
print('마커 삭제 오류 $e');
return false;
}
}
/// 모든 SavedMarker 를 삭제하는 기능 추가
Future<bool> clearAllMakers() async {
try {
final prefs = await SharedPreferences.getInstance();
// "saved_markers" : "[{'id' : "1"}, {'id' : "2"}]"
return await prefs.remove(_markerKey);
} catch (e) {
return false;
}
}
/// 마커 객체 하나만 추가 하는 코드 (중복체크)
Future<bool> addMarker(SavedMaker newMarker) async {
try {
// 1. {..} 데이터를 가져와야 한다.
// 2. {..} + {..} 기존에 있던 데이터에 새롭게 추가 한다.
// 3. SP에 다시 리스트 저장하기
/// List 객체가 새롭게 추가 된 상태
final List<SavedMaker> markers = await loadMarkers();
/// 중복 체크 Stream API를 활용 하면 편리 하다.
// 자료구조에 any() 메서드 사용 해보기
/// markers.any() <-- 조건을 만족하는 요소가 하나라도 있으면 true 반환
if (markers.any((marker) => marker.id == newMarker.id)) {
print('이미 존재 하는 마커 입니다');
return false;
}
// 새 마커를 리스트 자료구조에 추가 하는 코드
markers.add(newMarker);
// 리스트 객체를 다시 SharedPreferences에 저장 하는 코드
return await saveMakers(markers);
} catch (e) {
return false;
}
}
}
main.dart
import 'package:class_google_map_v1/screens/map_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '나의 지도',
debugShowCheckedModeBanner: false,
home: const MapScreen(),
);
}
}
'Flutter' 카테고리의 다른 글
| 구글 맵 3단계 마커 활용 (0) | 2025.08.20 |
|---|---|
| SharedPreferences (0) | 2025.08.20 |
| 카메라 및 이미지 다루기 4단계 (0) | 2025.08.20 |
| 카메라 다루기 3단계 (0) | 2025.08.20 |
| 카메라 다루기 2단계 (0) | 2025.08.20 |