🧘

生成AIを使って健康アプリをリリースしました!

2024/01/26に公開

アプリを公開しました!

健康アプリを作りました!

こちらからダウンロード出来ます!ぜひ使ってみてください。
https://apps.apple.com/jp/app/1日1回だけ腹筋するアプリ/id6471345904

よくある腹筋計測アプリです。
特徴は達成条件が 1日1回以上 と超ミニマムとなっているので、継続しやすいのが特徴です。

https://www.youtube.com/shorts/w8LssdguBVg

画面だけ撮影しているので何が起こっているかよくわからん感じですが、カウントアップごとに頑張って腹筋してます。
日別の合計回数や、継続日数、トータル合計回数なども見えています。

実はこのアプリ、生成AIで作りました!

この記事はアプリ説明がメインではなく、
このアプリ製作をChatGPTやGitHub Copilotなどの生成AIにほぼほぼ頼ることで作ったので、そのアプローチ方法についてまとめます。

つくりかた

小さくはじめる

まずアプリに必要な機能を全網羅して、それらの仕様を詳細に…と
「欲しい物をとにかく全部くれ!」と指示すると、うまく進めることが出来ません。

実際の開発と同様、インクリメンタルに小さな機能を積み上げていくように進めていきます。

まずがクリティカルかつ基本的な加速度センサーを使った数値の変化を観測できるようにします。
スマホのXYZ軸の傾きに反応して、数値が変化するまでのコードを生成してもらいます。
その上で今回のユースケースとして、胸に抱えた状態で腹筋をすると1回としてカウントしてくれるようなコードを生成してもらいましょう。

実際の指示した内容は以下のとおりです。

I would like to create an application for measuring abdominal muscle counts using an accelerometer.
Please modify this code to count up and display the number of sit-ups by tilt detection using the accelerometer.

(和訳はこちら)

加速度センサーを使って腹筋回数を計測するアプリケーションを作りたいと思います。
加速度センサーを使った傾き検知で腹筋回数をカウントアップして表示するコードを修正お願いします。

すると以下のように返信が帰ってきます。

(長いので開いて見てください)

To modify the given Flutter application to count the number of sit-ups by detecting tilt with the accelerometer, we need to set up a threshold for the tilt angle that will indicate when a sit-up is performed. We'll use the z axis reading since a sit-up typically involves moving forwards and backwards in a plane parallel to the ground, which would cause significant changes in the z axis if the device is placed on the user's torso.

Here's an outline of the changes needed:

  1. Add a variable to count the number of sit-ups.
  2. Define a threshold for the z axis accelerometer value that will indicate a sit-up.
  3. Update the count when the threshold is crossed.
  4. Ensure that the count only increments once per sit-up, not continuously as the threshold is crossed.
  5. Display the count on the UI.

Here's the modified code:

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]);
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Sit-Up Counter',
      home: MyHomePage(title: 'Sit-Up Counter'),
    );
  }
}

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

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<double>? _accelerometerValues;
  final _streamSubscriptions = <StreamSubscription<dynamic>>[];

  // Variables for counting sit-ups
  int _sitUpCount = 0;
  bool _isUserUp = false;
  final double _sitUpThreshold = 9.81 / 2; // Adjust the threshold accordingly

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title ?? 'Sit-Up Counter'),
        elevation: 4,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Sit-ups: $_sitUpCount',
              style: Theme.of(context).textTheme.headline4,
            ),
            const SizedBox(height: 20),
            Text('Accelerometer: ${_accelerometerValues?.map((v) => v.toStringAsFixed(1)).join(', ')}'),
          ],
        ),
      ),
    );
  }

  
  void dispose() {
    for (final subscription in _streamSubscriptions) {
      subscription.cancel();
    }
    super.dispose();
  }

  
  void initState() {
    super.initState();
    _streamSubscriptions.add(
      accelerometerEvents.listen(
        (AccelerometerEvent event) {
          // Use setState to rebuild the UI with new values
          setState(() {
            _accelerometerValues = <double>[event.x, event.y, event.z];

            // Check if the device has been tilted enough to count as a sit-up
            if (event.z > _sitUpThreshold && !_isUserUp) {
              _isUserUp = true;
              _sitUpCount++;
            } else if (event.z < _sitUpThreshold && _isUserUp) {
              // This condition checks if the user has returned to the initial position
              _isUserUp = false;
            }
          });
        },
        onError: (e) {
          showDialog(
            context: context,
            builder: (context) {
              return const AlertDialog(
                title: Text("Error"),
                content: Text("Accelerometer events error: $e"),
              );
            },
          );
        },
      ),
    );
  }
}

In this code, the _sitUpCount variable holds the number of sit-ups performed. The _sitUpThreshold variable defines the z-axis value that we consider the user to have performed a sit-up; this threshold may need to be adjusted based on how you expect the user to position their device. The _isUserUp variable is a flag to indicate when the user is in the 'up' position, to avoid counting multiple sit-ups if the accelerometer data fluctuates around the threshold.

