Google Flutter コラム – 第5回 仮想的なカーナビ作ってみた

本コラムシリーズで紹介してきたGoogle Flutterおよびmapboxを使って仮想的なカーナビを作成してみます。

第5回では第2~4回の内容を踏まえた上で作成するため、先にこちらをご覧ください。

画面構成

以下のような構成で作成します。※画像クリックで拡大できます。

画面遷移について

Google Futterでは画面遷移をする際に、Navigatorクラスを使用します。

スタックと呼ばれる構造を使用して、画面を管理しています。Google Flutterでは、スタックの下の要素から順に重ねて出力します。

1つめの画像では、スタックに地図画面のみ存在するため、そのまま地図画面が表示されています。

2つめの画像では、地図画面の上に検索画面が存在するため、地図画面の上に重ねて検索画面が表示されます。

実際には重なっているため、地図画面は見えません。

スタックと言われると以前に紹介したStackウィジェットが思い当たると思います。

Navigatorの実態はStackウィジェットを使って各ページを管理しているそうです。

そこで上に重ねるページのサイズを小さくしたり、背景を透明にしたりすろと下のページが見えるのか?と思い実験してみたところ・・・

pushしたページしか表示されませんでした。

どうやらpushする際に、もともと表示されていたページはoffstageという画面に表示しない領域に移動されているようです。

気を取り直して、本コラムで画面遷移に使用したコードを紹介します。

まず、画面遷移するページを登録します。ページ名と遷移先の画面を指定します。以下のように記載します。

  return MaterialApp(
/*** 省略 ***/
routes: {
"ページ名1": (BuildContext context) => ページ1(),
"ページ名2": (BuildContext context) => ページ2(),
},
);

次に、実際に画面遷移するコードです。ここでは3種類を紹介します。

  • 次の画面に遷移したい場合は以下のように記載します。
    ※画面構成の赤色の矢印にあたります。
  Navigator.of(context).pushNamed("ページ名")
  Navigator.of(context).pushNamed("ページ名")
  • 1つ前の画面に戻る場合は以下のように記載します。
    ※画面構成の青色の矢印にあたります。
  Navigator.of(context).pop()
  Navigator.of(context).pop()
  • 指定したページまで戻る場合は以下のように記載します。
    ※画面構成の緑色の矢印にあたります。
  Navigator.of(context).popUntil(ModalRoute.withName("ページ名"))
  Navigator.of(context).popUntil(ModalRoute.withName("ページ名"))

これらのコードを画面遷移したいところに記載します。

例えば、あるボタンを押した時や入力が完了した時、タイマーが作動した時などです。

以下はボタンを押した時に画面遷移を実施するコードの例です。

【Navigatorサンプル】 ※Runボタンを押すことでサンプルを実行できます


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {"page1":(context)=>Page1(),
               "page2":(context)=>Page2(),
              },
      home: Page1(),
    );
  }
}

class Page1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      body: const Center(
        child: Text("page1"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){
          Navigator.of(context).pushNamed("page2");
        },
        child: const Text("進む"),
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.orange,
      body: const Center(
        child: Text("page2"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){
          Navigator.of(context).pop();
        },
        child: const Text("戻る"),
      ),
    );
  }
}
            

データベースについて

データベースにアクセスする

本コラムでは名古屋市役所までいくことを想定しています。そのため、名古屋市役所の住所を検索するにあたってデータベース(以下DB)を活用します。

Google FlutterでDBを使用する場合はsqliteを使用します。
sqliteとはDBを管理するための仕組みで、Google Flutter公式でも紹介されています。使用するパッケージはsqfliteです。
DBは住所DBを使用します。インターネットからダウンロードし、プロジェクトに配置します。その後、DBにアクセスして必要な情報を抽出します。

失敗例

ここで先に失敗例を紹介します。DBにアクセスする際にopenDatabaseというメソッドを用いるのですが、引数にデータベースのパスを設定する必要があります。

そこで先ほどプロジェクトに配置したDBを設定したところ、DBが見つからない旨のエラーが表示されてしまいました。

 // 失敗例;
