[Flutter][クソアプリ工房][002] ゴミマップ ② GoogleMapとlocationを使ってアプリ上に地図を表示する

前回の [クソアプリ工房][002] ゴミマップ ①企画・設計)の続きです。

『ゴミマップ』開発、スタート。

 

はじめに

  • 音声付きの解説動画をYouTube『クソアプリ工房』にアップしています。(ゴミマップで検索)
  • 文字を読むのがダルい、聞き流しで Flutter 開発勉強できないかなーとお考えの方はぜひ覗いてみてください。
  • 本アプリのソースコードは全て無料でGitHubに公開しております。

ちなみに、私はコミュ障ぼっちでフリーランスのエンジニアをやっています、34歳です。私のプロフィールに興味を持ってくださった方は、ページ下部の筆者紹介、Twitter(@suekiaoi)やInstagram(@aoi.sueki)などからご確認いただければと思います。

また、ぼっち社会人向けに1年かけて書いていたコラムが書籍化されました。ぜひ一度、お手にとって読んでみてください。

友達0のコミュ障が「一人」で稼げるようになったぼっち仕事術 (アルファポリス)

 

ゴール:『ゴミマップ』画面イメージ

前回作ったこのイメージに従って実装していきます。

ちなみに『ゴミマップ』機能は超シンプル。

  • Googleマップを表示する
  • 現在地を表示する
  • 写真を撮る
  • マップ上に写真を表示する
  • 写真が撮影された座標を登録/削除する

 

 

STEP1 レイアウトする

まずは一番下のレイヤーにGoogleマップを表示する用のContainer、その上にタイトルバー、カメラボタンを設置する。

幸いなことにFlutterデモアプリが既に完成イメージのレイアウトとかなり似ているので、こいつをそのまま使うことにしました。

やったのは、テーマカラーとアイコン変えただけ。

before

after

 

STEP2 Google Map と連携

Googleマップのライブラリを使って地図を表示していきます。

 

手順①google_maps_flutter 2.0.3 をインストール

以下のサイトの手順にしたがってインストールしていきまます。

google_maps_flutter | Flutter PackageA Flutter plugin for integrating Google Maps in iOS and Android applications.
google_maps_flutter | Flutter Package pub.dev
google_maps_flutter | Flutter Package

ターミナルで下記を実行すると、

$ flutter pub add google_maps_flutter

pubspec.yamlに依存関係が自動で追記されます。

dependencies:
  google_maps_flutter: ^2.0.3

 

手順② Google Map APIの有効化

ここに書いてある通りにやります。

