Flutter のアプリのサンプルに少しずつ適当に手を加えてみる（Android Studio ＋ ChatGPT を使いつつ）

以前も、以下のようにいくつか記事を書いている、Android Studio を使った Flutterアプリ開発の話です。

- [Flutter を使った Androidアプリ開発など：TensorFlow形式から変換した LiteRT（TensorFlow Lite）のモデルを使った際のメモ - Qiita](https://qiita.com/youtoy/items/83fca076e981d157cd5e)
- [FVM で Windows上での Flutter の開発構築のメモ（＋ Androidアプリの開発環境も） - Qiita](https://qiita.com/youtoy/items/edca3e29ac3e39108579)
- [Flutter・Androidアプリの開発環境を作った時のメモ（Mac） - Qiita](https://qiita.com/youtoy/items/434020813fc4dde00131)

今回、サンプルとして出てくる内容に対して、少しずつ適当に手を加えてみてコード・挙動の変化を見てみました。

思いつきでの追加実装を、ChatGPT（o3-mini-high）も使いつつやっていきます。


## 実際に試していく

### ベースとなるコード

ベースとなるサンプルは、以下のコードです。

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

以下のような動作をするものになります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/KXa_TUQ-GAU?si=l71J9CtPAiVSZI0Q" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


### 書き換え：1回目

書きかえの 1回目です。

とりあえず、UI のパーツと数字を減少させる機能を、それぞれ追加してみます。

#### プロンプト

元のコードを維持しつつ、少しだけ手を加えたコードを見てみたかったので「先ほどのコードに最小限の変更を加える形で実現して。」という文章をつけています（これが影響を及ぼしているかは未確認ですが）。

> 【プロンプト】
右下にある ＋ ボタンの左隣に ー ボタンを追加して。
ー ボタンが押されると数字を１ずつ減らしてください。
ー ボタンは青系の色で。
先ほどのコードに最小限の変更を加える形で実現して。

#### 出力されたコード

出力されたコードは以下のとおりです。

ビルドしてそのまま動くものが出てきていました。

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  // 既存のインクリメント処理
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  // 新たに追加したデクリメント処理
  void _decrementCounter() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      // floatingActionButton を Row に変更して、左右にボタンを配置
      floatingActionButton: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            onPressed: _decrementCounter,
            tooltip: 'Decrement',
            backgroundColor: Colors.blue, // 青系の色指定
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}
```

主に、以下のコメントが挿入されているあたりに、コードが追加されているのが分かります。

- 新たに追加したデクリメント処理
- floatingActionButton を Row に変更して、左右にボタンを配置

#### 得られた結果

アプリの画面のスクリーンショットを掲載してみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9db7b58e-dc63-4e64-84e6-b014cf2f4de4.png" width=50%>

コードで少し気になる部分はあるものの、機能的には意図通りの実装が行われました。


### 書き換え：2回目

次に、UIパーツの表示位置を変えてみたり、テキストを表示する部分を追加してみます。

#### プロンプト

プロンプトは以下のとおりです。

> 【プロンプト】
中央にある固定のテキストと数字の表示を、センタリングは維持したまま、縦方向は上から1/3くらいの位置になるようにして。
また、下から1/3くらいの場所に、直近で押されたボタンが「インクリメント」「デクリメント」のどちらかを表示する動的なテキストを表示させて。
先ほどのコードに最小限の変更を加える形で実現して。

#### 出力されたコード

出力されたコードは以下のとおりで、そのまま動作するものが出力されました。

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  String _lastAction = ''; // 直近の動作を記憶するための状態変数

  void _incrementCounter() {
    setState(() {
      _counter++;
      _lastAction = 'インクリメント';
    });
  }

  void _decrementCounter() {
    setState(() {
      _counter--;
      _lastAction = 'デクリメント';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      // Stack を使い、固定テキスト＆カウンター、動的テキストを別々に配置
      body: Stack(
        children: [
          // 上部1/3程度に固定テキストとカウンター表示を配置
          Align(
            alignment: const Alignment(0, -0.33),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                const Text('You have pushed the button this many times:'),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
          // 下部1/3程度に直近で押されたボタンの動的テキストを配置
          Align(
            alignment: const Alignment(0, 0.33),
            child: Text(
              _lastAction,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ),
        ],
      ),
      // 以前追加したフローティングアクションボタン（－ボタンは青色）
      floatingActionButton: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            onPressed: _decrementCounter,
            tooltip: 'Decrement',
            backgroundColor: Colors.blue,
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}
```

追加された箇所などは、今回もコメントをもとに確認できました。


#### 得られた結果

アプリの画面のスクリーンショットを掲載してみます。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f1b97df6-fea3-431e-b2d8-19b1f2e23571.png" width=50%>

これも、機能的には意図通りのものになりました。


### 書き換え：3回目

次は、パッケージを追加して機能を足してみます。

利用するパッケージは、以前見かけて気になった以下の記事を見て、適当に 1つ選んでみました。

●mixi2ライセンスページから学ぶ、便利なFlutterパッケージ【120over】 - くらげになりたい。
 https://www.memory-lovers.blog/entry/2024/12/19/085505

[インテント](https://developer.android.com/guide/components/intents-common?hl=en)で別アプリの機能を呼び出すというものが、挙動として分かりやすそうです。

具体的には、以下を使う処理になります。

●android_intent_plus | Flutter package
 https://pub.dev/packages/android_intent_plus

![2025-03-26_01-57-18.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/adc5df58-4bf5-4567-8345-585aefb4d78f.jpeg)

#### プロンプト

そこで、画面左下にボタンの追加と、[android_intent_plus](https://pub.dev/packages/android_intent_plus) を使ったカメラアプリの起動追加してもらうことにします。

プロンプトは以下としました。

> 【プロンプト】
画面左下にボタンを１つ追加して。
そのボタンが押された時は、android_intent_plus を使ったカメラアプリの起動を行って。
先ほどのコードに最小限の変更を加える形で実現して。

#### 出力されたコード

出力されたコードは以下のとおりです。

今回は、パッケージ追加を行っていないので、以下をそのままビルドしても動きません。

```dart
import 'package:flutter/material.dart';
import 'package:android_intent_plus/android_intent.dart'; // 追加

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  String _lastAction = ''; // 直近の動作を記憶するための状態変数

  void _incrementCounter() {
    setState(() {
      _counter++;
      _lastAction = 'インクリメント';
    });
  }

  void _decrementCounter() {
    setState(() {
      _counter--;
      _lastAction = 'デクリメント';
    });
  }

  // 追加: カメラアプリ起動用のメソッド
  void _launchCamera() async {
    final AndroidIntent intent = AndroidIntent(
      action: 'android.media.action.IMAGE_CAPTURE',
    );
    await intent.launch();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      // Stack によるレイアウトに、左下配置のボタンを追加
      body: Stack(
        children: [
          Align(
            alignment: const Alignment(0, -0.33),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                const Text('You have pushed the button this many times:'),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
          Align(
            alignment: const Alignment(0, 0.33),
            child: Text(
              _lastAction,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ),
          // 追加: 画面左下にカメラ起動用のボタンを配置
          Align(
            alignment: Alignment.bottomLeft,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: FloatingActionButton(
                onPressed: _launchCamera,
                tooltip: 'Launch Camera',
                child: const Icon(Icons.camera_alt),
              ),
            ),
          ),
        ],
      ),
      // 既存の右下に配置したフローティングアクションボタン（インクリメント・デクリメント）
      floatingActionButton: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            onPressed: _decrementCounter,
            tooltip: 'Decrement',
            backgroundColor: Colors.blue,
            child: const Icon(Icons.remove),
          ),
          const SizedBox(width: 8),
          FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}
```

今回も、処理が追加された部分は、コメントを見ればすぐに見つけられそうです。

インテントの処理では `action: 'android.media.action.IMAGE_CAPTURE'` が使われていました。

Android の開発者ガイドのページで言うと、以下に該当する機能が使われているかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/c8ea8b41-ca4c-4a57-862d-969c43412baf.png)

#### パッケージの追加

現状は、パッケージの追加を行っていないので、ビルドをすると当然エラーが出ます。

![2025-03-26_01-54-14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ab11896b-80c3-4f62-83a1-26e8f37f2eb5.jpeg)

パッケージ追加は前に行ったことがあるので、今回は手動ですませてしまいます。

以下のページの「Installing」を選ぶと、コマンドを使ったパッケージの追加方法などが表示されます。

●android_intent_plus | Flutter package
 https://pub.dev/packages/android_intent_plus

![2025-03-26_01-59-15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/1db62c25-16e1-46dd-914d-7cf6e3035ffc.jpeg)

Android Studio の左下メニューの以下を選び、上記に書かれたコマンドを入力します。

![2025-03-26_02-01-09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0091e2f9-eda6-4ee9-8a2b-1bd35d256ec1.jpeg)


自分は FVM を使って環境を作っているので（そしてエイリアスの設定などは行っていないので）、 `fvm flutter pub add android_intent_plus` というコマンドを実行する形になります。

コマンド実行後、処理が終わった後に以下の「pubspec.yaml」を見ると、 `android_intent_plus: ^5.3.0` という行が追加されてました。

![2025-03-26_02-04-36.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/c55c13b3-a99e-4930-be11-453d89225edb.jpeg)

```yaml:「pubspec.yaml」の一部抜粋
dependencies:
  flutter:
    sdk: flutter

  ...
  android_intent_plus: ^5.3.0
```

#### ビルドエラーと対処

それでは、あらためてビルドしてみます。

しかしエラーが出て、その後に以下のメッセージが表示された状態になりました。

![2025-03-26_02-16-54.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/fb7eabeb-805b-436f-aa19-9a4f9677c8d5.jpeg)

どうやら、AGP のバージョンに絡むエラーのようです。
メッセージの下の方にある 2つのリンクを開き、ざっと内容も見てみました。

●Android Gradle Plugin failed with JavaVersion.VERSION_11 and OpenJDK 21 ea [294137077] - Issue Tracker
 https://issuetracker.google.com/issues/294137077?pli=1

●Android plugins: `Failed to transform core-for-system-modules.jar to match attributes` errors with JDK 21 · Issue #156304 · flutter/flutter
 https://github.com/flutter/flutter/issues/156304

とりあえず解決策の 1つは、自分の環境だと `android/settings.gradle` を変えてやる方法があるようでした。

以下の `id "com.android.application" version "8.1.0" apply false` という部分の `"8.1.0"` を `"8.2.1"` にするだけです。

![2025-03-26_02-19-21.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/d9c7e1fa-c70d-4e21-88cf-68acf5632878.jpeg)

#### 動作確認

その後、再ビルドしたところ、無事に実機で動作させられました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/Y5uAzf3bgxc?si=TjZSTajFNWk1mnLg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


## おわりに

とりあえず、適当な UI・機能の追加を試しつつ、どんなコードが追加されるかを見てみました。

毎回、変更内容がシンプルで小規模だったので、そのまま動作するコードが出力された形でした。