본문 바로가기

코딩강의/favorite_places(플러터-유데미)

~250. Displaying a Location Preview Map Snapshot via Google

1. add_place화면에 내 위치 확인과, 구글맵을 연동시켜보자.

 

다트 패키지에서 location과, http 를 다운 받아준다.

특히 location은 ios같은 경우 추가 세팅이 필요하니, 상세 내용을 잘 보자.

 

google map api를 사용하기 위해서 아래 사이트에서 계정 가입(vinaarba@gmail.com)을 하고, 데이터 전송 시 필요한, 키 값을 받아낸다.

https://developers.google.com/maps?hl=ko

 

Google Maps Platform  |  Google for Developers

Google Maps Platform 설명

developers.google.com

 

키 값은 여기서 확인 가능

 

 

 

2. 지오코딩 (Geocoding)과 map static api 이해

 

일반적으로 지오코딩이라는 용어는 사람이 읽을 수 있는 주소를 지도상의 위치로 변환하는 과정을 나타냅니다. 그 반대로 지도상의 위치를 사람이 읽을 수 있는 주소로 변환하는 과정을 역 지오코딩이라고 합니다.

 

그리고, map static api는 현재 위치 데이터를 기반으로 구글지도에 스냅샷으로 보여주는 api이다.

 

https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding?hl=ko

 

역 지오코딩 (주소 조회) 요청 및 응답  |  Geocoding API  |  Google for Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 의견 보내기 역 지오코딩 (주소 조회) 요청 및 응답 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분

developers.google.com

 

https://developers.google.com/maps/documentation/maps-static/overview?hl=ko

 

개요  |  Maps Static API  |  Google for Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 의견 보내기 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 시작하기 전에: Maps Stat

developers.google.com

 

 

 

 

- models/place.dart

import 'dart:io';

import 'package:uuid/uuid.dart';

const uuid = Uuid();

class PlaceLocation {
  const PlaceLocation({
    required this.latitude,
    required this.longitude,
    required this.address,
  });

  final double latitude;
  final double longitude;
  final String address;
}

class Place {
  Place({
    required this.title,
    required this.image,
    // required this.location,
  }) : id = uuid.v4();

  final String id;
  final String title;
  final File image;
  // final PlaceLocation location;
}

1) 모델에 PlaceLocation 클래스를 추가해주고 이를 place 값으로 연동해줄 예정이다. (해당 강의에서는 기본 에뮬레이터 디폴트 값 주소로 찍힐 예정). 위도, 경도, 주소값이 들어간다

 

- add_place.dart

import 'dart:io';

import 'package:favorite_places/widgets/location_input.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';

import 'package:favorite_places/widgets/image_input.dart';
import 'package:favorite_places/providers/user_places.dart';

class AddPlaceScreen extends ConsumerStatefulWidget {
  const AddPlaceScreen({super.key});

  @override
  ConsumerState<AddPlaceScreen> createState() {
    return _AddPlaceScreenState();
  }
}

class _AddPlaceScreenState extends ConsumerState<AddPlaceScreen> {
  final _titleController = TextEditingController();
  File? _selectedImage;

  void _savePlace() {
    final enteredTitle = _titleController.text;

    if (enteredTitle.isEmpty || _selectedImage == null) {
      return;
    }

    ref
        .read(userPlacesProvider.notifier)
        .addPlace(enteredTitle, _selectedImage!);

    Navigator.of(context).pop();
  }

  @override
  void dispose() {
    _titleController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Add new Place'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(12),
        child: Column(
          children: [
            TextField(
              decoration: const InputDecoration(labelText: 'Title'),
              controller: _titleController,
              style: TextStyle(
                color: Theme.of(context).colorScheme.onBackground,
              ),
            ),
            const SizedBox(height: 10),
            ImageInput(
              onPickImage: (image) {
                _selectedImage = image;
              },
            ),
            const SizedBox(height: 10),
            const LocationInput(),
            const SizedBox(height: 16),
            ElevatedButton.icon(
              onPressed: _savePlace,
              icon: const Icon(Icons.add),
              label: const Text('Add Place'),
            ),
          ],
        ),
      ),
    );
  }
}

 

1) LocationInput 위젯 추가

 

- location_input.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:location/location.dart';
import 'package:http/http.dart' as http;

import 'package:favorite_places/models/place.dart';

class LocationInput extends StatefulWidget {
  const LocationInput({super.key});

  @override
  State<LocationInput> createState() {
    return _LocationInputState();
  }
}

class _LocationInputState extends State<LocationInput> {
  PlaceLocation? _pickedLocation;
  var _isGettingLocation = false;

  String get locationImage {
    if (_pickedLocation == null) {
      return '';
    }
    final lat = _pickedLocation!.latitude;
    final lng = _pickedLocation!.longitude;
    return 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng=&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7Clabel:A%7C$lat,$lng&key=AIzaSyDLcwxUggpPZo8lcbH0TB4Crq5SJjtj4ag';
  }

