「1つのコードで iPhone と Android の両方で動くアプリを作りたい」——それを叶えてくれるのが Flutter です。今回はMac上でFlutterの開発環境を整え、簡単なカウンターアプリを作って、iOSシミュレータとAndroidエミュレータの両方で実際に動かしてみました。

つまずいたポイントや、再現に必要な設定値もすべて記録しています。

📱 この記事はシミュレータ中心です
iPhoneやAndroidの実機への転送には、実機の接続・開発者モードの有効化・Apple IDでの署名などが必要です。本記事では、まず誰でも試せるシミュレータ/エミュレータでの動作確認に絞って解説します(実機転送の概要は最後に触れます)。

Flutterとは何か(Dartという言語)

FlutterはGoogleが開発したオープンソースのUIフレームワークです(公式サイト)。最大の特徴は、1つのソースコードから iOS・Android・Web・デスクトップ向けのアプリを作れる点です。

Flutter 1つのコードで2つのOS

使う言語は Dart(ダート)。これもGoogle製の言語で、JavaやJavaScriptに似た書きやすい文法が特徴です。

  • 1コードで複数OS:iOSとAndroidで同じコードが動く
  • 独自描画エンジン:OSの部品に頼らず自前で画面を描くため、両OSで見た目が完全に揃う
  • ホットリロード:コードを保存した瞬間に画面へ反映され、開発が速い

開発環境のセットアップと正確なバージョン

今回の検証環境です。再現する場合の参考にしてください。

項目 バージョン
macOS26.5.1(Apple Silicon)
Flutter3.44.2(stable チャンネル)
Dart3.12.2
Xcode(iOS用)26.5
Android Studio / SDKSDK 36(Android 16)

Flutter SDK の導入

公式手順に従ってSDKを展開し、PATHを通します。すでに入っている場合は flutter upgrade で最新化できます。

# 最新安定版へアップグレード
flutter upgrade

# バージョン確認
flutter --version
# → Flutter 3.44.2 • channel stable / Dart 3.12.2

環境チェック:flutter doctor

flutter doctor で、iOS・Android開発に必要なツールが揃っているか診断できます。

flutter doctor
[✓] Flutter (Channel stable, 3.44.2, on macOS 26.5.1 darwin-arm64)
[✓] Android toolchain - develop for Android devices (Android SDK 34.0.0)
[!] Xcode - develop for iOS and macOS (Xcode 26.5)
    ! CocoaPods 1.15.2 out of date (1.16.2 is recommended).
[✓] Chrome - develop for the web
[✓] Connected device (2 available)
[✓] Network resources

CocoaPods が古いという警告が出ましたが、これは後述の通り、プラグインを使わない今回のアプリでは支障ありませんでした。

カウンターアプリを作る

ボタンで数字を増減させる「カウンターアプリ」を作ります。プロジェクトは flutter create で生成します。--org でBundle IDの前半(組織名)を指定できます。

flutter create --org com.eightblog --project-name counter_app counter_app
cd counter_app

これで131個のファイルが生成され、iOS・Android・Web・デスクトップ用のひな形がすべて揃います。アプリ本体のコードは lib/main.dart の1ファイルだけです。

Dartの基本文法をコードで解説

標準のひな形を少し拡張し、「1増やす」「1減らす」「リセット」の3ボタンにしました。これが lib/main.dart の中身です。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());   // アプリを起動する入口
}

// 状態を持たない部品は StatelessWidget
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'カウンターアプリ',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const CounterPage(title: 'カウンターアプリ'),
    );
  }
}

// 数字が変化する画面は、状態を持つ StatefulWidget
class CounterPage extends StatefulWidget {
  const CounterPage({super.key, required this.title});
  final String title;

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;   // カウントの値(状態)

  void _increment() {
    setState(() => _counter++);          // setStateで画面を更新
  }

  void _decrement() {
    setState(() {
      if (_counter > 0) _counter--;       // 0未満にはしない
    });
  }

  void _reset() {
    setState(() => _counter = 0);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('ボタンを押した回数:', style: TextStyle(fontSize: 18)),
            Text('$_counter', style: const TextStyle(fontSize: 64, fontWeight: FontWeight.bold)),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FilledButton.tonalIcon(onPressed: _decrement, icon: const Icon(Icons.remove), label: const Text('1減らす')),
                FilledButton.icon(onPressed: _increment, icon: const Icon(Icons.add), label: const Text('1増やす')),
              ],
            ),
            TextButton(onPressed: _reset, child: const Text('リセット')),
          ],
        ),
      ),
    );
  }
}

Dart初心者がここで覚えたいポイントです。

  • すべてがWidget(部品):画面・文字・ボタン・余白まで、UIはWidgetの組み合わせで作る
  • StatelessWidget と StatefulWidget:変化しない部品か、状態(数値など)を持つ部品かで使い分ける
  • setState():状態を変えたら setState() で囲む。これでFlutterが画面を再描画する
  • $変数:文字列の中に '$_counter' と書くと変数の値を埋め込める
  • =>(アロー):1行だけの関数は () => 処理 と短く書ける