try {
  // DB接続
  database = await openDatabase("プロジェクトに配置したDBファイルのパス/ファイル名", version: 1);
  print("connect DB");
} catch (e) {
  print(e);
  print("ERROR");
}

成功例

少し調べてみたところ、どうやらアプリケーションがDBを操作するための領域があるようです。

そのため、一度DBを専用の領域に作成(コピー)したのち、そのDBを使っていきます。

 // 成功例
_projectDBPath = "プロジェクトに配置したDBファイルのパス/ファイル名";
_databasePath = await getDatabasesPath();
_database = join(_databasePath, "DBファイル名");
 
try {
  bool exists = await databaseExists(_database);
  // DBが存在するかどうか
  if (!exists) {
    // 出力先のファイル作成
    await Directory(dirname(_database)).create(recursive: true);
    
    // DLしたデータベースファイルを読み込む
    ByteData data = await rootBundle.load(_projectDBPath);
    List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
    // 内部領域に書き込む
    await File(_database).writeAsBytes(bytes, flush: true);
    print("DB create");
  } else {
    print("DB exists");
  }
  // DB接続
  database = await openDatabase(_database, version: 1);
  print("connect DB");
} catch (e) {
  print(e);
  print("ERROR");
}

3,4行目は内部領域に保存するDBのパスと名前を設定しています。
7行目では、内部領域にDBが存在するかを確認しています。プロジェクトフォルダにDBファイルを配置するだけではアプリからアクセスすることができないためです。
9~19行目では、内部領域にDBが存在しないためプロジェクトフォルダに配置したDBファイルを読み込んで内部領域にコピーします。
25行目で改めて内部領域に存在するDBにアクセスしています。

データベースを使用する

次に、DBから必要な値を抽出します。DBに格納されているデータは以下のようになっています。
※一部省略、データは架空

都道府県

市区町村

町域

字丁目

事業所

愛知県

名古屋市中村区

☆☆町

1丁目

愛知県

名古屋市中村区

☆☆町

2丁目

愛知県

名古屋市中村区

☆☆町

3丁目

愛知県

名古屋市中村区

○○町

愛知県

名古屋市中村区

××町

○×株式会社

愛知県

豊田市

□□町

○○ビル

愛知県

豊田市

△△町

1丁目

△△

都道府県

愛知県

愛知県

愛知県

愛知県

愛知県

愛知県

愛知県

市区町村

名古屋市中村区

名古屋市中村区

名古屋市中村区

名古屋市中村区

名古屋市中村区

豊田市

豊田市

町域

☆☆町

☆☆町

☆☆町

○○町

××町

□□町

△△町

字丁目

1丁目

2丁目

3丁目

1丁目

事業所

○×株式会社

○○ビル

△△

以下は選択した都道府県を持つレコードから市区町村を抽出するコードです。抽出した市名を元にリストを作成します。

ken = _searchList[0];
maps = await database.query("DBのテーブル名", where: "都道府県 = ?", whereArgs: [ken]);
Future.forEach(maps, (element) {
      retList.add(maps[count]["市区町村"]);
      count++;
});
// 重複削除
retList = retList.toSet().toList();

1行目の_searchList[0]には選択した都道府県名が入っています。
2行目は、テーブルの都道府県と選択した都道府県が一致するレコードを抽出しています。
3~6行目では、抽出したレコードから「市区町村」を抜き出してretListへ追加します。
最後に、ListクラスのtoSet()メソッドを使用して重複したデータを削除したのち、toList()メソッドを使って元に戻して完了です。
上記テーブルの例では、「名古屋市中村区」と「豊田市」の2つのデータを持つリストになります。
このように選択した都道府県に紐づく市区町村のリストを作成しています。市区町村以降の住所についても同様です。

画面切り替えについて

タイマーによる画面切り替え

実際のカーナビでは交差点に接近すると「この信号を右折です」のようなアナウンスと共に交差点の画像が表示されます。
本コラムではタイマーを使って表現してみます。

Google Flutterでタイマーを使用する場合はTimerクラスを使用します。Timerクラスはdart:asyncパッケージに含まれています。

以下のように使います。

