【Flutter】GoogleMapを使用したアプリの開発について Part3

flutter

入力候補の取得について

前々回紹介したアプリでは、住所やランドマーク名などで検索する際に入力内容から検索先の候補を取得する処理を実装しています。
そして、その候補を元に、対象の位置を中心とした地図表示を行なっています。
今回はこちらの処理に関する内容を紹介したいと思います。

APIの追加について

入力候補に関する内容はPlaces APIというAPIを使用しています。
これはGoogleMapのSDKには含まれていませんので、新たに使用のための準備があります。
https://developers.google.com/maps/documentation/places/web-service/overview

前々回に作成したGoogleMapPLatformのプロジェクトに使用するAPIを紐づけます。
https://mapsplatform.google.com/intl/ja_ALL/

自身のプロジェクトから左ペインのAPIを選択し、「Places API」を選択。

「有効にする」ボタンを押下し、追加されたことを確認します。

ライブラリの追加と実装の修正について

APIはそのまま使用しても良いのですが、利便性からそれをラップしたライブラリを使用します。
有志が提供してくださるものはありがたく使用させてもらいましょう。

pubspec.yaml の dependencies にライブラリ情報を追加して、pub get します。

google_place: ^0.4.7

メインクラスは以下に変更します。
簡単な紹介にするために、コードが一部妙なものになっています。

全体はこんな感じです

import 'dart:convert';

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  Future(() async {
    LocationPermission permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      await Geolocator.requestPermission();
    }
  });
  runApp(const MyApp());
}

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

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

class _MyAppState extends State<MyApp> {
  static const String API_KYE = "取得したAPIキー";

  bool _hasPosition = false;
  late GoogleMapController mapController;
  late Position _currentPosition;
  late GooglePlace _googlePlace;
  List<AutocompletePrediction> _predictions = [];

  final TextEditingController _textEditingController = TextEditingController();

  // 初期値で日本全体を表示
  final LatLng _center = const LatLng(36.204824, 138.252924);

  // 現在地を取得しGoogleMapを表示する。
  Future<void> _getLocation() async {
    _currentPosition = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);
    setState(() {
      _hasPosition = true;
    });
  }

  void _onMapCreated(GoogleMapController controller) async {
    mapController = controller;
    await _getLocation();
  }

  Widget _createMap() {
    return GoogleMap(
      mapType: MapType.normal,
      onMapCreated: _onMapCreated,
      // 端末の位置情報を使用する。
      myLocationEnabled: true,
      // 端末の位置情報を地図の中心に表示するボタンを表示する。
      myLocationButtonEnabled: true,
      initialCameraPosition: CameraPosition(target: _center, zoom: 4),
    );
  }

    // 入力内容から自動補完した結果を取得する。
  Future<void> autoCompleteSearch(String value) async {
    _googlePlace = GooglePlace(API_KYE);
    final result = await _googlePlace.autocomplete.get(value, language: "ja");
    if (result != null && result.predictions != null) {
      setState(() {
        _predictions = result.predictions!;
      });
    }
  }

    // PlaceIDから画面表示する範囲を取得する。
  Future<void> _getTargetLatLng(String? placeId) async {
        // ここでもAPIキーを使用する。
    String requestUrl =
        'https://maps.googleapis.com/maps/api/place/details/json?language=ja&place_id=${placeId}&key=${API_KYE}';
    http.Response? response;
    response = await http.get(Uri.parse(requestUrl));

    if (response.statusCode == 200) {
      final res = jsonDecode(response.body);
      var northEast = res['result']['geometry']['viewport']['northeast'];
      var southWest = res['result']['geometry']['viewport']['southwest'];
      double northEastLat = double.parse(northEast['lat'].toString());
      double northEastLng = double.parse(northEast['lng'].toString());
      double southWestLat = double.parse(southWest['lat'].toString());
      double southWestLng = double.parse(southWest['lng'].toString());
      setState(() {
        LatLngBounds bounds = LatLngBounds(
            southwest: LatLng(southWestLat, southWestLng),
            northeast: LatLng(northEastLat, northEastLng));
        mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 30.0));
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Maps Sample App'),
          backgroundColor: Colors.green[700],
        ),
        body: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(4.0),
              child: TextFormField(
                decoration: const InputDecoration(
                  border: OutlineInputBorder(
                    borderSide: BorderSide(
                      color: Colors.blueAccent,
                    ),
                  ),
                  labelText: "Address or Landmark",
                ),
                controller: _textEditingController,
                onChanged: (value) {
                  if (_hasPosition) {
                    setState(() => _hasPosition = false);
                  }
                  if (value.isNotEmpty) {
                    autoCompleteSearch(value);
                  } else {
                    if (_predictions.isNotEmpty) {
                      setState(() {
                        _predictions = [];
                      });
                    }
                  }
                },
              ),
            ),
            Expanded(
              child: Stack(children: [
                _createMap(),
                if (!_hasPosition) _predictCards(),
              ]),
            ),
          ],
        ),
      ),
    );
  }

  Widget _predictCards() {
    return SingleChildScrollView(
      child: ListView.builder(
        shrinkWrap: true,
        itemCount: _predictions.length, // 検索結果を格納したpredictions配列の長さを指定
        itemBuilder: (context, index) {
          return Card(
            child: ListTile(
              title: Text(_predictions[index].description.toString()),
              onTap: () {
                _getTargetLatLng(_predictions[index].placeId);
                setState(() {
                  _hasPosition = true;
                });
              },
            ),
          );
        },
      ),
    );
  }
}

