본문 바로가기

Flutter/04 Widgets

[Flutter] Widgets - Google map 3(Place autocomplete)

이번 카테고리는 google map 을 사용하는 방법에 대해서 알아보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

소스코드 위치 - Release 07_place_autocomplete2 · mike-bskim/google_map_test · GitHub

 

Release 07_place_autocomplete2 · mike-bskim/google_map_test

 

github.com

 

 

화면은 아래와 같습니다.

 

 

이전 블로그에서 설명하지 않은 리팩토링을 포함하여 기능이 추가된 부분을 설명하겠습니다.

약간 혼란이 있을수 있습니다.

프로젝트 구조는 소스코드를 참고하시고, 블로그에서는 기본 기능을 중심으로 참고하시면 좋을것 같습니다.

 

 

main.dart

 

import 'package:flutter/material.dart';

import 'pages/place_autocomplete.dart';
import 'pages/places_nearby.dart';
import 'widgets/custom_button.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Google Maps Demo',
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Maps'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            CustomButton(
              title: 'Places Nearby',
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) => const PlacesNearby()),
                );
              },
            ),
            const SizedBox(height: 20),
            CustomButton(
              title: 'Place Autocomplete',
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) {
                    return const PlaceAutocomplete();
                  }),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

 

 

place.dart

 

class Place {
  final String description;
  final String placeId;
  final String name;

  Place({
    required this.description,
    required this.placeId,
    required this.name,
  });

  // Place.fromJson(Map<String, dynamic> json)
  //     : description = json['description'],
  //       placeId = json['place_id'];
  //
  factory Place.fromJson(Map<String, dynamic> json) => Place(
        description: json['description'],
        placeId: json['place_id'],
        name: json['structured_formatting']['main_text'],
      );

  Map<String, dynamic> toMap() {
    return {
      'description': description,
      'placeId': placeId,
    };
  }
}

class PlaceDetail {
  final String placeId;
  final String formattedAddress;
  final String formattedPhoneNumber;
  final String name;
  final double rating;
  final String vicinity;
  final String website;
  final double lat;
  final double lng;

  PlaceDetail({
    required this.placeId,
    required this.formattedAddress,
    required this.formattedPhoneNumber,
    required this.name,
    required this.rating,
    required this.vicinity,
    this.website = '',
    required this.lat,
    required this.lng,
  });

  PlaceDetail.fromJson(Map<String, dynamic> json)
      : placeId = json['place_id'] ?? '',
        formattedAddress = json['formatted_address'] ?? '',
        formattedPhoneNumber = json['formatted_phone_number'] ?? '',
        name = json['name'] ?? '',
        rating = json['rating'].toDouble() ?? 0.0,
        vicinity = json['vicinity'] ?? '',
        website = json['website'] ?? '',
        lat = json['geometry']['location']['lat'] ?? 0.0,
        lng = json['geometry']['location']['lng'] ?? 0.0;

  Map<String, dynamic> toMap() {
    return {
      'placeId': placeId,
      'formateedAddress': formattedAddress,
      'formateedPhoneNumber': formattedPhoneNumber,
      'name': name,
      'rating': rating,
      'vicinity': vicinity,
      'website': website,
      'lat': lat,
      'lng': lng,
    };
  }
}

 

 

place_autocomplete.dart

 

import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:uuid/uuid.dart';
import 'dart:async';

import '../models/place.dart';
import '../services/google_map_service.dart';

class PlaceAutocomplete extends StatefulWidget {
  const PlaceAutocomplete({Key? key}) : super(key: key);

  @override
  PlaceAutocompleteState createState() => PlaceAutocompleteState();
}

class PlaceAutocompleteState extends State<PlaceAutocomplete> {
  final TextEditingController _searchController = TextEditingController();
  var uuid = const Uuid();
  // 패키지 버전 및 null safety 업데이트로 인해서 변수 초기화 절차가 달라졌다
  // 몇몇 설정은 오류는 없어도 경고문구가 발생해서 추가한 경우도 있음
  late String sessionToken;
  // googleMapServices, 초기에 선언만하면 나중에 인스턴스를 할당할때 하위함수들을 못찾아서 여기서 할당함.
  // googleMapServices, 선언만 해도 동작하는데 문제가 없음
  var googleMapServices = GoogleMapServices(sessionToken: const Uuid().v4());
  // PlaceDetail?, null 가능하게 처리하여 이후 변수사용시 ! 사용하여 에러처리함
  PlaceDetail? placeDetail;
  final Completer<GoogleMapController> _completeController = Completer();
  final Set<Marker> _markers = {}; //Set();