プロジェクトの細かな設定(Bundle ID等)

アプリを識別するための設定値です。flutter create --org の指定がここに反映されています。

項目
アプリ名counter_app
バージョン(pubspec)1.0.0+1
iOS Bundle IDcom.eightblog.counterApp
iOS Deployment TargetiOS 13.0
Android applicationIdcom.eightblog.counter_app
Android SDK(min/target/compile)Flutterのデフォルト値を継承

依存パッケージは pubspec.yaml で管理します。今回の追加依存はアイコン用の cupertino_icons のみで、ネイティブプラグインは使っていません。

name: counter_app
version: 1.0.0+1
environment:
  sdk: ^3.12.2
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0
flutter:
  uses-material-design: true

iOSシミュレータで動かす

まずiOSシミュレータを起動します。今回は iPhone 17 Pro(iOS 26.5)を使いました。

# シミュレータ一覧を確認して起動
xcrun simctl list devices available | grep iPhone
xcrun simctl boot "iPhone 17 Pro"

# アプリをこのシミュレータで実行
flutter run -d "iPhone 17 Pro"

初回ビルドは約100秒かかりました。ビルドが終わると、シミュレータにアプリが自動で表示されます。「1増やす」を7回タップした状態がこちらです。

iOSシミュレータで動くカウンターアプリ

右上の「DEBUG」帯は、デバッグビルドであることを示すFlutterの目印です(リリースビルドでは消えます)。

Androidエミュレータで動かす

次にまったく同じコードをAndroidで動かします。Android Studioで作成した Pixel 7(Android 16 / API 36)エミュレータを起動し、デバイスを指定して実行するだけです。

# Androidエミュレータを起動
emulator -avd Pixel_7_API_36

# 同じアプリをAndroidで実行
flutter run -d emulator-5554

Androidは初回にGradleビルドが走るため約200秒かかりました。結果がこちらです。iOSとAndroidで、まったく同じコードから同じ見た目のアプリが動いています。

iOSとAndroidで同じアプリが動作

これがFlutterの真骨頂です。独自の描画エンジンで画面を描くため、2つのOSでボタンの形も色も完全に揃っています。

ハマったポイントと対処

ハマり①:CocoaPods が古いという警告

flutter doctor で「CocoaPods 1.15.2 out of date」という警告が出ました。CocoaPodsはiOSのプラグイン管理に使われるツールです。

  • 症状:Xcodeの項目に [!] マークが付く
  • 影響:今回のようなプラグイン無しのアプリでは支障なし。iOSシミュレータで問題なく動いた
  • 対処:将来プラグイン(カメラ・位置情報など)を使う場合は更新が必要。sudo gem install cocoapods で更新する(管理者パスワードの入力が必要)

ハマり②:初回ビルドが遅い

iOS・Androidとも、初回だけビルドに時間がかかります(iOS約100秒・Android約200秒)。2回目以降や、ホットリロードは一瞬です。「固まった?」と思っても待つのが正解でした。

ハマり③:実機はネットワーク越しに検出されようとする

flutter devices 実行時に、同じネットワーク上のiPhoneを検出しようとしてエラーが出ることがありました。シミュレータ検証では -d でデバイスを明示指定すれば問題ありません。

💡 シミュレータは複数同時起動に注意
iOSシミュレータもAndroidエミュレータも、複数同時に起動するとMacの負荷が一気に上がります。検証する側だけを1台起動し、終わったら停止するのがおすすめです。

React Nativeとの比較

クロスプラットフォーム開発のもう一つの定番が React Native(Meta製)です。両者の違いを整理します。

FlutterとReact Nativeの比較
  • Flutter:Dartを学ぶ必要はあるが、2つのOSで見た目が完全に揃い、動作も高速。ゼロからモバイルアプリを始めるならこちら
  • React Native:JavaScript/TypeScriptが使えるので、Web(React)の経験者には入りやすい。OSネイティブの部品を使うため、各OSらしい見た目に寄る

「Webの知識を活かしたい」ならReact Native、「モバイルアプリそのものを綺麗に作りたい」ならFlutter、というのが私の感想です。

まとめ

  • Flutter 3.44.2 / Dart 3.12.2 で、1つのコードからiOS・Android両対応のアプリを作成
  • カウンターアプリでDartの基本(Widget・StatefulWidget・setState)を学習
  • iPhone 17 Pro(iOS 26.5)Pixel 7(Android 16)のシミュレータで同一コードが同じ見た目で動作
  • ⚙️ 設定:Bundle ID は com.eightblog.counterApp、iOS Deployment Target 13.0、依存はcupertino_iconsのみ
  • ⚠️ ハマり:CocoaPods古い警告(プラグイン無しなら問題なし)・初回ビルドが遅い

「1つのコードが2つのOSでそのまま動く」体験は、想像以上に感動的でした。シミュレータだけでもアプリ開発の流れは十分つかめます。次のステップとして、実機への転送や、カメラ・通知などのプラグイン活用にも挑戦してみたいです。

それでは、今回はここまで。ありがとうございました😊