The initState method sets up a listener for accelerometer events. When a significant change is detected in the z value (above the _sitUpThreshold), it increments the sit-up counter. It also checks if the user has returned to the initial position before it can increment the counter again.

The build method updates to display the number of sit-ups, and the accelerometer data is still displayed for reference. You may want to remove or hide the accelerometer data in a production app, as it's mostly useful for debugging purposes.

コードをざっと出力することが出来ました!


しかしながら、実は最初のコード生成では、上手くいきませんでした。
加速度センサーの変化方向に、Z軸を使うようなコードになってしまうのです。(使いたいのはY軸)
私はコードから仕様を理解することで、y軸方向の数値変化を使用するよう修正することが可能でしたが、挙動仕様を伝えることだけではドンピシャなコードを生成させることができませんでした。
ここだけ、手動で利用するコードを変更しました。

コードのミスや思ったとおりにならない挙動についても、生成AIに指摘することで修正することももちろん可能ですが、どうしてもうまく行かない場合は微調整する場面もありました。

インクリメンタルにすすめる

まずは加速度センサーを使ってカウントアップするような基本的な機能だけ、実装することが出来ました!

こんな形でアプリケーションを形作るのに必要な小さなタスクに分割して、徐々に機能を足しながら作っていく形が、生成AIを使った開発ではうまくいく進め方になります。

このどんなタスクが必要かを考える/より単純なタスクに分割していくプロセスは、頑張って考えましょう!(もちろんこの部分を生成AIに手伝ってもらうこともできます。)

1つ1つのタスクは小さければ小さいほど、失敗する可能性は少なくなります。

例)
- カウントアップ時に振動してカウントアップしたことをわかりやすくする。
- データベースに保存する。
- 履歴を表示する。
- 継続日数を表示する。
- トータル合計回数を表示する。
- SNSへのシェア機能をつける。
- データのインポート・エクスポート機能をつける。
- ↑のためのメニューメニューを用意する

各タスクごとに、生成AIに指示していく形で進めていきます。
いくつか↓に例を示します。

カウントアップ時に振動させる

I would like to make the terminal shake a little during the count up.
Please give me step-by-step instructions on how I should modify the code so that it is easy for a beginner to understand.
(カウントアップの時に端末が少し揺れるようにしたいです。
初心者にもわかりやすいように、どのようにコードを修正すればよいか、順を追って教えてください。)

画面表示の条件分岐

It worked fine! Thank you very much!
For users who haven't done a single sit-up today, "Let's do one sit-up!" message only, otherwise the screen display will be the same as before.
(うまくいきました!ありがとうございました!
今日一度も腹筋をしていないユーザーには、「腹筋を一回しましょう!」というメッセージのみ表示され、それ以外は今まで通りの画面表示となります。)

メニューボタンの追加

It worked fine! Thank you very much!
I would like to be able to import and export my abdominal log database in csv. First I would like to add a menu button in the upper right corner to open the menu.
(うまくいきました!ありがとうございました!
腹部のログデータベースをcsvでインポート、エクスポートできるようにしたいのですが。まず、右上にメニューを開くボタンを追加したいと思います。)

1回毎に紙吹雪を散らす

I would like to have confetti displayed when the number of sit-ups reaches one or more.
Please use this package.
https://pub.dev/packages/confetti
(腹筋回数が1回以上になったら紙吹雪を表示させたい。
こちらのパッケージをご利用ください。)

うまくいかないときにどうするか

何度も生成AIをやり取りを続けていると、以前の文脈をうまく把握出来ていない/忘れているケースがあったりします。
すると、提示されるコードの一部が巻き戻ることがあります。

そういう時には、コードの全量を提示することで簡単に現状を同期・リセットすることが出来ます。
私はコード全量を渡しつつ、This is current code. Explain this code.と書くことが多いです。

例)

This is current code. Explain this code.

[~ コード全量 ~]

今回のケースでは簡単なものを作っているため、全てのコードをmain.dartに書く雑な仕事になっています…
が、逆にコード全体を簡単に共有できるので、そういう意味では開発しやすかったのかもしれません笑。

まとめ

このようなアプローチで、簡単なアプリであれば知識がない中でも十分開発していくことができました。(DartやFlutterはほとんど触ったことがありませんでした。)
もちろんまだまだ上手くコードを生成できない所はありますが、生成AIは開発者の良き味方になってくれるはずです!
さらに技術やエコシステムが発展していくことで、開発技術に触れたことがない人でもプロダクトを作ったりできる、そんな未来が来るかもしれません。

参考

成果物

コードはこちらに公開しています。

プロンプトとやり取り一覧

ChatGPTでコード生成の際にやり取りしていた内容を公開しておきます。

GitHubで編集を提案

Discussion