入力した内容を受け取って候補を取得する部分はここです。
前々回発行したAPIキーはここでも使用します。

    // 入力内容から自動補完した結果を取得する。
  Future<void> autoCompleteSearch(String value) async {
    _googlePlace = GooglePlace(API_KYE);
    final result = await _googlePlace.autocomplete.get(value, language: "ja");
    if (result != null && result.predictions != null) {
      setState(() {
        _predictions = result.predictions!;
      });
    }
  }

実行するとまずは日本全体が表示され、検索欄に文字を入力すると自動補完・予測変換された結果が表示されます。

「とうきょう」と入力していきますが、入力の度に候補が変わっていきます。

PlaceIDから緯度経度の取得について

入力候補の結果にはターゲット地点の位置を示す緯度経度が含まれていません。

その代わり、PlaceIDなるものが付与されているので、そこから緯度経度を取得する別のAPIを使用します。

    // PlaceIDから画面表示する範囲を取得する。
  Future<void> _getTargetLatLng(String? placeId) async {
        // APIのURIでAPIキーを使用する。
    String requestUrl =
        'https://maps.googleapis.com/maps/api/place/details/json?language=ja&place_id=${placeId}&key=${API_KYE}';
    http.Response? response;
    response = await http.get(Uri.parse(requestUrl));

    if (response.statusCode == 200) {
      final res = jsonDecode(response.body);
      var northEast = res['result']['geometry']['viewport']['northeast'];
      var southWest = res['result']['geometry']['viewport']['southwest'];
      double northEastLat = double.parse(northEast['lat'].toString());
      double northEastLng = double.parse(northEast['lng'].toString());
      double southWestLat = double.parse(southWest['lat'].toString());
      double southWestLng = double.parse(southWest['lng'].toString());
      setState(() {
        LatLngBounds bounds = LatLngBounds(
            southwest: LatLng(southWestLat, southWestLng),
            northeast: LatLng(northEastLat, northEastLng));
        mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 30.0));
      });
    }
  }

こちらはAPIを直に叩いて取得しています。
ここでもAPIキーが必要になります。
※取得したいのはターゲット地点の緯度経度ですが、対象の大きさにより表示したいズーム加減は異なるため、ここでは表示する範囲の右上と左下の緯度経度を指定してます。

これで検索結果のPlaceIDを中心に表示する動作となりました。
先ほどの続きで「とうきょうえ」まで入力して、候補に表示されている「東京駅」を選択した場合です。

地図表示について

こちらも前回と同様、表示の際にはいろいろな設定できます。
今回は、表示する位置を変更する際に、アニメーションで動かすものにしています。
検索結果にはその場所のレビューなど、GoogleMapで検索した時に表示されるデータがありますので用途に合わせていろいろできると思います。

今回は以上です。
ありがとうございました。

関連記事

  1. 【Widget of the Week】#10 Table

  2. 【Widget of the Week】#2 Expanded

  3. 【予告】Widget of the Week 解説スタート!

  4. flutter

    【Flutter】Flutter実装メモ  enumの拡張

  5. flutter

    【Flutter】開発環境構築(Flutter,VSCode,Wind…

  6. flutter

    【Flutter】Pankoに「どこでもあみだくじ」機能を追加

最近の記事

  1. AWS
  2. AWS
  3. AWS
  4. AWS
  5. AWS
  6. AWS

制作実績一覧

  1. Checkeys