FlutterでRunlogger(風)を作ってみた

,

以前Objective-Cで作成していたRunLoggerをFlutterの調査・勉強の為にテスト的に基本部分を作ってみました。

地図とRun画面

1)状況とFlutter

モバイル開発環境として現在はXamarin/NetMAUIを使用しているが、世の中を見るとFlutterが優勢の様なのでFlutterを調査・勉強していたが、実際にアプリを作成してみないと良くは分からないので、実際にアプリを作る事にしました。

以前Objective-Cで作成していたRunloggerを題材とし、地図・Run(距離・スピード・標高)とcsv保存機能のみ作成する事とします(=Runlogger機能の1/5程度)。

Objective-Cで作成していたRunlogger画面です。

画面は「Run/地図/カメラ/ログ/設定」があり、座標系もWgs84以外にTokyoや19座標系も選べる様にしてあります。

またログにはカロリーグラフ表示もできる様にしてました。

2)作成したファイル・フォルダー

実際に修正・作成したのは以下のファイルとフォルダーでした。
実際のソースなどはGitHubにアップしてありますので、そちらを参照してください。

│  README.md
│  pubspec.yaml
├─lib
│  │  run.dart
│  │  map.dart
│  │  main.dart
│  │  
│  └─model
│          gps.dart
│          debugData.dart
│          gpsLogData.dart
│          
└─assets
    └─images
        │  map-01.png
        │  run-01.png
        │  set-00.png
        │  
        └─launcher
                icon_ios.png
ファイル名内容など
README.mdGitに配置する概要説明書
pubspec.yaml追加パッケージ設定や、sdk指定を行う。
<現在の追加パッケージ>
fl_chart: ^0.69.0
platform_maps_flutter: ^1.0.2
cupertino_icons: ^1.0.8
geolocator: ^13.0.1
latlong2: ^0.9.1
flutter_launcher_icons: ^0.14.1
intl: ^0.19.0
path_provider: ^2.1.5
main.dartRunloggerメインプログラムソース
ここでTab設定を行っている。TAB切り替えて、map/run画面を表示する。set画面は未実装です。
map.dartmap(地図)画面プログラムソース
地図表示と現在位置にピンを立てる。
run.dartrun(地図)画面プログラムソース
走行速度・距離・標高を表示する
model/gps.dartGPSの関連情報を取得するクラス.
別々な関数や値をまとめるためのクラスで、staticで全てアクセスできる様にした物です(ある意味ではシングルトーンパターンですね)。
model/gpsLogData.dartrunloggerの「logger」部分のデータを管理するクラスです。「Start」「Stop」間の位置や高さ、スピード、距離などを保存しており「Stop」時にcsvファイルとして保存します。
model/gpsLogData.dart動作検証用の座標値リストデータファイルです。Debugモードの時に使用されます。
assets/images/map-01.pngmapタブ用icon画像
assets/images/run-01.pngrunタブ用icon画像
assets/images/set-00.pngsetタブ用icon画像
assets/images/launcher/icon_ios.png起動icon画像
flutter_launcher_iconsを使用してiOS用の画像に変換される。

3)Map(地図)画面

Runloggerを起動すると地図画面が表示されます。ログが開始されて居ませんので、走行時間・走行距離・「stopボタン」などは灰色表示です。

「Startボタン」でログを開始します。

「Stopボタン」でログを終了します。終了時にcsvファイルにログを保存します。

「RunTypeボタン」は未実装です。歩き・走り・自転車などでカロリー計算パラメータなどを切り替える為に使用予定です。

「+」地図を拡大します

「ー」地図を縮小します。

「階層ボタン」地図・衛生画像を切り替えます。

現在位置に赤いピンを配置します。以前の位置がわかる様に20個のピンを巡回配置します。

4)Run(走行)画面

地図画面で「Run」タブを押すとRun画面になります。

「Startボタン」でログを開始します。

「Stopボタン」でログを終了します。終了時にcsvファイルにログを保存します。

「RunTypeボタン」は未実装です。歩き・走り・自転車などでカロリー計算パラメータなどを切り替える為に使用予定です。

速度・標高をグラフ表示します。

「<<ボタン」はログ開始から20個のグラフを表示します。

「>>ボタン」はログ最後から20個のグラフを表示します。

「allボタン」はログ全体グラフを表示します。

5)開発メモ

