본문 바로가기

Flutter/04 Widgets

[Flutter] Widgets - Google map 5

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

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

소스코드 위치 - https://github.com/mike-bskim/google_map_test/releases/tag/09_refactoring_1

 

Release 09_refactoring_1 · mike-bskim/google_map_test

 

github.com

 

 

 

오늘은 지난 블로그에서 화면 초기 로딩 부분을 수정하였습니다.

_goToCurrentPosition 함수를 이용해서 초기 더미 position 에서 현재 자신의 위치 정보를 가져와서 이동하게 초기화면을 처리했는데, 이 부분을 CircularProgressIndicator() 를 이용하여 조금 더 자연스럽게 처리하였습니다.

 

// nullable 로 처리하기 위해서 선언 부분 수정.
Position? position;

position 변수와 관련된 모든 모든 부분을 position! 으로 처리함.

 

 

late Position position;

// late 로 처리하면 아래 오류가 발생함.

======== Exception caught by widgets library =======================================================
The following LateError was thrown building AutocompleteLocationPage(dirty, state: AutocompleteLocationPageState#fa8b2):
LateInitializationError: Field 'position' has not been initialized.

 

 

autocomplete_location_page.dart

 

import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:geolocator/geolocator.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 AutocompleteLocationPage extends StatefulWidget {
  const AutocompleteLocationPage({Key? key}) : super(key: key);

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

class AutocompleteLocationPageState extends State<AutocompleteLocationPage> {
  final TextEditingController _searchController = TextEditingController();
  var uuid = const Uuid();
  late String sessionToken;
  var googleMapServices = GoogleMapServices(sessionToken: const Uuid().v4());
  PlaceDetail? placeDetail;
  final Completer<GoogleMapController> _completeController = Completer();
  final Set<Marker> _markers = {};

  // 더미 객체 생성로 생성하지 않고 nullable 로 처리
  Position? position;

  // Position position = Position(
  //     longitude: 127.1189054,
  //     latitude: 37.382782,
  //     timestamp: DateTime.now(),
  //     accuracy: 0.0,
  //     altitude: 0.0,
  //     heading: 0.0,
  //     speed: 0.0,
  //     speedAccuracy: 0.0);
  double distance = 0.0;
  String myAddress = '';

  @override
  void initState() {
    super.initState();
    sessionToken = uuid.v4();
    _checkGPSAvailability();
  }

  void _checkGPSAvailability() async {
// gps 사용 허가 확인
    LocationPermission geolocationStatus = await Geolocator
        .checkPermission(); // Geolocator().checkGeolocationPermissionStatus
    debugPrint('geolocationStatus: [$geolocationStatus]');

    // GeolocationStatus.granted
    if (geolocationStatus == LocationPermission.denied ||
        geolocationStatus == LocationPermission.deniedForever) {
      showDialog(
        // 다이얼로그 밖 영역 클릭시 사라지지 않게 처리
        barrierDismissible: false,
        context: context,
        builder: (ctx) {
          return AlertDialog(
            title: const Text('GPS 사용 불가'),
            content: const Text('GPS 사용 불가로 앱을 사용할 수 없습니다'),
            actions: <Widget>[
              ElevatedButton(
                child: const Text('OK'),
                onPressed: () {
                  Navigator.pop(ctx);
                },
              ),
            ],
          );
        },
      ).then((_) => Navigator.pop(context));
    } else {
      await _getGPSLocation();
      myAddress = await GoogleMapServices.getAddrFromLocation(
          position!.latitude, position!.longitude);
      _setMyLocation();
      // _goToCurrentPosition(position!.latitude, position!.longitude);
    }
  }

// 현재 내 위치 리턴
  Future<void> _getGPSLocation() async {
    // Geolocator().getCurrentPosition()
    position = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);
  }

// 인자로 전달한 위도/경도 정보를 중심으로 지도 이동
  Future<void> _goToCurrentPosition(double lat, double lng) async {
    final GoogleMapController controller = await _completeController.future;
    controller.animateCamera(
      // CameraUpdate.newCameraPosition(
      //     CameraPosition(target: LatLng(lat, lng), zoom: 14)),
      CameraUpdate.newLatLng(LatLng(lat, lng)),
    );
  }

// _markers 에 위치 정보 추가
  void _setMyLocation() {
    setState(() {
      _markers.add(Marker(
        markerId: const MarkerId('myInitPosition'),
        position: LatLng(position!.latitude, position!.longitude),
        infoWindow: InfoWindow(title: '내 위치', snippet: myAddress),
      ));
    });
  }

// 검색 결과를
  void _moveCamera() async {
    if (_markers.isNotEmpty) {
      setState(() {
        _markers.clear();
      });
    }

    // 검색 결과 위치로 이동
    GoogleMapController controller = await _completeController.future;
    controller.animateCamera(
      CameraUpdate.newLatLng(
        LatLng(placeDetail!.lat, placeDetail!.lng),
      ),
    );

    // 내 위치의 위도/경도 정보 추출
    await _getGPSLocation();
    // 내 위도/경도를 기반으로 주소명 추출
    myAddress = await GoogleMapServices.getAddrFromLocation(
        position!.latitude, position!.longitude);

    // 내위치와 검색위치 사이의 거리 계산
    distance = Geolocator.distanceBetween(position!.latitude,
        position!.longitude, 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,
          ),
        ),
      );
    });
  }

  // 검색결과의 상세 정보 화면 출력
  Widget _showPlaceInfo() {
    if (placeDetail == null) {
      return Container();
    }
    return Column(
      children: <Widget>[
        Card(
          child: ListTile(
            title: Text('내 위치: $myAddress - ${placeDetail!.name}'),
            subtitle: Text('${distance.toStringAsFixed(2)} m'),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.branding_watermark),
            title: Text(placeDetail!.name),
            visualDensity: const VisualDensity(vertical: -1),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.location_city),
            title: Text(placeDetail!.formattedAddress),
            visualDensity: const VisualDensity(vertical: -1),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.phone),
            title: Text(placeDetail!.formattedPhoneNumber),
            visualDensity: const VisualDensity(vertical: -1),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.favorite),
            title: Text('${placeDetail!.rating}'),
            visualDensity: const VisualDensity(vertical: -1),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.place),
            title: Text(placeDetail!.vicinity),
            visualDensity: const VisualDensity(vertical: -1),
          ),
        ),
        Card(
          child: ListTile(
            leading: const Icon(Icons.web),
            title: Text(placeDetail!.website),
            visualDensity: const VisualDensity(vertical: -1),
          ),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      // 키보드 숨기기
      onTap: () => FocusScope.of(context).unfocus(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Places Autocomplete & distance'),
        ),
        body: position == null
            ? const Center(child: CircularProgressIndicator())
            : Padding(
                padding: const EdgeInsets.symmetric(
                    horizontal: 16.0, vertical: 16.0),
                child: Column(
                  children: <Widget>[
                    SizedBox(
                      height: 25.0,
                      child: Image.asset('assets/images/powered_by_google.png'),
                    ),
                    // 검색어와 관련이 높은 검색 결과 표시
                    TypeAheadField(
                      // 0.5초 동안 입력변화가 없으면 suggestionsCallback 실행
                      debounceDuration: const Duration(milliseconds: 500),
                      textFieldConfiguration: TextFieldConfiguration(
                        style: const TextStyle(fontSize: 12),
                        controller: _searchController,
                        autofocus: true,
                        decoration: const InputDecoration(
                            contentPadding: EdgeInsets.all(8.0),
                            border: OutlineInputBorder(),
                            hintText: 'Search places...'),
                      ),
                      // 검색어(pattern)를 이용하여 유사 결과 제안
                      suggestionsCallback: (pattern) async {
                        if (sessionToken.isEmpty) {
                          // == null, 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.description),
                          subtitle: Text(temp.name),
                        );
                      },
                      // suggestion 의 타입 캐스팅을 안하면 아래처럼 직접 캐스팅 후 접근 가능
                      onSuggestionSelected: (suggestion) async {
                        placeDetail = await googleMapServices.getPlaceDetail(
                          (suggestion as Place).placeId,
                          sessionToken,
                        );
                        sessionToken = '';
                        _moveCamera();
                      },
                    ),
                    const SizedBox(height: 20),
                    SizedBox(
                      width: double.infinity,
                      height: 250,
                      child: GoogleMap(
                        mapType: MapType.normal,
                        initialCameraPosition: CameraPosition(
                          target: LatLng(
                            position!.latitude, position!.longitude,
                            // 37.382782,
                            // 127.118905,
                          ),
                          zoom: 14,
                        ),
                        onMapCreated: (GoogleMapController controller) {
                          _completeController.complete(controller);
                        },
                        myLocationEnabled: true,
                        markers: _markers,
                      ),
                    ),
                    const SizedBox(height: 20),
                    Expanded(
                        child: SingleChildScrollView(child: _showPlaceInfo())),
                  ],
                ),
              ),
      ),
    );
  }
}