【Flutter/Dart】情報管理アプリ＋Google地図アプリを作ろう

# 1.はじめに
これまでに7つのAndroidStudio&Flutter&Dart記事を書いている。以下は内容、

- 【Flutter/Dart】環境構築方法の紹介
https://qiita.com/my_programming/items/ad1493e3fef2fb63e036

- 【Flutter/Dart】簡単なプロジェクトの設定と共有方法の紹介
https://qiita.com/my_programming/items/cec09574a3b6e39be277

- 【Flutter/Dart】エミュレータの言語設定をかえよう
https://qiita.com/my_programming/items/6c67fed22c624fd9494b

- 【Flutter/Dart】Dartの文法について学ぼう
https://qiita.com/my_programming/items/9ba25114ef217d077ca5

- 【Flutter/Dart】情報管理アプリを作ろう
https://qiita.com/my_programming/items/9ed2276f6e9076f43444

- 【Flutter/Dart】Google地図アプリを作ろう
https://qiita.com/my_programming/items/26b9ac6f0d2b3d1bd766

- **【Flutter/Dart】情報管理アプリ＋Google地図アプリを作ろう（今回説明する内容）**

今回は、情報管理アプリとGoogle地図アプリをあわせて以下のようなアプリを作る。
-Google地図上でマーカーつけした情報を記録できる機能
-記録した情報をリスト化し、画面に順番に表示する機能
-選択した住所のマーカーに画面を遷移する機能
-選択した住所を画面遷移先からメール転送できる機能