  @override
  void initState() {
    super.initState();
    // 초기 세션을 미리할때 할당해서 오류 방지
    sessionToken = uuid.v4();
    _markers.add(const Marker(
      markerId: MarkerId('myInitialPosition'),
      position: LatLng(37.382782, 127.1189054),
      infoWindow: InfoWindow(title: 'My Position', snippet: 'Where am I?'),
    ));
  }

// 새로 받은 상세정보를 마커에 추가하고 지도위치도 새로 갱신
  void _moveCamera() async {
    if (_markers.isNotEmpty) {
      setState(() {
        _markers.clear();
      });
    }

    GoogleMapController controller = await _completeController.future;
    controller.animateCamera(
      CameraUpdate.newLatLng(
        LatLng(placeDetail!.lat, placeDetail!.lng),
      ),
    );

    setState(() {
      _markers.add(
        Marker(
          markerId: MarkerId(placeDetail!.placeId),
          position: LatLng(placeDetail!.lat, placeDetail!.lng),
          infoWindow: InfoWindow(
            title: placeDetail!.name,
            snippet: placeDetail!.formattedAddress,
          ),
        ),
      );
    });
  }

// 상세 정보를 화면에 표시 with 카드 위젯
  Widget _showPlaceInfo() {
    if (placeDetail == null) {
      return Container();
    }
    return Column(
      children: <Widget>[
        Card(
          child: ListTile(
            leading: const Icon(Icons.branding_watermark),
            title: Text(placeDetail!.name),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.location_city),
            title: Text(placeDetail!.formattedAddress),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.phone),
            title: Text(placeDetail!.formattedPhoneNumber),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.favorite),
            title: Text('${placeDetail!.rating}'),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.place),
            title: Text(placeDetail!.vicinity),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.web),
            title: Text(placeDetail!.website),
          ),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Places Autocomplete'),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 32.0),
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              SizedBox(
                height: 45.0,
                child: Image.asset('assets/images/powered_by_google.png'),
              ),

              // 검색어와 관련이 높은 검색 결과 표시
              TypeAheadField(
                // TypeAheadField, TypeAheadFormField
                // 0.5초 이내 변경은 무시함
                debounceDuration: const Duration(milliseconds: 500),
                textFieldConfiguration: TextFieldConfiguration(
                  controller: _searchController,
                  autofocus: true,
                  decoration: const InputDecoration(
                      border: OutlineInputBorder(),
                      hintText: 'Search places...'),
                ),
                // 0.5초 동안 입력변화가 없으면 suggestionsCallback 실행
                // 검색어(pattern)를 이용하여 유사 결과 제안
                suggestionsCallback: (pattern) async {
                  if (sessionToken.isNotEmpty) {
                    sessionToken = uuid.v4();
                  }

                  googleMapServices =
                      GoogleMapServices(sessionToken: sessionToken);

                  // googleMapServices 을 위에서 선언과 동시에 객체를 할당하지 않으면
                  // getSuggestions 선언 위치를 찾을수 없음(Ctrl+좌클릭), 못찾는 이유는 모르겠음
                  // 객체는 바로위에서 할당을 다시 하므로 동작에는 문제가 없음,
                  return await googleMapServices.getSuggestions(pattern);
                },
                itemBuilder: (context, suggestion) {
                  // suggestion 의 타입 캐스팅을 해야 객체 변수에 접근 가능
                  var temp = suggestion as Place;
                  return ListTile(
                    title: Text(temp.name),
                    subtitle: Text(temp.description),
                  );
                },
                onSuggestionSelected: (suggestion) async {
                  // suggestion 의 타입 캐스팅을 안하면 아래처럼 직접 캐스팅 후 접근 가능
                  placeDetail = await googleMapServices.getPlaceDetail(
                    (suggestion as Place).placeId,
                    sessionToken,
                  );
                  sessionToken = ''; //null;
                  _moveCamera();
                },
              ),
              const SizedBox(height: 20),
              SizedBox(
                width: double.infinity,
                height: 350,
                child: GoogleMap(
                  mapType: MapType.normal,
                  initialCameraPosition: const CameraPosition(
                    target: LatLng(37.382782, 127.1189054),
                    // target: LatLng(placeDetail!.lat, placeDetail!.lng),
                    zoom: 14,
                  ),
                  onMapCreated: (GoogleMapController controller) {
                    _completeController.complete(controller);
                  },
                  myLocationEnabled: true,
                  markers: _markers,
                ),
              ),
              const SizedBox(height: 20),
              _showPlaceInfo(),
            ],
          ),
        ),
      ),
    );
  }
}

 

 