Getting started with Google Maps Platform  |  Google Developers Maps Platform Overview Products Pricing Documentation Get Started Get Started with Google Maps Platform API Picker Billing Account Credits Billing Reporting FAQ Support and Resources Incident Management Maps Maps JavaScript API Maps SDK for Android Maps SDK for iOS Maps Static API Street View Static API Maps Embed API Maps URLs Maps Elevation API Routes Directions API Distance Matrix API Roads API Solutions Industry Solutions Retail Gaming Services Places Places API Places SDK for Android Places SDK for iOS Places Library, Maps JavaScript API Geocoding API Geolocation API Time Zone API Additional Resources API Key Best Practices Map Coverage Details Optimization Guide Deprecations Asset Tracking Plan Root CA Migration FAQ Public Programs Premium Plan Blog Language English Bahasa Indonesia Deutsch Español Español – América Latina Français Português – Brasil Русский 中文 – 简体 中文 – 繁體 日本語 한국어 Documentation Google Maps Platform Overview Products Pricing Documentation More Blog Google Maps Platform Documentation Get Started Get Started with Google Maps Platform API Picker API Key Best Practices Billing Account Credits Billing Reporting FAQ Support and Resources Incident Management Maps Maps JavaScript API Maps SDK for Android Maps SDK for iOS Maps Static API Street View Static API Maps Embed API Maps URLs Routes Directions API Distance Matrix API Roads API Places Places API Places SDK for Android Places SDK for iOS Places Library, Maps JavaScript API Geocoding API Geolocation API Time Zone API Elevation API Solutions Industry Solutions Retail Gaming Services Additional Resources Map Coverage Details Optimization Guide Optimizing Web Service Usage Deprecations Asset Tracking Plan Root CA Migration FAQ Public Programs Domains Pre-Launch Checklist Premium Plan Get Started Get Started with Google Maps Platform API Picker Billing Account Credits Billing Reporting FAQ Support and Resources Incident Management Maps Maps JavaScript API Maps SDK for Android Maps SDK for iOS Maps Static API Street View Static API Maps Embed API Maps URLs Maps Elevation API Routes Directions API Distance Matrix API Roads API Solutions Industry Solutions Retail Gaming Services Places Places API Places SDK for Android Places SDK for iOS Places Library, Maps JavaScript API Geocoding API Geolocation API Time Zone API Additional Resources API Key Best Practices Map Coverage Details Optimization Guide Deprecations Asset Tracking Plan Root CA Migration FAQ Public Programs Premium Plan /* Styles inlined from /maps/styles/local.css */ /* added for new pricing and plans page */ .full-bleed { background-color: #F9F9F9; display: flex; flex-flow: row nowrap; margin: 0 -10000px; margin-top: 10px; padding: 0 10000px; position: relative; } .full-bleed.pricing-section { margin-top: 10px; margin-bottom: 40px; padding-bottom: 64px; padding-top: 64px; } .full-bleed.pricing-summary { margin-top: 0px; margin-bottom: 10px; padding-bottom: 24px; padding-top: 24px; } .licensing-selector-frame { margin-top: 0px; height: 680px; } .intro { margin: 2em 0 2em 0; } .pricing-top { width: 30%; min-height: 320px; } .pricing-intro { margin-top: 4em; } .pricing-card { margin-top: 40px; padding: 1em; background-color: #fff; } .pricing-card2 { background-color: #F7F7F7; width: 50%; min-height: 240px; } .pricing-card h3 { font: 400 26px/40px Roboto,sans-serif; letter-spacing: -.01em; color: #757575; margin: 0 0 .4em 0; } .pricing-card h4 { line-height: 1.3em; } .pricing-card ul { color: #757575; list-style-type: none; padding: 0px; margin: 0px; font-weight: normal; } .pricing-card ul li { background-image: url('/maps/images/lhimages/compare-yes.svg'); background-repeat: no-repeat; background-position: 0px 1px; padding-left: 30px; } @media (max-width: 1000px) { .pricing-card, .pricing-top { width: 100%; min-height: 110px; } .pricing-intro { margin-top: 0px; } } .lighthouse-table th, .lighthouse-table td { padding-left: 20px; } .lighthouse-table thead tr th { text-transform: uppercase; } .lighthouse-table thead tr th.normal { text-transform: initial; } .lighthouse-table tr { border: 0; } .lighthouse-table td { vertical-align: middle; } .lighthouse-table td.center { text-align: center; } .lighthouse-table p { margin: 0; } table.lighthouse-style-2 td, table.lighthouse-style-2 th { color: #65686C; background-color: #E5F3FE; border: 0 white solid; border-right-width: 2px; } table.lighthouse-style-2 td:last-child, table.lighthouse-style-2 th:last-child { border-right-width: 0; } table.lighthouse-style-2 tr { border: 0 #e4e8ee solid; border-bottom-width: 1px; } table.lighthouse-style-2 .price-table-footer td { background-color: #FAFAFA; font-weight: 600; } table.lighthouse-style-2 tr:last-of-type td { padding-left: 20px; } table.lighthouse-style-2 th { background-color: #6DA6F3; color: white; } table.lighthouse-style-2 th.alt { background-color: #E6E6E6; color: #65686C; } table.lighthouse-style-2 td.alt { background-color: #FAFAFA; } table.lighthouse-style-2 td.alt-2 { background-color: #FFFFFF; font-weight: 400; padding-top: 20px; text-align: center; } table.lighthouse-style-2 tr.alt-2 { background: transparent; border-bottom-width: 0; } table.lighthouse-style-3 td { background-color: #FFFFFF; } .lighthouse-table .margin-top p { margin-top: 1em; } .price-table-tailer-link { font-size: .9em; } .price-table-tailer-link img { margin-right: 10px; width: 16px; } .mobile { display: none; } .lighthouse-table { table-layout: fixed; } .lighthouse-table th { width: 33%; } @media screen and (max-width: 720px) { .mobile { display: block; font-weight: bold; } .desktop-only { display: none; } table.lighthouse-table td { display: block; border-right-width: 0; } /* Table chrome */ table.lighthouse-style-2 td.alt { background-color: #6DA6F3; color: #fff; } table.lighthouse-style-2 td.alt a { color: #fff; } table.lighthouse-style-2 th.alt { background-color: #fff; padding-left: 0; } } .lighthouse-table-cta { table-layout: fixed; border-collapse: initial; } .lighthouse-table-cta td { background-color: #fff; } .lh-info { font-size: 70%; margin-left: 4px; display: inline-block; } .footnote { font-size: 70%; } .td-footnote { font-size: 80%; } table.comparison .chk { text-align: center; } #premium { margin-top: 45px; } .faq h4 { margin-bottom: 0; line-height: 1.3em; } .faq p { margin-top: .5em; } .devsite-landing-row-item-image.clear { background-color: transparent; } .devsite-landing-row-1-up .devsite-landing-row-item-description.max-width { width: 100%; } @media (max-width: 1000px) { .devsite-landing-row-1-up .devsite-landing-row-item-description.max-width { width: initial; } } .devsite-landing-row-item-image { background: transparent; } .find-section { height: 260px; display: flex; flex-flow: row nowrap; background-color: #F7F7F7; overflow: hidden; margin-top: 0px; } .find-section .find-section-body { padding: 20px; padding-top: 21px; width: 45%; } .find-section .find-section-media { height: 100%; width: 55%; } .find-section .find-section-media img { background: transparent; padding: 0 20px; } .find-section .find-section-media.devicelenses { background-image: url("/maps/images/lhimages/devices/2x1_devicelenses.png"); background-origin: content-box; background-position: right; background-repeat: no-repeat; background-size: cover; height: 100%; padding: 0 20px; } @media (max-width: 1000px) { .licensing-selector-frame { height: 1200px; } .find-section { flex-flow: column; height: initial; } .find-section .find-section-body { width: initial; } .find-section .find-section-media { padding-right: 0; width: initial; } .find-section .find-section-media.devicelenses { background: none; } } .logos-container { height: 100%; } .logos-container .logos { align-items: center; display: flex; flex-flow: row nowrap; height: 100%; justify-content: space-around; } .logos-container .logos img { height: 200px; /*was 60px */ padding: 5px; width: auto; } @media (max-width: 1000px) { .logos-container { padding: 20px; text-align: center; } .logos-container .logos { display: none; } } .link-container { padding-top: 20px; } .devsite-enable-billing-dialog { left: 50%; margin-left: -274px; top: 25%; width: 548px; color: #65686C; padding: 32px 24px; } .devsite-enable-billing-dialog p { line-height: 24px; } .devsite-enable-billing-dialog ol { list-style: none; padding: 0; margin: 40px 0; } .devsite-enable-billing-dialog li { counter-increment: step-counter; font-size: 14px; line-height: 16px; margin-bottom: 28px; } .devsite-enable-billing-dialog li::before { content: counter(step-counter); margin-right: 22px; font-weight: 500; padding: 6px 10px; border: 2px solid #ECECEC; border-radius: 50%; font-size: 14px; color: #00BCD4; } /* Lists on the API picker page */ .gc-picker-list { list-style-type: none; padding-left: 2em; text-indent: -2em; margin-bottom: 0px; } .lh-link { font: 500 14px/20px Roboto,sans-serif; text-transform: uppercase; } .lh-link:focus { text-decoration: none; } #details { border: none; padding-top: 80px; margin-top: -80px; } @media screen and (max-width: 720px) { .licensing-selector-frame { height: 2160px; } .mobile-hidden { display: none !important; } } @media screen and (max-width: 400px) { .licensing-selector-frame { height: 2310px; } .mobile-hidden { display: none !important; } } /* Table that showcases an illustration next to a description. */ body.docs table.illustrated { border: none; } body.docs td.illustrated-narrow { border: none; width: 150px; text-align: center; vertical-align: middle; } body.docs td.illustrated-remainder { border: none; vertical-align: top; } body.docs h3.illustrated { margin-top: 0px; } /* Styles inlined from /maps/documentation/local_extensions.css */ .version { text-align: right; } .code { color: #006000; font-family: "Courier", monospace; font-size: 100%; } .grid td { padding: 4px; border-collapse: collapse; border: 1px solid gray; } .header td { font-weight: bold; background: #EEEEEE; } .encodeBox { width: 550px; height: 40px; font-size: 14px; font-family: Courier; } .inputField { width: 160px; } #pointList { width: 300px; font-size: 12px; } #txtAddress { width: 14em; } .welcome { border: none; } .welcome td{ border: none; padding:0 10px 0 10px; } .number { background-color: #E5ECF9; text-align:center; vertical-align:middle; padding: 0 5px; } .leftpadding { padding: 0 0 0 10px; } .identifier { color: black; } /* List the contents inline to save vertical space */ .summarylist { margin-right: 4em; } .summarylist li { display: inline; margin: 0em; margin-right: 0.2em; line-height: 1.5em; } /* Self-links for Maps API for Flash */ a.self-link:link { color: #0000cc; text-decoration: none; } a.self-link:active { color: #0000cc; text-decoration: none; } a.self-link:visited { color: #0000cc; text-decoration: none; } /* Self-links for Maps API */ .self-link { cursor: pointer; } .blackbg { background: #333; } /* Styles inlined from /maps/styles/common.css */ /* Enable Billing Modal Dialog Styles */ .devsite-enable-billing-dialog { left: 50%; margin-left: -274px; top: 25%; width: 548px; } .devsite-dialog-close { color: #747474; } #devsite-dialog-onload-billing-enabled { padding: 0; } #devsite-dialog-onload-billing-enabled .get-key-check { background: no-repeat #8cc152 center/72px url(/maps/images/lhimages/v2/[email protected]); height: 150px; width: 100%; } #devsite-dialog-onload-billing-enabled .devsite-dialog-contents, #devsite-dialog-onload-billing-enabled .devsite-dialog-buttons { margin: 8px; } .devsite-enable-billing-dialog ol { list-style: none; margin: 40px 0; padding: 0; } .devsite-enable-billing-dialog li { counter-increment: step-counter; font-size: 14px; line-height: 16px; margin-bottom: 28px; } .devsite-enable-billing-dialog li::before { border-radius: 50%; border: 2px solid #ececec; color: #00bcd4; content: counter(step-counter); font-size: 14px; font-weight: 500; margin-right: 22px; padding: 6px 10px; } /* Style Wizard introduction */ .styleWizardIntroMain { border-radius: 3px; height: 500; width: 640; } .styleWizardIntroSixMapSamples { left: calc(50% - (500px / 2)); line-height: 1; position: relative; text-align: center; width: 500px; } .styleWizardIntroSample { border: 1px solid rgba(0,0,0,0.07); border-radius: 3px; box-shadow: 0 1px 12px 0 rgba(0,0,0,0.10); display: inline-block; height: 140px; margin: 10px; overflow: hidden; width: 140px; } .styleWizardIntroCaption { text-align: center; } /* TODO(cl/330601385) waiting on fix to be deployed */ .devsite-jsfiddle-hide { position:absolute; top: -99999px; left: -99999px; display:block; } /* Styles for /maps/solutions best practices content */ #bp-background { border-radius: 25px; background: #e8eaed; } #bp-heading span { float:left; margin-right:10px; position:relative; padding: 10px; } /* Use as
Getting started with Google Maps Platform  |  Google Developers developers.google.com
Getting started with Google Maps Platform  |  Google Developers

Getting Started

  • API key の取得  https://cloud.google.com/maps-platform/.
  • EGoogleMapSDKを有効にします。
    • Google Developers Console に移動
    • Googleマップを有効にするプロジェクトを選択します。
    • ナビゲーションメニューを選択し、「Googleマップ」を選択します。
    • Googleマップメニューから「API」を選択します。
    • Google Maps for Androidを有効にするには、[追加のAPI]セクションで[Maps SDK for Android]を選択し、[有効にする]を選択します。
    • Google Maps for iOSを有効にするには、[追加のAPI]セクションで[Maps SDK for iOS]を選択し、[有効にする]を選択します。
    • 有効にしたAPIが[有効なAPI]セクションにあることを確認してください。

手順③ APIキーの取得

GCPでAPIキーを取得します。手順は以下の通り。

Getting started with Google Maps Platform  |  Google Developers Maps Platform Overview Products Pricing Documentation Get Started Get Started with Google Maps Platform API Picker Billing Account Credits Billing Reporting FAQ Support and Resources Incident Management Maps Maps JavaScript API Maps SDK for Android Maps SDK for iOS Maps Static API Street View Static API Maps Embed API Maps URLs Maps Elevation API Routes Directions API Distance Matrix API Roads API Solutions Industry Solutions Retail Gaming Services Places Places API Places SDK for Android Places SDK for iOS Places Library, Maps JavaScript API Geocoding API Geolocation API Time Zone API Additional Resources API Key Best Practices Map Coverage Details Optimization Guide Deprecations Asset Tracking Plan Root CA Migration FAQ Public Programs Premium Plan Blog Language English Bahasa Indonesia Deutsch Español Español – América Latina Français Português – Brasil Русский 中文 – 简体 中文 – 繁體 日本語 한국어 Documentation Google Maps Platform Overview Products Pricing Documentation More Blog Google Maps Platform Documentation Get Started Get Started with Google Maps Platform API Picker API Key Best Practices Billing Account Credits Billing Reporting FAQ Support and Resources Incident Management Maps Maps JavaScript API Maps SDK for Android Maps SDK for iOS Maps Static API Street View Static API Maps Embed API Maps URLs Routes Directions API Distance Matrix API Roads API Places Places API Places SDK for Android Places SDK for iOS Places Library, Maps JavaScript API Geocoding API Geolocation API Time Zone API Elevation API Solutions Industry Solutions Retail Gaming Services Additional Resources Map Coverage Details Optimization Guide Optimizing Web Service Usage Deprecations Asset Tracking Plan Root CA Migration FAQ Public Programs Domains Pre-Launch Checklist Premium Plan Get Started Get Started with Google Maps Platform API Picker Billing Account Credits Billing Reporting FAQ Support and Resources Incident Management Maps Maps JavaScript API Maps SDK for Android Maps SDK for iOS Maps Static API Street View Static API Maps Embed API Maps URLs Maps Elevation API Routes Directions API Distance Matrix API Roads API Solutions Industry Solutions Retail Gaming Services Places Places API Places SDK for Android Places SDK for iOS Places Library, Maps JavaScript API Geocoding API Geolocation API Time Zone API Additional Resources API Key Best Practices Map Coverage Details Optimization Guide Deprecations Asset Tracking Plan Root CA Migration FAQ Public Programs Premium Plan /* Styles inlined from /maps/styles/local.css */ /* added for new pricing and plans page */ .full-bleed { background-color: #F9F9F9; display: flex; flex-flow: row nowrap; margin: 0 -10000px; margin-top: 10px; padding: 0 10000px; position: relative; } .full-bleed.pricing-section { margin-top: 10px; margin-bottom: 40px; padding-bottom: 64px; padding-top: 64px; } .full-bleed.pricing-summary { margin-top: 0px; margin-bottom: 10px; padding-bottom: 24px; padding-top: 24px; } .licensing-selector-frame { margin-top: 0px; height: 680px; } .intro { margin: 2em 0 2em 0; } .pricing-top { width: 30%; min-height: 320px; } .pricing-intro { margin-top: 4em; } .pricing-card { margin-top: 40px; padding: 1em; background-color: #fff; } .pricing-card2 { background-color: #F7F7F7; width: 50%; min-height: 240px; } .pricing-card h3 { font: 400 26px/40px Roboto,sans-serif; letter-spacing: -.01em; color: #757575; margin: 0 0 .4em 0; } .pricing-card h4 { line-height: 1.3em; } .pricing-card ul { color: #757575; list-style-type: none; padding: 0px; margin: 0px; font-weight: normal; } .pricing-card ul li { background-image: url('/maps/images/lhimages/compare-yes.svg'); background-repeat: no-repeat; background-position: 0px 1px; padding-left: 30px; } @media (max-width: 1000px) { .pricing-card, .pricing-top { width: 100%; min-height: 110px; } .pricing-intro { margin-top: 0px; } } .lighthouse-table th, .lighthouse-table td { padding-left: 20px; } .lighthouse-table thead tr th { text-transform: uppercase; } .lighthouse-table thead tr th.normal { text-transform: initial; } .lighthouse-table tr { border: 0; } .lighthouse-table td { vertical-align: middle; } .lighthouse-table td.center { text-align: center; } .lighthouse-table p { margin: 0; } table.lighthouse-style-2 td, table.lighthouse-style-2 th { color: #65686C; background-color: #E5F3FE; border: 0 white solid; border-right-width: 2px; } table.lighthouse-style-2 td:last-child, table.lighthouse-style-2 th:last-child { border-right-width: 0; } table.lighthouse-style-2 tr { border: 0 #e4e8ee solid; border-bottom-width: 1px; } table.lighthouse-style-2 .price-table-footer td { background-color: #FAFAFA; font-weight: 600; } table.lighthouse-style-2 tr:last-of-type td { padding-left: 20px; } table.lighthouse-style-2 th { background-color: #6DA6F3; color: white; } table.lighthouse-style-2 th.alt { background-color: #E6E6E6; color: #65686C; } table.lighthouse-style-2 td.alt { background-color: #FAFAFA; } table.lighthouse-style-2 td.alt-2 { background-color: #FFFFFF; font-weight: 400; padding-top: 20px; text-align: center; } table.lighthouse-style-2 tr.alt-2 { background: transparent; border-bottom-width: 0; } table.lighthouse-style-3 td { background-color: #FFFFFF; } .lighthouse-table .margin-top p { margin-top: 1em; } .price-table-tailer-link { font-size: .9em; } .price-table-tailer-link img { margin-right: 10px; width: 16px; } .mobile { display: none; } .lighthouse-table { table-layout: fixed; } .lighthouse-table th { width: 33%; } @media screen and (max-width: 720px) { .mobile { display: block; font-weight: bold; } .desktop-only { display: none; } table.lighthouse-table td { display: block; border-right-width: 0; } /* Table chrome */ table.lighthouse-style-2 td.alt { background-color: #6DA6F3; color: #fff; } table.lighthouse-style-2 td.alt a { color: #fff; } table.lighthouse-style-2 th.alt { background-color: #fff; padding-left: 0; } } .lighthouse-table-cta { table-layout: fixed; border-collapse: initial; } .lighthouse-table-cta td { background-color: #fff; } .lh-info { font-size: 70%; margin-left: 4px; display: inline-block; } .footnote { font-size: 70%; } .td-footnote { font-size: 80%; } table.comparison .chk { text-align: center; } #premium { margin-top: 45px; } .faq h4 { margin-bottom: 0; line-height: 1.3em; } .faq p { margin-top: .5em; } .devsite-landing-row-item-image.clear { background-color: transparent; } .devsite-landing-row-1-up .devsite-landing-row-item-description.max-width { width: 100%; } @media (max-width: 1000px) { .devsite-landing-row-1-up .devsite-landing-row-item-description.max-width { width: initial; } } .devsite-landing-row-item-image { background: transparent; } .find-section { height: 260px; display: flex; flex-flow: row nowrap; background-color: #F7F7F7; overflow: hidden; margin-top: 0px; } .find-section .find-section-body { padding: 20px; padding-top: 21px; width: 45%; } .find-section .find-section-media { height: 100%; width: 55%; } .find-section .find-section-media img { background: transparent; padding: 0 20px; } .find-section .find-section-media.devicelenses { background-image: url("/maps/images/lhimages/devices/2x1_devicelenses.png"); background-origin: content-box; background-position: right; background-repeat: no-repeat; background-size: cover; height: 100%; padding: 0 20px; } @media (max-width: 1000px) { .licensing-selector-frame { height: 1200px; } .find-section { flex-flow: column; height: initial; } .find-section .find-section-body { width: initial; } .find-section .find-section-media { padding-right: 0; width: initial; } .find-section .find-section-media.devicelenses { background: none; } } .logos-container { height: 100%; } .logos-container .logos { align-items: center; display: flex; flex-flow: row nowrap; height: 100%; justify-content: space-around; } .logos-container .logos img { height: 200px; /*was 60px */ padding: 5px; width: auto; } @media (max-width: 1000px) { .logos-container { padding: 20px; text-align: center; } .logos-container .logos { display: none; } } .link-container { padding-top: 20px; } .devsite-enable-billing-dialog { left: 50%; margin-left: -274px; top: 25%; width: 548px; color: #65686C; padding: 32px 24px; } .devsite-enable-billing-dialog p { line-height: 24px; } .devsite-enable-billing-dialog ol { list-style: none; padding: 0; margin: 40px 0; } .devsite-enable-billing-dialog li { counter-increment: step-counter; font-size: 14px; line-height: 16px; margin-bottom: 28px; } .devsite-enable-billing-dialog li::before { content: counter(step-counter); margin-right: 22px; font-weight: 500; padding: 6px 10px; border: 2px solid #ECECEC; border-radius: 50%; font-size: 14px; color: #00BCD4; } /* Lists on the API picker page */ .gc-picker-list { list-style-type: none; padding-left: 2em; text-indent: -2em; margin-bottom: 0px; } .lh-link { font: 500 14px/20px Roboto,sans-serif; text-transform: uppercase; } .lh-link:focus { text-decoration: none; } #details { border: none; padding-top: 80px; margin-top: -80px; } @media screen and (max-width: 720px) { .licensing-selector-frame { height: 2160px; } .mobile-hidden { display: none !important; } } @media screen and (max-width: 400px) { .licensing-selector-frame { height: 2310px; } .mobile-hidden { display: none !important; } } /* Table that showcases an illustration next to a description. */ body.docs table.illustrated { border: none; } body.docs td.illustrated-narrow { border: none; width: 150px; text-align: center; vertical-align: middle; } body.docs td.illustrated-remainder { border: none; vertical-align: top; } body.docs h3.illustrated { margin-top: 0px; } /* Styles inlined from /maps/documentation/local_extensions.css */ .version { text-align: right; } .code { color: #006000; font-family: "Courier", monospace; font-size: 100%; } .grid td { padding: 4px; border-collapse: collapse; border: 1px solid gray; } .header td { font-weight: bold; background: #EEEEEE; } .encodeBox { width: 550px; height: 40px; font-size: 14px; font-family: Courier; } .inputField { width: 160px; } #pointList { width: 300px; font-size: 12px; } #txtAddress { width: 14em; } .welcome { border: none; } .welcome td{ border: none; padding:0 10px 0 10px; } .number { background-color: #E5ECF9; text-align:center; vertical-align:middle; padding: 0 5px; } .leftpadding { padding: 0 0 0 10px; } .identifier { color: black; } /* List the contents inline to save vertical space */ .summarylist { margin-right: 4em; } .summarylist li { display: inline; margin: 0em; margin-right: 0.2em; line-height: 1.5em; } /* Self-links for Maps API for Flash */ a.self-link:link { color: #0000cc; text-decoration: none; } a.self-link:active { color: #0000cc; text-decoration: none; } a.self-link:visited { color: #0000cc; text-decoration: none; } /* Self-links for Maps API */ .self-link { cursor: pointer; } .blackbg { background: #333; } /* Styles inlined from /maps/styles/common.css */ /* Enable Billing Modal Dialog Styles */ .devsite-enable-billing-dialog { left: 50%; margin-left: -274px; top: 25%; width: 548px; } .devsite-dialog-close { color: #747474; } #devsite-dialog-onload-billing-enabled { padding: 0; } #devsite-dialog-onload-billing-enabled .get-key-check { background: no-repeat #8cc152 center/72px url(/maps/images/lhimages/v2/[email protected]); height: 150px; width: 100%; } #devsite-dialog-onload-billing-enabled .devsite-dialog-contents, #devsite-dialog-onload-billing-enabled .devsite-dialog-buttons { margin: 8px; } .devsite-enable-billing-dialog ol { list-style: none; margin: 40px 0; padding: 0; } .devsite-enable-billing-dialog li { counter-increment: step-counter; font-size: 14px; line-height: 16px; margin-bottom: 28px; } .devsite-enable-billing-dialog li::before { border-radius: 50%; border: 2px solid #ececec; color: #00bcd4; content: counter(step-counter); font-size: 14px; font-weight: 500; margin-right: 22px; padding: 6px 10px; } /* Style Wizard introduction */ .styleWizardIntroMain { border-radius: 3px; height: 500; width: 640; } .styleWizardIntroSixMapSamples { left: calc(50% - (500px / 2)); line-height: 1; position: relative; text-align: center; width: 500px; } .styleWizardIntroSample { border: 1px solid rgba(0,0,0,0.07); border-radius: 3px; box-shadow: 0 1px 12px 0 rgba(0,0,0,0.10); display: inline-block; height: 140px; margin: 10px; overflow: hidden; width: 140px; } .styleWizardIntroCaption { text-align: center; } /* TODO(cl/330601385) waiting on fix to be deployed */ .devsite-jsfiddle-hide { position:absolute; top: -99999px; left: -99999px; display:block; } /* Styles for /maps/solutions best practices content */ #bp-background { border-radius: 25px; background: #e8eaed; } #bp-heading span { float:left; margin-right:10px; position:relative; padding: 10px; } /* Use as
Getting started with Google Maps Platform  |  Google Developers developers.google.com
Getting started with Google Maps Platform  |  Google Developers

使ってみるをクリックし、Cloud Console で次の操作を行います。

  1. 1 つ以上のサービスを選択する
  2. プロジェクトを作成する
  3. 請求先アカウントを設定する
  4. 選択したサービスに関連付けられている API を有効にする
  5. API キーを作成する

 

手順④ 取得したAPIキーをアプリにセット

ここにある通りにやる。今回はiOSのみなので AppDelegate.swift を編集していきます。

google_maps_flutter | Flutter PackageA Flutter plugin for integrating Google Maps in iOS and Android applications.
google_maps_flutter | Flutter Package pub.dev
google_maps_flutter | Flutter Package

赤字部分を追記。

AppDelegate.swift

import UIKit
import Flutter
import GoogleMaps

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("ここにAPIキーを貼り付け")
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

 

ここでホットリロードしたらエラーが起きました。

エラー:

Error output from CocoaPods:
↳

    [!] Automatically assigning platform `iOS` with version `9.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.

Error: To set up CocoaPods for ARM macOS, run:
  arch -x86_64 sudo gem install ffi

Error running pod install
Error launching application on iPhone 12 Pro Max.

 

解決方法:

素直にターミナルで以下を実行すればOK。

arch -x86_64 sudo gem install ffi

 

手順⑤ GoogleMapを表示してみる

ご親切にExampleが載っていたので参考にしつつ動作確認。

google_maps_flutter | Flutter PackageA Flutter plugin for integrating Google Maps in iOS and Android applications.
google_maps_flutter | Flutter Package pub.dev
google_maps_flutter | Flutter Package

 

赤字を追記。レイアウトが崩れたのでCenterウィジェットは削除してます。あとMapTypeはnormal。

main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Gomi Map',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: MyHomePage(title: 'ゴミマップ'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State {
  int _counter = 0;

  void _takePhoto() {
    setState(() {
      _counter++;
    });
  }

  Completer _controller = Completer();

  static final CameraPosition _kGooglePlex = CameraPosition(
    target: LatLng(37.42796133580664, -122.085749655962),
    zoom: 14.4746,
  );

  static final CameraPosition _kTarget = CameraPosition(
    bearing:  192.8334901395799,
    target: LatLng(37.43296265331129, -122.08832357078792),
    tilt: 59.440717697143555,
    zoom: 19.151926040649414
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: GoogleMap(
        mapType: MapType.normal,
        initialCameraPosition: _kGooglePlex,
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller);
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _takePhoto,
        tooltip: '撮影',
        child: Icon(Icons.camera_alt),
      ),
    );
  }
}

 

手順⑥ カメラボタンの位置と形を変更

カメラボタン(FloatingActionButton)がGoogleMapの現在地ボタンに被っちゃってて現在地ボタンが押せないのでるので位置を変更します。

before

after

CenterとColumnで包んで、とりあえず画面下の中央に移動。crossAxisAlignment と mainAxisAlignment で配置するけど、以下のチートシートがわかりやすくてとても便利。

ついでにボタンの形もexpandedで変更します。

main.dart

floatingActionButton: Center( // ボタンを中央下に配置
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.end,
      children: [
        Container(
         margin: EdgeInsets.only(bottom: 15.0), // ボタン位置の微調整 
         child: FloatingActionButton.extended( // ボタンの形を変更
              onPressed: _takePhoto,
              label: Text('ゴミみっけ!'),
              icon: Icon(Icons.camera_alt),
            ),
        ),
      ],
    ),
  ),

 

 

手順⑦ GoogleMap上に現在地を表示

pubspec.yaml に locationを追加し、Pub getします。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  google_maps_flutter: ^2.0.3
  location: ^3.0.0

 

https://pub.dev/packages/location に記載してあるとおり、設定していきます。

今回は iOS のみなのでAndroidとWebはやりません。

Info.plistの末尾にNSLocationWhenInUseUsageDescriptionNSLocationAlwaysUsageDescriptionを追記します。

ios/Runner/Info.plist

    ...
    <key>io.flutter.embedded_views_preview</key>
    <true/>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>location is required for this app</string>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>location is required for this app</string>
</dict>
</plist>

 

あとはGoogleMapの_onMapCreatedメソッドに組み込んでいくだけです。

こちらをめちゃ参考にさせていただきました↓

butachin/flutter_mapsGoogle Maps APIを用いたFlutterのアプリです. Contribute to butachin/flutter_maps development by creating an account on GitHub.
butachin/flutter_maps github.com
butachin/flutter_maps

長くなるし、わたしもコピペしただけでよくわからないのでフルのソースは Github からご参照ください。

 

途中で遭遇したエラー

NoSuchMethodError: The getter 'latitude' was called on null.
Receiver: null
Tried calling: latitude

どうやら、現在地を取得する前に画面描画しようとして「どこで初期表示したらいいかわかりまへん!」ってエラーになってるっぽい。(非同期呼び出しが終了する前に緯度にアクセスしようとしている)

<解決方法>

そんな時はこう! 描画しようとした時に現在地まだ取れてなかったらCircularProgressIndicator (くるくる回るやつ)を表示してねというif文を追記。

lib/main.dart

@override
Widget build(BuildContext context) {
  if (currentLocation == null) {
    return Center(
      child: CircularProgressIndicator(),
    );
  } else {
    return Scaffold(

 

できた! 画面中央に見覚えのある現在地ピン(青い丸)が表示されています。

ちなみに、この現在地はシミュレータのデフォルト(サンフランシスコのどこか?)です。リアルなわたしの現在位置を知りたい人はお友達からお願いします笑

 

長くなってきたので続きは次回。

 

ここまで読んでいただき、ありがとうございました!

参考になったよ、という方はTwitter(@suekiaoi)やInstagram(@aoi.sueki)などでフォローしていただけると励みになります。

不明点・ご質問などありましたらわたしのTwitter DM(@suekiaoi)にお気軽にどうぞ!

それでは!

末岐 碧衣
  • 末岐 碧衣
  • フリーランス のシステムエンジニア。独立後、一度も営業せずに月収 96 万円を達成。1986年大阪生まれ。早稲田大学理工学部卒。システムエンジニア歴 12年。
    2009年、ITコンサルティング企業に入社。3年目でコミュ障が爆発し人間関係が崩壊。うつにより休職するも、復帰後はコミュ障の自覚を持ち、「チームプレイ」を徹底的に避け、会社組織内においても「一人でできる仕事」に専念。社内外から評価を得た。
    無理に「チームプレイ」するよりも「一人でできる仕事」に専念した方が自分も周囲も幸せにできることを確信し、2015年フリーランスとして独立。