// タイマー起動
Timer(期間,コールバック);
// タイマー起動
Timer(期間,コールバック);

期間には、Durationを使います。Timerをコールして指定した期間が経過するとコールバックされます。
以下のサンプルは5秒のタイマーを起動し、画面の表示方法を切り替えるコードです。

【Timerサンプル】 ※Runボタンを押すことでサンプルを実行できます


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


void main() {
  runApp(MaterialApp(home: TimerSample()));
}

class TimerSample extends StatefulWidget {
  @override
  _TimerSampleState createState() => _TimerSampleState();
}

class _TimerSampleState extends State {
  bool flag = false;

  @override
  Widget build(BuildContext context) {
    // タイマー起動
    Timer(const Duration(seconds: 5), () {
      // コールバックではフラグの切り替えを実施
      setState(() {
        flag = true;
      });
    });

    return Scaffold(
      body: Center(
        child: SizedBox(
          width: 200,
          height: 200,
          child: flag ? 
            Row(children:[
              Container(
                width: 100,
                height: 100,
                color: Colors.blue,
              ),
              Container(
                width: 100,
                height: 100,
                color: Colors.green,
              ),
            ]):
            Column(children:[
              Container(
                width: 100,
                height: 100,
                color: Colors.blue,
              ),
              Container(
                width: 100,
                height: 100,
                color: Colors.green,
              ),
            ]),
        ),
      ),
    );
  }
}
            

ボタンによる画面切り替え

実際のカーナビでは自車位置周辺の交通情報を表示するための画面が存在します。
本コラムではボタンを押すことで画面を表示するようにしてみます。

地図上に表示するために、showDialogを使用しています。
一般的なダイアログはユーザに情報を出した後は閉じるだけのことが多く、ダイアログ自体の変化はありません。
今回はダイアログ内で複数の情報を切り替えて表示させるため、少し工夫が必要です。

showDialog(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setState) => SimpleDialog(省略),
    ),
);

3,4行目でStatefulBuilderを使って、SimpleDialogを作成しています。
こうすることで、showDialogで表示する情報を切り替えることができます。

ダイアログ下部に配置したボタンを押すことで、表示する交通情報を変更しています。
表示中のページが1つ目の場合は戻るボタンを、最後のページの場合は進むボタンをトーンダウンしてボタンを無効化しています。
ボタンのトーンダウンは以下のようにonPressedでnullを指定します。

ElevatedButton(
  child: const Text("ボタン"),
  onPressed: 条件 ? () {処理} : null
)

以下はダイアログの表示、ダイアログ内の表示変更を実施するコードの例です。

【ダイアログ画面サンプル】 ※Runボタンを押すことでサンプルを実行できます


import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: DialogSample()));
}

class DialogSample extends StatefulWidget {
  @override
  _DialogSampleState createState() => _DialogSampleState();
}