여기서 특이사항은 아래와 같다. 타입캐스팅을 할때와 안할때 차이가 아래처럼 나타난다.

 

 

 

 

google_map_service.dart

 

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

import '../key.dart';
import '../models/place.dart';

class GoogleMapServices {
  final String sessionToken;

  GoogleMapServices({required this.sessionToken});

// 검색어 관련 추천 결과를 리턴
  Future<List> getSuggestions(String query) async {
    const String baseUrl =
        'https://maps.googleapis.com/maps/api/place/autocomplete/json';
    String type = 'establishment';
    String url =
        '$baseUrl?input=$query&key=$googleApiKey&type=$type&language=ko&components=country:kr&sessiontoken=$sessionToken';

    debugPrint('url: $url');
    debugPrint('Autocomplete(sessionToken): $sessionToken');

    final http.Response response = await http.get(Uri.parse(url));
    final responseData = json.decode(response.body);
    final predictions = responseData['predictions'];

    List<Place> suggestions = [];

    for (int i = 0; i < predictions.length; i++) {
      final place = Place.fromJson(predictions[i]);
      suggestions.add(place);
      // debugPrint('${suggestions[i].description}, ${suggestions[i].placeId}');
      // debugPrint('${place.description}, ${place.placeId}');
    }

    return suggestions;
  }

// token 은 sessionToken 값이고, 지명 id 를 전달하여 상세 정보를 리턴
  Future<PlaceDetail> getPlaceDetail(String placeId, String token) async {
    const String baseUrl =
        'https://maps.googleapis.com/maps/api/place/details/json';
    String url =
        '$baseUrl?key=$googleApiKey&place_id=$placeId&language=ko&sessiontoken=$token';

    debugPrint('Place Detail(sessionToken): $sessionToken');
    final http.Response response = await http.get(Uri.parse(url));
    final responseData = json.decode(response.body);
    final result = responseData['result'];

    final PlaceDetail placeDetail = PlaceDetail.fromJson(result);
    // debugPrint(placeDetail.toMap().toString());

    return placeDetail;
  }


  static Future<String> getAddrFromLocation(double lat, double lng) async {
    const String baseUrl = 'https://maps.googleapis.com/maps/api/geocode/json';
    String url = '$baseUrl?latlng=$lat,$lng&key=$googleApiKey&language=ko';

    final http.Response response = await http.get(Uri.parse(url));
    final responseData = json.decode(response.body);
    final formattedAddr = responseData['results'][0]['formatted_address'];
    debugPrint(formattedAddr);

    return formattedAddr;
  }

  static String getStaticMap(double latitude, double longitude) {
    return 'https://maps.googleapis.com/maps/api/staticmap?center=&$latitude,$longitude&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7Clabel:C%7C$latitude,$longitude&key=$googleApiKey';
  }
}

 

 

구글 검색 API 를 사용시 표시해야하는 이미지 추가

 

 

 

 

 

 

[참고자료] 헤비프랜

- https://www.youtube.com/watch?v=OeR5SSBGpBs&list=PLGJ958IePUyBeZRFKmL5NrZrYi0mPnNcq&index=4 

 

'Flutter > 04 Widgets' 카테고리의 다른 글

[Flutter] Widgets - Google map 5  (0) 2022.07.03
[Flutter] Widgets - Google map 4(Geolocator)  (0) 2022.07.02
[Flutter] Widgets - Google map 2  (0) 2022.06.30
[Flutter] Widgets - Google map  (0) 2022.06.28
[Flutter] Widgets - Completer  (0) 2022.06.28