はじめに
この記事を読み終わると、以下のアプリができます。

ちなみにこの記事は、以下の記事の続編です。
上記の記事で実装したアプリに対し、以下の機能を追加していきます。
- google map上への現在地の表示機能
- google map上にHomeボタンの追加
- 取得した場所情報の一覧表示機能
- 一覧ページの複数要素でのソート機能
現在地の表示
以下の記事に従うと、現在地を表示することができるようになる。
気をつけないといけないのは、
今の実装ではStoreMap
はstatelessWidget
となっているのを現在地の有無により表示を変化させるためにstatefullWidget
にしなければいけないという点。
iOSで現在地を変更するには、以下の記事を参考にした。
記事の中ではdebag
の中にあると書いてあったが、自分のバージョンだとFeatures
の中にあった。
ホームボタンの追加
これは、StoreMapのStateの中で定義する。
理由は、MyHomeの中で定義してしまうと、地図画面と一覧画面を切り替えた際にも表示されたままになってしまうからだ。
手順
(1) GoogleMapのwidgetをscaffoldで作成するよう修正する
(2) ボタンタップ時に遷移するポジションを指定しておく
(3) floatingActionButtonをLocationをCenterにして設置する。
(4) タップ時の処理で、カメラを動かすよう記述
class _StoreMapState extends State<StoreMap> {
// 省略
static final CameraPosition _mapHome = CameraPosition(
target: LatLng(37.7763629, -122.4241918),
zoom: 13.000,
); // <- (2)
// 省略
@override
Widget build(BuildContext context) {
return
Scaffold( // <- (1)
body: GoogleMap(
// 省略
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, // <- (3)
floatingActionButton: FloatingActionButton.extended(
onPressed: _goHome, // <- (4)
label: Text('Home'),
icon: Icon(Icons.home),
),
);
}
}
今回は一旦、map上のmerkerからちょっとずらした場所をhomeに設定しておいた。
一覧表示
画面切り替えの実装
BottomNavigationBarを使い、画面切り替えを実現する。
このアプリでは、スポットの地図表記と一覧表記を実現したい。
そのページの遷移を実現するためのボタンを作ってみる。
上記ページを参考に、Map画面とCounter画面(デフォルトで作ってあるやつ)を遷移する機能をアプリに追加.
記事内では、ページ遷移の際にアニメーションを用いているが、本アプリではスライドアニメーションは不要とした。
この画面切り替えを実装すると、画面切り替え操作をしたときに以下のエラーが出た。
Performing hot restart...
Syncing files to device iPhone 12...
Restarted application in 506ms.
[VERBOSE-2:ui_dart_state.cc(199)] Unhandled Exception: Bad state: Future already completed
# 0 _AsyncCompleter.complete (dart:async/future_impl.dart:45:31)
# 1 _StoreMapState.build.<anonymous closure> (package:map_test_2/widgets/map.dart:110:36)
# 2 _GoogleMapState.onPlatformViewCreated (package:google_maps_flutter/src/google_map.dart:355:19)
<asynchronous suspension>
何がいけないのかコードを一つずつ検証していったところ、どうやらmapController
をStoreMapに渡しているのが悪かったらしい。
mapController
を渡さず、StoreMapの中で自分でmapControllerを定義するようにしたら解決した。
しかしこの解決方法だと、カルーセルと別のcontrollerを使うことになってしまうので、カルーセルタップ時に選択されたスポットを地図の中心に表示する機能が使えなくなってしまう。
この問題は、後で以下の記事を参考に修正する。
今回やりたいのは、リスト表示の方の作り込みなので。

データの追加
indexページができても、登録データが一つだけだとリストの見た目が悪いので、
何個か他のデータを入れておく。
flutter公式のサンプルアプリのreadmeに記されているデータ形式は以下。
ice_cream_stores:
placeId: ChIJ70taCKKAhYAR5IMmYwQT4Ts
address: 432 Octavia St #1a, San Francisco, CA 94102, USA
location: 37.7763629, -122.4241918
name: Smitten Ice Cream
このplaceIdとは、場所に多対1で紐づいている値らしい。
placeIdがわかれば場所の特定ができるが、場所からplaceIdは一意には特定できない、ということ。
以下のページで場所からplaceIdと住所を取得し、二つ目のページで住所から緯度経度を取得してfirebaseのcloud firestoreに保存する。
とりあえず、適当な場所を5つくらい登録した。
listページの作成
現在counter画面で仮置きしている部分を、スポット情報の一覧画面にする。
以下の記事を参考に、検索まで行えるようにしちゃう。
inputTextとListBuilderとCardの単体の使い方の例
search用のinputTextFieldとlistを縦に並べるためにColumnを使う
listを入れ子にするための解決策

listページのソート機能
以下の記事群を参考に、設置するボタンのデザインを考える。
デザインの参考
設置位置の参考
ソート方式は、とりあえず「名前」と「緯度」。
緯度でソートしたいケースがわからないけど、とりあえず挙動の確認をするためにこれでいく。

コードは以下。
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
enum SortType{
name,
latitude
}
class IndexPage extends StatefulWidget {
const IndexPage({
Key? key,
required this.documents,
}) : super(key: key);
final List<DocumentSnapshot> documents;
@override
State<IndexPage> createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> {
var _inputString;
var _selectedSortType = SortType.name;
void _tapSortButton(SortType sortType) {
setState(() {
_selectedSortType = sortType;
});
}
// ボタン
Widget _buildButtonArea() {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
child: Text(
'名前',
style: TextStyle(
color: _selectedSortType == SortType.name ? Colors.black: Colors.white,
),
),
style: ElevatedButton.styleFrom(
primary: _selectedSortType == SortType.name ? Colors.white : Colors.grey,
onPrimary: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
onPressed: () {
_tapSortButton(SortType.name);
},
),
ElevatedButton(
child: Text(
'緯度',
style: TextStyle(
color: _selectedSortType == SortType.latitude ? Colors.black: Colors.white,
),
),
style: ElevatedButton.styleFrom(
primary: _selectedSortType == SortType.latitude ? Colors.white: Colors.grey,
onPrimary: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
onPressed: () {
_tapSortButton(SortType.latitude);
},
),
]
)
);
}
// 検索欄
Widget _buildInput() {
return Container(
margin: const EdgeInsets.only(bottom: 20, left: 20, right: 20, top: 5),
height: 40,
child: TextField(
decoration: InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: '場所名前/住所を入力',
),
onChanged: (inputString) {
setState(() {
_inputString = inputString;
});
},
)
);
}
// 場所の一覧
Widget _buildPlacesList() {
var _places = _inputString == null ? widget.documents : widget.documents.where((n) => n["name"].contains(_inputString) || n["address"].contains(_inputString)).toList();
switch(_selectedSortType) {
case SortType.name:
_places.sort((a,b) => a["name"].compareTo(b["name"]));
break;
case SortType.latitude:
_places.sort((a,b) => a["location"].latitude.compareTo(b["location"].latitude));
break;
default:
break;
}
return ListView.builder(
itemCount: _places.length,
itemBuilder: (BuildContext context, int index) {
final place = _places[index];
return _buildCard(place);
},
);
}
// 場所のカード
Widget _buildCard(DocumentSnapshot place) {
return Card(
margin: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
place["name"],
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16.0
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 12.0),
child: Text(
place["address"],
style: TextStyle(
fontWeight: FontWeight.w200,
color: Colors.grey
)
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.star),
SizedBox(
width: 50.0,
child: Text("11"),
),
Icon(Icons.remove_red_eye),
SizedBox(
width: 50.0,
child: Text("12"),
),
],
),
SizedBox(height: 16.0,)
],
),
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
body: Column(
children: <Widget>[
_buildButtonArea(),
_buildInput(),
Expanded(
child: _buildPlacesList()
),
// _buildBox(),
]
)
);
}
}