いくつかFlutterで開発する上で困った事が有りますのでメモとして残しておきます。
おそらく、FlutterやDartの初心者ならば同じ様にはまり、感じる事では無いかと思います。

1。#ifdef などのコンパイルスイッチが無い?
 DebugモードとReleaseモードのソース切り替えとして#ifdefなどを使用して居ましたが、Dart言語にはなさそうです。
 代わりに「 if (kDebugMode)  」を使用する。

2。VScodeでコンパイルした場合に、iOSのバージョン設定が問題となった。
  最小OSバージョンを「ios/Runner/info.plist」に次の行を追加して最小OSバージョンを14.5に設定しました。

<!-- info.plist にiOS最小バージョンを設定する例 -->
  <key>MinimumOSVersion</key>
  <string>14.5</string>

3。通常のiOSアプリと同じ様に、GPSやファイル共有を使用するには「ios/Runner/info.plist」に次の行を追加します。

<!-- info.plist にGPS使用許可を設定する例 --> 
   <key>NSLocationWhenInUseUsageDescription</key>
    <string>GPS座標をログの為に使用します</string>
    <key>NSLocationAlwaysUsageDescription</key>
    <string>GPS座標をログの為に使用します.</string>
<!-- info.plist にファイル共有使用許可を設定する例 --> 
   <key>LSSupportsOpeningDocumentsInPlace</key>
    <true/>

4。TABのアイコン画像は「assets/images/」に配置して、ImageIconで設定します

// 画像からiconを作成する例 
ImageIcon(AssetImage('assets/images/map-01.png'))
ImageIcon(AssetImage('assets/images/run-01.png'))
mageIcon(AssetImage('assets/images/set-00.png'))

5。画面の再描画タイミング
 画面全体は「Widget build(BuildContext context)」関数内に記述しますが、画面の更新は「setState」関数を呼ぶ事で実行する(=のが、最初はわかりませんでした)。なので、ボタンが押されたり、1秒タイマーで1秒毎に現在座標を取得して、地図のピンを移動する。。などの処理が実現できます。

// [拡大ボタン]拡大ボタンを押した場合の実行関数例: setState関数を呼ぶ時に、Cameraのzoomを
// +1している。この後Build関数が実行され、拡大地図が表がされる。
  void zoomIn() {
    setState(() {
      _zoom += 1.0;
      _controller.animateCamera(
        CameraUpdate.newCameraPosition(
          CameraPosition(
            target: LatLng(_lat, _lon),
            zoom: _zoom,
          ),
        ),
      );
    });
  }
// [タイマー処理]1秒毎に座標を取得して、マーカテーブルにピンを追加する。
// この後Build関数が実行され、地図が移動してマーカが表示される。
    // 1. Timer.periodic : 新しい繰り返しタイマーを作成します
    // 1秒ごとに _counterを1ずつ足していく
    Timer.periodic(
      // 第一引数:繰り返す間隔の時間を設定
      const Duration(seconds: 1),
      // 第二引数:その間隔ごとに動作させたい処理を書く
      (Timer timer) async {
        // 位置情報を取得する
        final pos = Gps.currentPos;
        _lat = pos.latitude;
        _lon = pos.longitude;
        // マーカーを追加する
        Marker mak = Marker(
          markerId: MarkerId('$_markerNo'),
          position: LatLng(_lat, _lon), //
          infoWindow: InfoWindow(title: '$_markerNo'),
          icon: BitmapDescriptor.defaultMarker,
        );
        // マーカーを描画データテーブルに追加する。最大20個までで、巡回する
        _markTerable[_markerNo] = mak;
        _markerNo++;
        if (_markerNo >= 20) {
          _markerNo = 0;
        }

        //実態がある場合のみ、画面を更新する
        if (mounted == true) {
          // 表示文字の更新
          _timeStr = Gps.getGpsLogTime();
          _distanceStr = Gps.getGpsLogDistance();
          // 画面を更新する
          setState(() {
            _controller.animateCamera(
              CameraUpdate.newCameraPosition(
                CameraPosition(
                  target: LatLng(_lat, _lon),
                  zoom: _zoom,
                ),
              ),
            );
          });
        }
      },
    );