# 2.情報管理アプリ＋Google地図アプリを作ろう
## 2-1.Google地図上でマーカーつけした情報を記録できる機能
・新しいFlutterプロジェクトを作成する。プロジェクト名はapp_listとする。
・google_maps_flutterというプラグインを[Dart Packages](https://pub.dev/)で検索、バージョンをコピーして`pubspec.yaml`に貼り付けてから、`Pub get`を押す。
``` pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  google_maps_flutter: ^2.1.8
```
・`android/app/build.gradle`を開き`minSdkVersion`を20に変える。
・`android/app/src/main/AndroidManifest.xml`に以下のAPIキーを追加する。APIキーの部分については各自入手したものに変更する。
``` android/app/src/main/AndroidManifest.xml
<application android:label="map_app3" android:name="${applicationName}" android:icon="@mipmap/ic_launcher">
<meta-data android:name="com.google.android.geo.API_KEY" android:value="APIキー"/>
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
```
・`android/app/src/main/AndroidManifest.xml`に以下の権限設定のコードを追加する。
``` android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.map_app3">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
```
・google_maps_flutter.dartをimport、MyAppという名前のStetelessWidgetとStatefulWidget/_MapViewStateという名前のStatefulWidgetを作成、_MapViewStateにGoogleMap Widgetを書く。_initialLocation変数を作成してカメラポジションを東京駅とする。

``` lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

// main関数
void main() {
  runApp(MyApp());
}

// Stateless Widgetを継承したmyAppクラス
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Maps',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MapView(),
    );
  }
}

// Stateful Widgetを継承したMapViewクラス。Stateful Widgetの生成
class MapView extends StatefulWidget {
  @override
  _MapViewState createState() => _MapViewState();
}
// 生成されたStateful WidgetのMapViewクラスを実行するためのメインとなる実行用クラス（_MapViewState）
class _MapViewState extends State<MapView> {
  // マップビューの初期位置
  CameraPosition _initialLocation = CameraPosition(target: LatLng(35.68145403034362, 139.76707116150914), zoom: 16);

  @override
  Widget build(BuildContext context) {
    // 画面の幅と高さを決定する
    var height = MediaQuery.of(context).size.height;
    var width = MediaQuery.of(context).size.width;

    return Container(
      height: height,
      width: width,
      child: Scaffold(
        body: Stack(
          children: <Widget>[
            GoogleMap(
              initialCameraPosition: _initialLocation,
              myLocationEnabled: true,
              myLocationButtonEnabled: false,
              mapType: MapType.normal,
              zoomGesturesEnabled: true,
              zoomControlsEnabled: false,
            ),
          ],
        ),
      ),
    );
  }
}
```

・ここまでのコードを実行した結果は以下、
<img width="100" alt="step2_1_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/88706e36-1f54-9f6c-e123-8f3c45e84318.png">

・次に画面にボタンを追加していく。ボタンの制御用途として以下の変数を追加する。後でデータを格納するためlateをつけておく。
``` lib/main.dart
  // マップの表示制御用
  late GoogleMapController mapController;
```
・GoogleMap WidgetにmapControllerを追加する。
``` lib/main.dart
  GoogleMap(
    initialCameraPosition: _initialLocation,
    myLocationEnabled: true,
    myLocationButtonEnabled: false,
    mapType: MapType.normal,
    zoomGesturesEnabled: true,
    zoomControlsEnabled: false,
    onMapCreated: (GoogleMapController controller) {
      mapController = controller;
    },
  ),
```

・ボタンのUIを追加する。
``` lib/main.dart
  // ここからボタンを表示するためのコードを追加
  // ズームイン・ズームアウトのボタンを配置
  SafeArea(
    child: Align(
      alignment: Alignment.centerRight,
      child: Padding(
        padding: const EdgeInsets.only(right: 10.0, bottom: 100.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            // ズームインボタン
            ClipOval(
              child: Material(
                color: Colors.blue.shade100, // ボタンを押す前のカラー
                child: InkWell(
                  splashColor: Colors.blue, // ボタンを押した後のカラー
                  child: SizedBox(
                    width: 50,
                    height: 50,
                    child: Icon(Icons.add),
                  ),
                  onTap: () {
                    mapController.animateCamera(
                      CameraUpdate.zoomIn(),
                    );
                  },
                ),
              ),
            ),
            SizedBox(height: 20),
            //　ズームアウトボタン
            ClipOval(
              child: Material(
                color: Colors.blue.shade100, // ボタンを押す前のカラー
                child: InkWell(
                  splashColor: Colors.blue, // ボタンを押した後のカラー
                  child: SizedBox(
                    width: 50,
                    height: 50,
                    child: Icon(Icons.remove),
                  ),
                  onTap: () {
                    mapController.animateCamera(
                      CameraUpdate.zoomOut(),
                    );
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    ),
  ),

  // 画面を初期位置に移動
  SafeArea(
    child: Align(
      alignment: Alignment.bottomRight,
      child: Padding(
        padding: const EdgeInsets.only(right: 10.0, bottom: 10.0),
        // 現在地表示ボタン
        child: ClipOval(
          child: Material(
            color: Colors.blue.shade100, // ボタンを押す前のカラー
            child: InkWell(
              splashColor: Colors.blue, // ボタンを押した後のカラー
              child: SizedBox(
                width: 50,
                height: 50,
                child: Icon(Icons.my_location),
              ),
              onTap: () {
                mapController.animateCamera(
                  CameraUpdate.newCameraPosition(
                      _initialLocation
                  ),
                );
              },
            ),
          ),
        ),
      ),
    ),
  ),
```

・ここまでのコードを実行した結果は以下。ズームイン、ズームアウト、初期位置への移動のための各ボタンが画面に表示される様になる。
<img width="100" alt="step2_1_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/576a4581-fee4-cfb1-af86-9504f21bad9c.png">

・次にマーカーをリスト化していくための変数を作る。
``` lib/main.dart
  // マーカーリスト保存用
  List<Marker> myMarker = [];
```

・GoogleMap Widgetにマーカー表示のコードを追加するとともに、マップをクリックしたときにマーカーをリストに追加するための関数をontapの引数として設定。
``` lib/main.dart
  GoogleMap(
    initialCameraPosition: _initialLocation,
    myLocationEnabled: true,
    myLocationButtonEnabled: false,
    mapType: MapType.normal,
    zoomGesturesEnabled: true,
    zoomControlsEnabled: false,
    markers: Set.from(myMarker),
    onTap: _handleTap,
    onMapCreated: (GoogleMapController controller) {
      mapController = controller;
    },
  ),
```

・マップをクリックした時にマーカーをリストに追加するため下記の関数を_MapViewStateクラス内に追加する。
``` lib/main.dart
  // マップをクリックした時にマーカーをリストに追加
  _handleTap(LatLng tappedPoint) async {
    late LatLng iniPos;
    late LatLng endPos;
    Marker marker_tmp = Marker(
      markerId: MarkerId(tappedPoint.toString()),
      position: tappedPoint,
    );

    setState(() {
      myMarker.add(marker_tmp);
    });
  }
```
・ここまでのコードを実行した結果は以下。マップを4回クリックした結果を表示。
<img width="100" alt="step2_1_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/8757192f-13db-de63-f4dc-ea8bb8eb4db3.png">

## 2-2.記録した情報をリスト化し、画面に順番に表示する機能
・マーカーリストには各マーカーの経度・緯度の情報を蓄えられている。ここでは経度・緯度の情報から住所を得るための段取りを説明する。まず、geocorder2のパッケージをdart packagesで検索、バージョンをコピーして`pubspec.yaml`に貼り付けてから、`Pub get`を押す。
``` pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  google_maps_flutter: ^2.1.8
  geocoder2: ^1.1.2
```
・次にgeocorder2をimportする。
``` lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geocoder2/geocoder2.dart';
```
・次に住所をリストに保存するための変数location_dataを作る。
``` lib/main.dart
  // マーカー住所保存用
  List<GeoData> location_data = [];
```
・次に経度・緯度を住所に変換する関数を作る。APIキーの部分については各自入手したものに変更する。
``` lib/main.dart
  // 経度・緯度を住所に変換
  Future<GeoData> getLocation(Marker data_marker) async{
    GeoData data = await Geocoder2.getDataFromCoordinates(
        latitude: data_marker.position.latitude,
        longitude: data_marker.position.longitude,
        googleMapApiKey: "API Key");
    return data;
  }
```
・次にマーカーを追加した時にgetLocation関数を呼び出し緯度・経度を住所に変更した結果をlocation_dataに格納できる様にする。
``` lib/main.dart
  // マップをクリックした時にマーカーをリストに追加
  _handleTap(LatLng tappedPoint) async {
    late LatLng iniPos;
    late LatLng endPos;
    Marker marker_tmp = Marker(
      markerId: MarkerId(tappedPoint.toString()),
      position: tappedPoint,
      draggable: true,
      onDragStart: (iniPosition) {iniPos = iniPosition;},
      onDragEnd: (endPosition) {endPos = endPosition;},
    );

    await getLocation(marker_tmp).then((GeoData location_tmp) async {
      print(location_tmp);
      setState(() {
        myMarker.add(marker_tmp);
        location_data.add(location_tmp);
      });
    });
  }
```

・location_dataの一覧表示するための機能を追加する。
``` lib/main.dart
  // 住所一覧表示画面
  SafeArea(
    child: Align(
      alignment: Alignment.topCenter,
      child: Padding(
        padding: const EdgeInsets.only(top: 30.0),
        child: Container(
          decoration: BoxDecoration(
              color: Colors.black38
          ),
          width: width * 0.85,
          child: Padding(
            padding: const EdgeInsets.only(top: 5.0, bottom: 5.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
              Container(
                height: 150, // 高さ指定
                child: ListView.builder(
                  itemCount: location_data.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(location_data[index].address ?? ''),
                      textColor: Colors.white,
                      onTap: (){
                        // ここに2-3コード追加
                      },
                    );
                  }
                )),
              ]
            ),
          ),
        ),
      ),
    ),
  ),
```
・ここまでのコードを実行した結果は以下。半透明の黒いウィンドウに白抜き文字で住所が表示されている。黒いウィンドウからはみ出す部分についてはマウスで縦方向にスクロールすることで画面に住所を表示させられる。
<img width="100" alt="step2_1_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/f49a36a7-e351-44ba-479c-4eea8107bf66.png">

## 2-3.選択した住所のマーカーに画面を遷移する機能
・住所一覧の中からメールで送信したい住所を選択し、その住所に該当するマーカーにスクリーンを動かせる様にする。まず、共有したい住所情報の記録用の変数を作る。
``` lib/main.dart
  // 共有したい住所情報の記録用変数
  String shared_address = 'no address';
```
・次に黒いウィンドウ内に一覧表示された住所をマウスでクリックすると画面をその住所があるマーカー位置に移動できる様にする。
``` lib/main.dart
// 住所一覧表示画面
  SafeArea(
    child: Align(
      alignment: Alignment.topCenter,
      child: Padding(
        padding: const EdgeInsets.only(top: 30.0),
        child: Container(
          decoration: BoxDecoration(
              color: Colors.black38
          ),
          width: width * 0.85,
          child: Padding(
            padding: const EdgeInsets.only(top: 5.0, bottom: 5.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
              Container(
                height: 150, // 高さ指定
                child: ListView.builder(
                  itemCount: location_data.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(location_data[index].address ?? ''),
                      textColor: Colors.white,
                      onTap: (){
                        setState(() {
                          if (location_data.length > 0) {
                            shared_address = location_data[index].address;

                            print('${location_data.length}, ${index}, ${shared_address}');

                            mapController.animateCamera(
                              CameraUpdate.newCameraPosition(
                                CameraPosition(
                                    target: LatLng(location_data[index].latitude,
                                        location_data[index].longitude), zoom: 18),
                              ),
                            );
                          }
                        });
                      },
                    );
                  }
                )),
              ]
            ),
          ),
        ),
      ),
    ),
  ),
```
・ここまでのコードを実行した結果は以下。一つ前の表示画面に写っている4つのマーカーのうち、右上のマーカーに該当する住所を一覧から選んだところ。選んだ結果はshared_address変数に文字列として格納されている。2-4でこれをメールで外部に転送できる様にする。
<img width="100" alt="step2_1_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/6c6c6e5f-9731-ce1b-7b8a-1693e9a536dd.png">

## 2-4.選択した住所を画面遷移先からメール転送できる機能
・ここでは選択した住所をメールで他者と共有できる様にする。flutterアプリでメールを送信するにはflutter_email_senderを使うと良い。そこでflutter_email_senderをdart packagesで検索する。その後に、flutter_email_senderの名前の隣にあるコピーアイコンを押してからpubspec.yamlに情報を貼り付けPub getを最後に押す。
``` pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  google_maps_flutter: ^2.1.8
  geocoder2: ^1.1.2
  flutter_email_sender: ^5.1.0
```

・次に`android/app/src/main/AndroidManifest.xml`に以下のメールアプリを使うためのコードを追加する。
``` android/app/src/main/AndroidManifest.xml
    <queries>
        <intent>
            <action android:name="android.intent.action.SENDTO"/>
            <data android:scheme="mailto"/>
        </intent>
    </queries>
```
・次にflutter_email_senderをimportする。
``` lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geocoder2/geocoder2.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
```

・次にメール送信画面を呼び出すためのボタンを配置する。住所を何も選んでない状態ではボタンは非アクティブの状態、住所を選ぶとメール送信画面を呼び出せるアクティブ状態になる。アクティブ状態ではMailScreenという名前のStatefulWidgetを呼び出せる様になる。選択した住所はshared_address変数に格納されており、MailScreenクラスに引数として渡される。
``` lib/main.dart
  SafeArea(
    child: Align(
      alignment: Alignment.bottomCenter,
      child: Padding(
        padding: const EdgeInsets.only(right: 10.0, bottom: 10.0),
        // 現在地表示ボタン
        child:  ElevatedButton(
          onPressed: (shared_address != 'no address')? () async {
            print('button pressed');
  
            // "push"で新規画面に遷移
            // リスト追加画面から渡される値を受け取る
            final flag_mail = await Navigator.of(context).push(
              MaterialPageRoute(builder: (context) {
                // 遷移先の画面としてリスト追加画面を指定
                return MailScreen(address: shared_address);
              }),
            );
            if (flag_mail != null) {
              print('sent mail');
            }
          }: null,
          child: Text("share address"),
        ),
      ),
    )
  ),
```

・次にMailScreenクラスは引数として渡されたshared_addressをその後の_MailScreenStateクラスに渡す。_MailScreenStateは実際にメール送信に関わる状態を管理する。メール送信画面は、メールアドレス・メール題名・メールの本文からなる。メールの本文には予めshared_addressに格納された文字列が表示されている。メールアドレスとメール題名の欄にはヒントの文字がデフォルトで表示されている。
``` lib/main.dart
// Stateful Widgetを継承したMailScreenクラス。Stateful Widgetの生成
class MailScreen extends StatefulWidget {
  final String address;
  const MailScreen({Key? key, required this.address}) : super(key: key);
  @override
  State<MailScreen> createState() => _MailScreenState();
}

// 生成されたStateful WidgetのMailScreenクラスを実行するためのメインとなる実行用クラス（_MailScreenState）
class _MailScreenState extends State<MailScreen> {
  late String address;
  late TextEditingController _emailController;
  late TextEditingController _subjectController;
  late TextEditingController _bodyController;

  @override
  void initState() {
    super.initState();
    address = widget.address;
    _emailController = TextEditingController();
    _subjectController = TextEditingController();
    _bodyController = TextEditingController(text: address);
  }

  @override
  void dispose() {
    _emailController.dispose();
    _bodyController.dispose();
    _subjectController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('メール送信')),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Column(
            children: [
              const SizedBox(height: 40),
              TextFormField(
                controller: _emailController,
                decoration: InputDecoration(hintText: 'メールアドレス'),
              ),
              const SizedBox(height: 20),
              TextFormField(
                controller: _subjectController,
                decoration: InputDecoration(hintText: 'メール題名'),
              ),
              const SizedBox(height: 20),
              TextFormField(
                controller: _bodyController,
                // decoration: InputDecoration(hintText: address),
              ),
              const SizedBox(height: 20),
              ElevatedButton(onPressed: _sendEmail, child: Text('送信')),
              const SizedBox(height: 40),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _sendEmail() async {
    final email = Email(
      body: _bodyController.text,
      subject: _subjectController.text,
      recipients: [_emailController.text],
      isHTML: false,
    );
    await FlutterEmailSender.send(email);
  }
}
```
・ここまでのコードを実行した結果は以下。
画面には4つのマーカーが表示されているが住所一覧で何も選んでいない状態。画面下部のメール送信ボタンが非アクティブになっている。
<img width="100" alt="step2_1_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/b8085a8d-1fb0-7c45-ee2d-f97accdfd5d6.png">

住所一覧で4つ目の住所を選択した状態。画面が住所に該当するマーカー上に移動。メール送信ボタンがアクティブになる。
<img width="100" alt="step2_1_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/e46a4953-6faa-e9ae-480a-f19e6ab4c9fa.png">

メール送信画面が表示される。メールアドレスとメールの題名を書いてから送信ボタンを押すとスマートフォンのメーラーを介してメールが実際に送信される。
<img width="100" alt="step2_1_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/80d0ead3-3bb7-1ece-0c6c-b34a7b3e1059.png">

ここまでに使ったコードをgithubに置いたのでリンクを紹介
https://github.com/MY-CODE-1981/map_list

# 3.まとめ
情報管理アプリとGoogle地図アプリをあわせたアプリを作成した。