  void _getCurrentLocation() async {
    Location location = Location();

    bool serviceEnabled;
    PermissionStatus permissionGranted;
    LocationData locationData;

    serviceEnabled = await location.serviceEnabled();
    if (!serviceEnabled) {
      serviceEnabled = await location.requestService();
      if (!serviceEnabled) {
        return;
      }
    }

    permissionGranted = await location.hasPermission();
    if (permissionGranted == PermissionStatus.denied) {
      permissionGranted = await location.requestPermission();
      if (permissionGranted != PermissionStatus.granted) {
        return;
      }
    }

    setState(() {
      _isGettingLocation = true;
    });

    locationData = await location.getLocation();
    final lat = locationData.latitude;
    final lng = locationData.longitude;

    if (lat == null || lng == null) {
      return;
    }

    final url = Uri.parse(
        'https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lng&key=AIzaSyDLcwxUggpPZo8lcbH0TB4Crq5SJjtj4ag');
    final response = await http.get(url);
    final resData = json.decode(response.body);
    final address = resData['results'][0]['formatted_address'];

    setState(() {
      _pickedLocation = PlaceLocation(
        latitude: lat,
        longitude: lng,
        address: address,
      );
      _isGettingLocation = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    Widget previewContent = Text(
      'No location chosen',
      textAlign: TextAlign.center,
      style: Theme.of(context).textTheme.bodyLarge!.copyWith(
            color: Theme.of(context).colorScheme.onBackground,
          ),
    );

    if (_pickedLocation != null) {
      previewContent = Image.network(
        locationImage,
        fit: BoxFit.cover,
        width: double.infinity,
        height: double.infinity,
      );
    }

    if (_isGettingLocation) {
      previewContent = const CircularProgressIndicator();
    }

    return Column(
      children: [
        Container(
          height: 170,
          width: double.infinity,
          alignment: Alignment.center,
          decoration: BoxDecoration(
            border: Border.all(
              width: 1,
              color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
            ),
          ),
          child: previewContent,
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            TextButton.icon(
              icon: const Icon(Icons.location_on),
              label: const Text('Get Current Location'),
              onPressed: _getCurrentLocation,
            ),
            TextButton.icon(
              icon: const Icon(Icons.map),
              label: const Text('Select on Map'),
              onPressed: () {},
            ),
          ],
        ),
      ],
    );
  }
}

1) 먼저 _getCurrentLocation 함수 부분을 분석해보자.

Location location = Location();

    bool serviceEnabled;
    PermissionStatus permissionGranted;
    LocationData locationData;

    serviceEnabled = await location.serviceEnabled();
    if (!serviceEnabled) {
      serviceEnabled = await location.requestService();
      if (!serviceEnabled) {
        return;
      }
    }

    permissionGranted = await location.hasPermission();
    if (permissionGranted == PermissionStatus.denied) {
      permissionGranted = await location.requestPermission();
      if (permissionGranted != PermissionStatus.granted) {
        return;
      }
    }

    setState(() {
      _isGettingLocation = true;
    });

--> 할당값 선언 및 권한 승인 부분이고, 모든게 끝나면 setState를 통해 isGettingLocation 값을 true로 변경 (로딩 모양을 위해)

locationData = await location.getLocation();
    final lat = locationData.latitude;
    final lng = locationData.longitude;

    if (lat == null || lng == null) {
      return;
    }

    final url = Uri.parse(
        'https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lng&key=AIzaSyDLcwxUggpPZo8lcbH0TB4Crq5SJjtj4ag');
    final response = await http.get(url);
    final resData = json.decode(response.body);
    final address = resData['results'][0]['formatted_address'];

    setState(() {
      _pickedLocation = PlaceLocation(
        latitude: lat,
        longitude: lng,
        address: address,
      );
      _isGettingLocation = false;
    });
  }

--> getLocation을 통해 현재 기기의 위도latitude, 경도longtitude 값을 받아온다.

해당 값이 null이 아니면 api 값에 lat, lng 값과 key값을 넣고, 주소 값을 받아온다.resonse.body값에는 다양한 값이 있고, 그중에 formatted_address가 우리가 쓸 정보임.

그리고 setState를 통해 _pickLocation에 PlaceLocation 모델값을 할당해준다. 그리고 값을 받아왔기 때문에 _isGettingLocation값을 false로 바꿔준다.

 

그리고 끝으로, 빌드 부분에 컨테이너 child에는 previewContent가 들어가는데, 이때 _pickedLocation 값이 있다면, Image.network를 통해 get 함수인 locationImage 리턴 값을 사용 할 수 있다.

 

get 함수 locationImage는 

 

  String get locationImage {
    if (_pickedLocation == null) {
      return '';
    }
    final lat = _pickedLocation!.latitude;
    final lng = _pickedLocation!.longitude;
    return 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng=&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7Clabel:A%7C$lat,$lng&key=AIzaSyDLcwxUggpPZo8lcbH0TB4Crq5SJjtj4ag';
  }
 

static map api 를 활용하여, 필요 정보 부분에 위도 값과 경도 값을 넣어주면 된다. (기타 뒤에 칼라나, zoom 정도를 바꿀 수 있음)

 

- 결과 화면

get current location 버튼 누르기 전

 

 

위치 값은 디폴트로 구글 본사로 되어있음.