6。地図上のボタン配置とサイズ
 platform_maps_flutterで地図を表示しています。その上にボタンやテキストを配置する方法として「Positioned」を使用して居ます。昔はGUI部品を座標指定で配置するようなGUIビルダーが多く有りましたが、近年はコードで記述する様な形式になりつつあると思われますので、「Positioned」で座標指定する方法が良いのか若干気になる点では有ります(=が、まだまだ初心者なので他の方法が思いつかなかった。良い方法があったら教えてください)。
 また、配置ボタンのサイズ指定ができなかったので「SizeBox」も使用して居ます。

 今まで色々なGUI部品を使用して居ましたが、width/heightはどの部品も持って居たと思いますが、SizeBoxで指定するのはFlutterが初めての経験です。当然ある物としてwidth/heightを探しまくりました。常識から疑う必要があったのが驚きでした(驚き1)。

// [ボタン配置] 地図画面上にボタンを配置する為に、Positionedで位置指定している
      Positioned(
        top: 10, // 上から10ピクセルの位置
        left: 10, // 肥大から20ピクセルの位置
        child: Column(children: [
          SizedBox(
            width: btnSize, // 希望の幅
            height: btnHight1,
            child: FloatingActionButton(
              onPressed: Gps.logSartFlg
                  ? null
                  : () => {
                        Gps.gpsLogStart(),
                      },
              backgroundColor: 
                 Gps.logSartFlg ? Colors.cyan : Colors.grey,
              child: Text("走行時間: $_timeStr"),
            ),
          ),
          const SizedBox(height: 10), // 10ピクセルのスペース
          SizedBox(
            width: btnSize, // 希望の幅
            height: btnHight1,
            child: FloatingActionButton(
              onPressed: Gps.logSartFlg
                  ? null
                  : () => {
                        Gps.gpsLogStart(),
                      },
              backgroundColor: 
                 Gps.logSartFlg ? Colors.cyan : Colors.grey,
              child: Text("走行距離: $_distanceStr"),
            ),
          ),
          const SizedBox(height: 10), // 10ピクセルのスペース
          Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: <Widget>[
              SizedBox(
                width: btnWidth1, // 希望の幅
                height: btnHight1,
                child: FloatingActionButton(
                  onPressed: Gps.logSartFlg
                      ? null
                      : () => {
                            Gps.gpsLogStart(),
                          },
                  backgroundColor: 
                     Gps.logSartFlg ? Colors.grey : Colors.blue,
                  child: const Text("Start"),
                ),
              ),
              SizedBox(
                width: btnWidth1, // 希望の幅
                height: btnHight1,
                child: FloatingActionButton(
                  onPressed: Gps.logSartFlg
                      ? () => {
                            Gps.gpsLogStop(),
                          }
                      : null,
                  backgroundColor: 
                     Gps.logSartFlg ? Colors.blue : Colors.grey,
                  child: const Text("Stop"),
                ),
              ),
              SizedBox(
                width: btnWidth1, // 希望の幅
                height: btnHight1,
                child: FloatingActionButton(
                  onPressed: () => {},
                  child: const Text("RunType"),
                ),
              ),
            ],
          ),
        ]),
      ),

7。Dart言語にはプライベート変数は無い?
 クラス内部のプライベート変数を指定しようとしたら「無い」様です。慣習的にアンダースコア(_)をつけた変数をプライベートと見なす様で驚きでした(驚き2)(=この仕様で良いのかな?)。

// [プライベート変数例]
// Run画面で使用している、プライベート変数例(一部)
// _timeStrや_distanceStrなどアンダースコア(_)を付けた変数が
// プライベート変数です。
// 
class _MyRunPageState extends State<MyRunPage> {
  // 表示様変数データ
  String _timeStr = "00:00:00";
  String _distanceStr = "0.0km";
  String _speedStr = "0.0m/s";
  String _altStr = "0.0m";
  // グラフデータ
  List<FlSpot> _altGraphList = [];
  List<FlSpot> _spdGraphList = [];
  GraphType _graphType = GraphType.all;

  // グラフ最大最小値範囲変数
  double _minAlt = 0, _maxAlt = 0;
  double _minSpd = 0, _maxSpd = 0;
  double _minGraphX = 0;
  final double _xMaxDefault = 20;
  double _maxGraphX = 20;
  int _intervalVal = 5;

6)雑感

 Runloggerの主要機能を作成してみてFlutter/Dartの使い方が少し理解できました。