class _DialogSampleState extends State {
  // ダイアログに表示する数字
  int _num = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ダイアログ表示ボタン
      body: Center(
        child: ElevatedButton(
          child: const Text('ダイアログ表示'),
          onPressed: _showDialogSample,
        ),
      ),
    );
  }

  // ダイアログ表示
  void _showDialogSample() {
    // ダイアログを表示する
    showDialog(
      context: context,
      builder: (context) => StatefulBuilder(
        builder: (context, setState) => SimpleDialog(
          children: [
            Align(
              alignment: Alignment.topLeft,
              child: _closeButton(),
            ),
            Center(
              child: Text(_num.toString()),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                // 数字減少ボタン
                ElevatedButton(
                  child: const Text('-'),
                  // 反映が遅いので連打すると-1になることがあります
                  onPressed: _num <= 0 ? null : () {
                    setState(() {
                      _num--;
                    });
                  },
                ),

                // 数字増加ボタン
                ElevatedButton(
                  child: const Text('+'),
                  // 反映が遅いので連打すると6になることがあります
                  onPressed: _num >= 5 ? null : () {
                    setState(() {
                      _num++;
                    });
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  // 閉じるボタン
  Widget _closeButton() {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        shape: const CircleBorder(),
      ),
      child: const Icon(
        Icons.clear,
      ),
      onPressed: (() {
        // ダイアログを消去
        Navigator.pop(context);
      }),
    );
  }
}
                

まとめ

Google Flutterを使って仮想的なカーナビを作成してみました。
画面遷移とダイアログを用いることで、画面同士をつなげて動きのあるアプリを作成できました。
また、本コラムを通じてデータベースやタイマーなど、少しレベルの高いコードに挑戦できました。
最後に、今回作成したアプリを動かした動画を紹介します。

おわりに

さて、本コラムシリーズ第1~5回にわたってGoogle Flutterについて紹介してきました。
本コラムシリーズを通して「Google Flutterおもしろそう!」「こういう考え方もあるのか!」と皆様のお役に立てれば幸いです。
Google Flutterの汎用性は高く、今後もいろいろな活動を実施していこうと思います。

キーワード

本コラムシリーズで紹介したキーワードとリンクです。ぜひご覧になってください。

flutterの導入 flutterを使うための環境構築を解説しています
GoogleMap連携 flutterでGoogleMapを使う方法を解説しています
mapbox連携 flutterでmapboxを使う方法を解説しています
ローカライズ GoogleMap、mapboxでのローカライズを比較しています
地図タイプ切り替え GoogleMap、mapboxでの地図タイプ切り替えを比較しています
渋滞表示 GoogleMap、mapboxでの渋滞表示を比較しています
緯度経度出力 GoogleMap、mapboxでの緯度経度の出力方法を比較しています
マーカー表示 GoogleMap、mapboxでのマーカー表示を比較しています
カスタムスタイル GoogleMap、mapboxでのカスタムスタイルを比較しています
YAML flutterを使う上で重要なpubspec.yamlについて解説しています
Pub.dev flutterを使う上で重要なパッケージについて解説しています
widget 画面を構成する要素であるwidgetについて解説しています
レイアウト制御 widgetの配置をどのように制御するかを解説しています
マテリアルデザイン マテリアルデザインに準拠したflutterアプリについて解説しています
釦表示 flutterでボタンを表示する方法について解説しています
アイコン表示 mapboxを使ってアイコンを表示する方法を解説しています
ドロワーメニュー flutterでドロワーメニューを表示する方法を解説しています
画面遷移 flutterで画面を切り替える方法を解説しています
データベース flutterでデータベースを使う方法を解説しています
タイマー flutterでタイマーを使う方法を解説しています
ダイアログ flutterでダイアログを使う方法を解説しています
flutterの導入 flutterを使うための環境構築を解説しています
GoogleMap連携 flutterでGoogleMapを使う方法を解説しています
mapbox連携 flutterでmapboxを使う方法を解説しています
ローカライズ GoogleMap、mapboxでのローカライズを比較しています
地図タイプ切り替え GoogleMap、mapboxでの地図タイプ切り替えを比較しています
渋滞表示 GoogleMap、mapboxでの渋滞表示を比較しています
緯度経度出力 GoogleMap、mapboxでの緯度経度の出力方法を比較しています
マーカー表示 GoogleMap、mapboxでのマーカー表示を比較しています
カスタムスタイル GoogleMap、mapboxでのカスタムスタイルを比較しています
YAML flutterを使う上で重要なpubspec.yamlについて解説しています
Pub.dev flutterを使う上で重要なパッケージについて解説しています
widget 画面を構成する要素であるwidgetについて解説しています
レイアウト制御 widgetの配置をどのように制御するかを解説しています
マテリアルデザイン マテリアルデザインに準拠したflutterアプリについて解説しています
釦表示 flutterでボタンを表示する方法について解説しています
アイコン表示 mapboxを使ってアイコンを表示する方法を解説しています
ドロワーメニュー flutterでドロワーメニューを表示する方法を解説しています
画面遷移 flutterで画面を切り替える方法を解説しています
データベース flutterでデータベースを使う方法を解説しています
タイマー flutterでタイマーを使う方法を解説しています
ダイアログ flutterでダイアログを使う方法を解説しています

お問い合わせ

名古屋本社

東京本社

【受付時間】9:00~18:00

pagetop

お客様のお悩み承ります。