「1つのコードで iPhone と Android の両方で動くアプリを作りたい」——それを叶えてくれるのが Flutter です。今回はMac上でFlutterの開発環境を整え、簡単なカウンターアプリを作って、iOSシミュレータとAndroidエミュレータの両方で実際に動かしてみました。
つまずいたポイントや、再現に必要な設定値もすべて記録しています。
iPhoneやAndroidの実機への転送には、実機の接続・開発者モードの有効化・Apple IDでの署名などが必要です。本記事では、まず誰でも試せるシミュレータ/エミュレータでの動作確認に絞って解説します(実機転送の概要は最後に触れます)。
Flutterとは何か(Dartという言語)
FlutterはGoogleが開発したオープンソースのUIフレームワークです(公式サイト)。最大の特徴は、1つのソースコードから iOS・Android・Web・デスクトップ向けのアプリを作れる点です。
使う言語は Dart(ダート)。これもGoogle製の言語で、JavaやJavaScriptに似た書きやすい文法が特徴です。
- 1コードで複数OS:iOSとAndroidで同じコードが動く
- 独自描画エンジン:OSの部品に頼らず自前で画面を描くため、両OSで見た目が完全に揃う
- ホットリロード:コードを保存した瞬間に画面へ反映され、開発が速い
開発環境のセットアップと正確なバージョン
今回の検証環境です。再現する場合の参考にしてください。
| 項目 | バージョン |
|---|---|
| macOS | 26.5.1(Apple Silicon) |
| Flutter | 3.44.2(stable チャンネル) |
| Dart | 3.12.2 |
| Xcode(iOS用) | 26.5 |
| Android Studio / SDK | SDK 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 resourcesCocoaPods が古いという警告が出ましたが、これは後述の通り、プラグインを使わない今回のアプリでは支障ありませんでした。
カウンターアプリを作る
ボタンで数字を増減させる「カウンターアプリ」を作ります。プロジェクトは 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 ID | com.eightblog.counterApp |
| iOS Deployment Target | iOS 13.0 |
| Android applicationId | com.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: trueiOSシミュレータで動かす
まず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回タップした状態がこちらです。
右上の「DEBUG」帯は、デバッグビルドであることを示すFlutterの目印です(リリースビルドでは消えます)。
Androidエミュレータで動かす
次にまったく同じコードをAndroidで動かします。Android Studioで作成した Pixel 7(Android 16 / API 36)エミュレータを起動し、デバイスを指定して実行するだけです。
# Androidエミュレータを起動
emulator -avd Pixel_7_API_36
# 同じアプリをAndroidで実行
flutter run -d emulator-5554Androidは初回にGradleビルドが走るため約200秒かかりました。結果がこちらです。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: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でそのまま動く」体験は、想像以上に感動的でした。シミュレータだけでもアプリ開発の流れは十分つかめます。次のステップとして、実機への転送や、カメラ・通知などのプラグイン活用にも挑戦してみたいです。
それでは、今回はここまで。ありがとうございました😊