何も知らない状態から一応は動くアプリまで作成するのに数週間でしたが、初めてObjective-Cでアプリを作成した時よりも簡単に感じました。iOSの知識があるので当たり前かもしれませんが、新しい環境=Flutter/Dartに慣れるのにGitHub CopilotやGeminiなどの手助けが非常に大きかったと思います。この様な環境が簡単に使える現代なら1週間も有れば(かつ若ければ)それなりのアプリが作れそうと思います。

 また今回のアプリではiOSのシミュレータのみで動作テストしていますが、iOS固有の部分はinfo.plistを弄ったぐらいなので、androidへの移植も簡単と思われます。マルチプラットホームとしてiOSへの依存がほとんど無いのはFlutterの大きなアドバンテージと思います。

7)今後

 Flutterと同様にSwiftでもRunlogger(風)を作ってみようかなと思います。SwiftもSwiftUIでFlutter同様にコードでGUIを記述する方法なので面白い比較ができるかもしれません。

 以前「RunloggerPro」としてサーキット走行の加速度もログ保存するアプリが途中で止まったままです。このRunlogger(風)アプリをベースにRTK-GPS・9軸センサーと連携して「RunloggerPro」を復活させようかな。。とも思って居ますが、どうなることや。

8)GUI作成方法について

いつもの様にGeminiさんに聞いてみました。
Flutterの様にマルチプラットフォームの場合、画面がそれぞれ異なりますからコードで記述する方法が何かと都合が良いのでしょう。

<質問>
「近頃のGUI作成は以前の様なGUIエディータからコードで記述する方式に移行しているように思われますが、実際はどうでしょうか」

<Gemini回答>
近年、GUI作成の主流がGUIエディタからコード記述へと大きくシフトしたように感じるというご認識、大変興味深いです。確かに、多くの開発現場でコードによるGUI構築が採用されるようになっており、その背景にはいくつかの要因が考えられます。

1。コードによるGUI構築が主流になりつつある理由
1)柔軟性とカスタマイズ性:
コードでは、GUIエディタでは実現しにくい複雑なレイアウトやインタラクションを自由に設計できます。
再利用可能なコンポーネントを作成し、大規模なアプリケーション開発を効率化することができます。
2)バージョン管理:
コードはバージョン管理システムで管理できるため、開発履歴を辿り、変更点を追跡しやすくなります。
チーム開発において、衝突やバグを減らすことができます。
3)コミュニティとエコシステム:
コードベースのフレームワークやライブラリが豊富に存在し、コミュニティによるサポートも充実しています。
新しい技術やトレンドに素早く対応できます。
4)複雑化するアプリケーション:
モバイルアプリやWebアプリケーションの機能が複雑化するにつれて、より柔軟な開発手法が求められるようになりました。

2。GUIエディタが完全に消滅したわけではない
一方で、GUIエディタも依然として利用されています。
1)プロトタイピング:
GUIエディタは、初期段階のUIデザインやプロトタイピングに適しています。
短時間で視覚的な表現を作成し、アイデアを共有することができます。
2)特定の用途:
特定のプラットフォームやツールとの連携が容易なGUIエディタもあります。
特化した機能を提供するGUIエディタは、特定の開発シーンで活躍します。

3。まとめ
GUI作成の方法は、プロジェクトの規模、開発チームのスキル、要件によって最適なものが異なります。
1)大規模で複雑なアプリケーション: コードによる構築が一般的。
2)迅速なプロトタイピング: GUIエディタが有効。
3)特定のプラットフォームやツールとの連携: GUIエディタが適している場合も。

どちらが優れているかではなく、それぞれのツールや手法の特性を理解し、適切に使い分けることが重要です。

4。今後の展望
1)ローコード/ノーコード開発: GUIエディタの発展により、コードを書かずにアプリ開発ができるようになっています。
2)AIによるUIデザイン支援: AIがUIデザインを自動生成したり、最適なUIを提案したりする技術も発展しています。
コードとGUIエディタの融合: コードとGUIエディタを組み合わせたハイブリッドな開発環境も登場しています。

GUI作成の方法は、今後も進化を続け、より効率的で創造的な開発を可能にしていくでしょう。

9)GitHubに公開します

今回作成した「Runlogger Flutter Test」環境をGitHubに公開しました。
地図を表示するアプリのサンプルを探すと「FlutterでGoogleMap。。。」みたいなのが多数でiOSのMapを使用する例は余り有りませんでした。またサンプルの地図アプリは画面1つで、PIN配置やルート検索するぐらいで「アプリ」に必要な複数画面のサンプルなどは有りませんでしたので、参考になると思いGitHubに公開しました。
   GitHubのURL:https://github.com/amru195704/runlogger

Screenshot


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

PAGE TOP