【Flutter×Google Maps】Cardメニューで位置情報をGoogle Mapsに連携しナビを開く
概要
Flutterアプリで地図を表示し、特定の地点に応じたCardメニューを表示することがあるかと思います。今回は、Cardメニュー内のボタンを押すと、その地点や現在地からのルート案内をGoogle Mapsアプリで直接開ける仕組みを実装してみようと思います。
動作環境
- Mac Book Air M3 24GB / macOS: 14.6.1
- iOSシュミレータ: iPhone 15 Pro / iOS: 17.4
- Androidシュミレータ: Android API VanillalceCream arm64-v8a
セットアップ
fvmを使ってプロジェクト作成していきます。
mkdir google_map_flutter_sample
cd google_map_flutter_sample
fvm use 3.24.3 --force
fvm flutter create .
次に必要なパッケージを追加します。 pubspec.yaml
に以下を追加します。
dependencies:
google_maps_flutter: ^2.9.0
url_launcher: ^6.3.1
Google Maps APIのキーの取得や設定を行います。↓の記事参照
次に単にMapを表示するだけの画面を実装し表示できればOKです。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class CardNaviMap extends StatefulWidget {
const CardNaviMap({super.key});
State<CardNaviMap> createState() => _CardNaviMapState();
}
class _CardNaviMapState extends State<CardNaviMap> {
final Completer<GoogleMapController> _controller =
Completer<GoogleMapController>();
static const CameraPosition _initialCameraPosition = CameraPosition(
target: LatLng(35.68123428932672, 139.76714355230686),
zoom: 12.0,
);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Card Navigation Map')),
body: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _initialCameraPosition,
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
),
);
}
}
サンプルデータ表示
次に実際のアプリで使われるようなEntityクラスを作成します。今回は以下のようなクラスにしました。
class Point {
int id;
double latitude;
double longitude;
String name;
Point(this.id, this.latitude, this.longitude, this.name);
}
このクラスを使ったサンプルデータを定義します。
final List<Point> samplePoints = [
Point(1, 35.6586, 139.7454, '東京タワー'),
Point(2, 35.7100, 139.8107, '東京スカイツリー'),
Point(3, 35.6764, 139.6993, '新宿御苑'),
Point(4, 35.6605, 139.7297, '六本木ヒルズ'),
Point(5, 35.6986, 139.7731, '秋葉原'),
Point(6, 35.6553, 139.7630, '浜離宮恩賜庭園'),
Point(7, 35.6595, 139.7004, '渋谷スクランブル交差点'),
Point(8, 35.7170, 139.7745, '上野動物園'),
Point(9, 35.6718, 139.6946, '代々木公園'),
Point(10, 35.6852, 139.7528, '皇居'),
];
これをMarkerとしてMapに表示させます。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Card Navigation Map')),
body: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _initialCameraPosition,
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
markers: samplePoints // ←追加
.map((point) => Marker(
markerId: MarkerId(point.id.toString()),
position: LatLng(point.latitude, point.longitude),
))
.toSet(),
),
);
}
↓の様にMakerが表示されていればOKです。
Cardメニュー表示
続いて現在選択されているMakerのCardメニューを表示させるサンプルを作成したいと思います。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class Point {
int id;
double latitude;
double longitude;
String name;
Point(this.id, this.latitude, this.longitude, this.name);
}
final List<Point> samplePoints = [
Point(1, 35.6586, 139.7454, '東京タワー'),
Point(2, 35.7100, 139.8107, '東京スカイツリー'),
Point(3, 35.6764, 139.6993, '新宿御苑'),
Point(4, 35.6605, 139.7297, '六本木ヒルズ'),
Point(5, 35.6986, 139.7731, '秋葉原'),
Point(6, 35.6553, 139.7630, '浜離宮恩賜庭園'),
Point(7, 35.6595, 139.7004, '渋谷スクランブル交差点'),
Point(8, 35.7170, 139.7745, '上野動物園'),
Point(9, 35.6718, 139.6946, '代々木公園'),
Point(10, 35.6852, 139.7528, '皇居'),
];
class CardNaviMap extends StatefulWidget {
const CardNaviMap({super.key});
State<CardNaviMap> createState() => _CardNaviMapState();
}
class _CardNaviMapState extends State<CardNaviMap> {
final Completer<GoogleMapController> _controller =
Completer<GoogleMapController>();
static const CameraPosition _initialCameraPosition = CameraPosition(
target: LatLng(35.68123428932672, 139.76714355230686),
zoom: 12.0,
);
final _pageController = PageController(
viewportFraction: 0.85,
);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Card Navigation Map')),
body: Stack(
alignment: Alignment.bottomCenter,
children: [
_map(),
_cards(),
],
),
);
}
Widget _map() {
return GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _initialCameraPosition,
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
markers: samplePoints
.map((point) => Marker(
markerId: MarkerId(point.id.toString()),
position: LatLng(point.latitude, point.longitude),
onTap: () async {
final index =
samplePoints.indexWhere((p) => p.id == point.id);
_pageController.jumpToPage(index);
},
))
.toSet(),
);
}
Widget _cards() {
return Container(
height: 148,
padding: const EdgeInsets.fromLTRB(0, 0, 0, 20),
child: PageView(
onPageChanged: (int index) async {
final selectedPoint = samplePoints.elementAt(index);
final GoogleMapController controller = await _controller.future;
final zoomLevel = await controller.getZoomLevel();
controller.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(selectedPoint.latitude, selectedPoint.longitude),
zoom: zoomLevel,
),
),
);
},
controller: _pageController,
children: _tiles(),
),
);
}
List<Widget> _tiles() {
return samplePoints.map(
(point) {
return Card(
child: SizedBox(
height: 100,
child: Center(
child: Text(point.name),
),
),
);
},
).toList();
}
}
こちらを実行すると以下の様な挙動になります。
Google Mapアプリを開く
今度はCardメニュー内のボタンを押すと、その地点をGoogle Mapアプリで開ける様にしたいと思います。
_launch
を追加し _tiles
を以下に修正します。
List<Widget> _tiles() {
return samplePoints.map(
(point) {
return Card(
child: SizedBox(
height: 100,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(point.name),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
final uri = Uri.parse(
"https://www.google.com/maps/search/?api=1&query=${point.latitude},${point.longitude}");
await _launch(context, uri);
},
child: const Text('GoogleMapで表示'),
),
],
),
),
);
},
).toList();
}
Future<void> _launch(BuildContext context, Uri uri) async {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
-
iOSのシュミレータの場合
-
Androidシュミレータの場合 (Google Mapインストール)
※ Google Maps URLs を開く場合ケース毎に挙動が異なります。詳細はこちら👇
現在地から対象の地点までのナビをGoogle Mapアプリで開く
今度はCardメニュー内のボタンを押すと、現在地からその地点までのナビがGoogle Mapアプリで開くようにしてみたいと思います。
_tiles
を以下に修正します。
List<Widget> _tiles() {
return samplePoints.map(
(point) {
return Card(
child: SizedBox(
height: 100,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(point.name),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
final uri = Uri.parse(
"https://www.google.com/maps/search/?api=1&query=${point.latitude},${point.longitude}");
await _launch(context, uri);
},
child: const Text('GoogleMapで表示'),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () async {
final uri = Uri.parse(
"https://www.google.com/maps/dir/?api=1&destination=${point.latitude},${point.longitude}");
await _launch(context, uri);
},
child: const Text('ナビ表示'),
),
],
)
],
),
),
);
},
).toList();
}
※ 現在地は東京駅に設定しています。
-
iOSシュミレータの場合
-
Androidシュミレータの場合 (Google Mapインストール)
参考URL
Discussion