公開日 2026-08-20

Flutterアプリを社内配布する(Android APK サイドロード + MDM 概要)

Flutter の signed release APK を Android 端末へ社内配布する流れを整理し、adb install、手動サイドロード、MDM へ渡す項目まで説明できるようにする。

目次

  1. 1. ゴールと非対象
  2. 対象読者
  3. この記事で到達する状態
  4. 非対象
  5. 2. 先に配布方法の全体像を掴む
  6. 3. 配布確認用の最小アプリを用意する
  7. 3-1. 環境構築と前提記事をそろえる
  8. 3-2. Flutter プロジェクトを作成する
  9. 3-3. エミュレーターを起動する
  10. 3-4. lib/main.dart を書き換える
  11. コードのポイント
  12. 3-5. flutter run で起動する
  13. 4. release APK を作成する
  14. 4-1. pubspec.yaml の version を更新する
  15. 4-2. release 署名を設定する
  16. コードのポイント
  17. 4-3. release APK を作成する
  18. 5. まずは adb install で配布する
  19. 6. 利用者向けの手動サイドロードを確認する
  20. 7. 配布後に確認する項目を固定する
  21. 8. MDM 配布へ広げるときの整理項目
  22. 9. まとめ

Flutterの環境切替と配布前チェック(flavor / release build / 権限確認) の次に整理したいのが、できた APK をどう渡し、どう入れてもらい、配布後に何を確認するかです。flutter build apk 自体は単純ですが、端末側のインストール経路・上書き条件・MDM との境界は曖昧になりがちです。この記事では Android に絞り、release APK の作成から手動サイドロード、MDM へ渡す項目の整理までをつなげます。

1. ゴールと非対象

対象読者

  • Flutter プロジェクトを flutter runflutter build apk までは試した人
  • Android 向け社内アプリを Google Play ではなく、手動配布または MDM 配布で回したい人
  • 署名、release build、配布前チェックまでは進んだが、利用者へどう届けるかまだ整理できていない人

この記事で到達する状態

  • release APK を作り、出力先を迷わず確認できる
  • adb install -r で検証端末へ上書きインストールできる
  • ファイルアプリから手動サイドロードする流れを説明できる
  • applicationIdversionCode が更新可否を決めると理解できる
  • MDM 担当へ何を渡せばよいかを一覧で整理できる

非対象

  • iOS の Ad Hoc 配布や TestFlight
  • Google Play Console への公開
  • Intune や Workspace ONE など特定 MDM 製品の画面操作
  • Sentry や Crashlytics など配布後監視

今回は Android の社内配布に絞ります。iOS、ストア公開、運用監視は後続へ分け、まずは「APK を作り、配り、更新ルールまで理解する」状態を固めます。

2. 先に配布方法の全体像を掴む

社内配布では、誰が配るかで使う経路が変わります。

方法向いている場面長所先に気をつける点
adb install開発者、検証担当、手元の実機確認最短で差し替えられるadb が使える PC とケーブル接続が必要
ファイル経由の手動サイドロード現場端末へ単発で配る利用者が実際に触る導線に近い未知のアプリのインストール許可が必要な場合がある
MDM 配布複数端末へ繰り返し配る配布先と更新タイミングを管理しやすいMDM 担当へ渡す情報を先に整理する必要がある
flowchart LR
  A[Flutter project] --> B[release APK を作成]
  B --> C[adb install で検証端末へ配布]
  B --> D[Download へ送って手動サイドロード]
  C --> E[version と package を確認]
  D --> E
  E --> F[配布後チェック]
  F --> G[MDM へ渡す項目を整理]

見るべき点は「インストールできたか」だけではありません。

  • 更新版を上書きできるか
  • 利用者がどこで許可操作をするか
  • MDM へ移すときに APK 以外の何が必要か

この 3 点を先に分けておくと、配布方法が変わっても説明がぶれにくくなります。

3. 配布確認用の最小アプリを用意する

3-1. 環境構築と前提記事をそろえる

環境構築がまだの場合は Windows 11で始めるFlutter開発環境 を先に参照してください。アプリ名、アイコン、スプラッシュも含めて Android 側の見た目と署名をまとめて整えたい場合は Flutterアプリのネイティブ設定を整える(アプリ名 / アイコン / スプラッシュ / 署名) を、flavor や配布前チェックを先に固めたい場合は Flutterの環境切替と配布前チェック(flavor / release build / 権限確認) を参照してください。

3-2. Flutter プロジェクトを作成する

今回使う最小サンプルは、配布チャネルと対象パッケージを画面に表示するものです。

このサンプルは flutter run で確認します。この記事では、このまま APK 作成とサイドロード確認まで進めるため、DartPad ではなくエミュレーターまたは実機で動かします。

次のコマンドでプロジェクトを作成します。

flutter create warehouse_distribution_sample
cd warehouse_distribution_sample

3-3. エミュレーターを起動する

利用可能なエミュレーター一覧を確認します。

flutter emulators

表示された ID を指定して起動します。

flutter emulators --launch <emulator_id>

3-4. lib/main.dart を書き換える

lib/main.dart は次の内容で書き換えます。配布チャネル、想定 APK 名、対象パッケージ名を画面へ出し、配布後の確認で見比べやすくしたサンプルです。

import 'package:flutter/material.dart';

const distributionChannel = String.fromEnvironment(
  'DIST_CHANNEL',
  defaultValue: 'stg',
);

const apkFileName = String.fromEnvironment(
  'APK_FILE_NAME',
  defaultValue: 'app-release.apk',
);

const packageId = String.fromEnvironment(
  'PACKAGE_ID',
  defaultValue: 'com.example.warehouse_distribution_sample',
);

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Internal Distribution Sample',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
      ),
      home: const DistributionHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('社内配布サンプル'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(24),
        children: [
          Container(
            padding: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              color: theme.colorScheme.primaryContainer,
              borderRadius: BorderRadius.circular(20),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  distributionChannel.toUpperCase(),
                  style: theme.textTheme.labelLarge?.copyWith(
                    color: theme.colorScheme.onPrimaryContainer,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  'この build をどの端末へ配るかを確認する画面',
                  style: theme.textTheme.titleLarge,
                ),
                const SizedBox(height: 8),
                Text('APK 名: $apkFileName'),
                Text('packageId: $packageId'),
              ],
            ),
          ),
          const SizedBox(height: 24),
          const _InfoCard(
            title: '今回の確認項目',
            lines: <String>[
              'release APK を作成する',
              'adb install と手動サイドロードの両方を試す',
              '配布後に version と package を確認する',
              'MDM へ渡す項目を整理する',
            ],
          ),
          const SizedBox(height: 16),
          const _InfoCard(
            title: '配布時のメモ',
            lines: <String>[
              '同じ packageId で新しい versionCode を使う',
              '単発検証は adb install が速い',
              '利用者配布はファイル経由の導線も確認する',
            ],
          ),
        ],
      ),
    );
  }
}

class _InfoCard extends StatelessWidget {
  const _InfoCard({
    required this.title,
    required this.lines,
  });

  final String title;
  final List<String> lines;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: theme.textTheme.titleMedium),
            const SizedBox(height: 12),
            for (final line in lines)
              Padding(
                padding: const EdgeInsets.only(bottom: 8),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('・'),
                    const SizedBox(width: 8),
                    Expanded(child: Text(line)),
                  ],
                ),
              ),
          ],
        ),
      ),
    );
  }
}

コードのポイント

String.fromEnvironment でビルド時に配布情報を注入する

const distributionChannel = String.fromEnvironment(
  'DIST_CHANNEL',
  defaultValue: 'stg',
);

const apkFileName = String.fromEnvironment(
  'APK_FILE_NAME',
  defaultValue: 'app-release.apk',
);

const packageId = String.fromEnvironment(
  'PACKAGE_ID',
  defaultValue: 'com.example.warehouse_distribution_sample',
);

String.fromEnvironment はビルド時に --dart-define で渡した値を定数として埋め込みます。const として宣言することで実行時に変更できないことを明示でき、defaultValue を設定しておくと --dart-define を省略した場合でも動作します。

3-5. flutter run で起動する

エミュレーターで起動して、画面に表示される配布チャネルと packageId を確認します。

flutter run --dart-define=DIST_CHANNEL=stg --dart-define=APK_FILE_NAME=app-release.apk --dart-define=PACKAGE_ID=com.example.warehouse_distribution_sample

配布用の導線では、アプリの機能そのものより「いまどの build を見ているか」が大事になります。手動配布に入る前に、画面上の識別情報を一度そろえておくと確認が早くなります。

配布チャネル、APK 名、packageId を表示したサンプルアプリ画面

4. release APK を作成する

4-1. pubspec.yamlversion を更新する

上書きインストールの成否に関わるので、配布前に pubspec.yamlversion を確認します。次のように versionName + versionCode を上げておきます。

version: 1.0.0+3

1.0.0 が利用者に見せる versionName+3 が Android 側の versionCode です。前回配布より versionCode が上がっていないと、同じ applicationId でも更新版として入れ替えられません。

4-2. release 署名を設定する

release APK を社内配布に使うなら、debug build ではなく署名済み release build を使います。新規 Flutter プロジェクトへ最小限の署名設定を入れます。アプリ名、アイコン、スプラッシュもまとめて整えたい場合は Flutterアプリのネイティブ設定を整える(アプリ名 / アイコン / スプラッシュ / 署名) を参照してください。

まずキーストアを作成します。PowerShell では次のコマンドで進められます。

keytool -genkey -v -keystore $env:USERPROFILE\upload-keystore.jks `
  -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 `
  -alias upload

ここで控えるのは 3 つです。

  • キーストアの保存先パス
  • storePasswordkeyPassword
  • alias。この例では upload

次に android/key.properties を作成します。

storePassword=your-store-password
keyPassword=your-key-password
keyAlias=upload
storeFile=C:\\Users\\your-name\\upload-keystore.jks

Windows では storeFile のバックスラッシュを二重にします。android/key.properties は機密情報を含むので、公開リポジトリでは .gitignore に追加しておきます。

続いて android/app/build.gradle.kts を更新します。import はファイル先頭、plugins ブロックより前に置き、android { ... } には signingConfigsbuildTypes.release の設定を追記します。

import java.io.FileInputStream
import java.util.Properties

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    signingConfigs {
        create("release") {
            val storeFilePath = keystoreProperties.getProperty("storeFile")
            keyAlias = keystoreProperties.getProperty("keyAlias")
            keyPassword = keystoreProperties.getProperty("keyPassword")
            storeFile = storeFilePath?.let { rootProject.file(it) }
            storePassword = keystoreProperties.getProperty("storePassword")
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

コードのポイント

key.propertiesProperties で読み込み、署名情報をソースコードに直書きしない

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

パスワードやエイリアスをソースコードに直書きせず、key.properties から読み込む構成です。key.properties.gitignore に追加しておくと、ソースコード管理から除外できます。

buildTypes.releasesigningConfig を紐付ける

        release {
            signingConfig = signingConfigs.getByName("release")
        }

buildTypes.releasesigningConfig を設定することで、flutter build apk --release 実行時に署名が自動で適用されます。debug build は別の signingConfig を使うため、release との分離が維持されます。

既存の compileOptionsdefaultConfig は消さずに残してください。ここまで入っていれば、このあとの flutter build apk --release で署名済み APK を作成できます。

4-3. release APK を作成する

flutter build apk --release --dart-define=DIST_CHANNEL=stg --dart-define=APK_FILE_NAME=app-release.apk --dart-define=PACKAGE_ID=com.example.warehouse_distribution_sample

前の記事で flavor を入れているなら、次のように --flavor stg を付けて読み替えます。

flutter build apk --flavor stg --release --dart-define=DIST_CHANNEL=stg --dart-define=APK_FILE_NAME=app-stg-release.apk --dart-define=PACKAGE_ID=com.example.warehouse_pocket.stg

出力先は通常、次の場所です。

build/app/outputs/flutter-apk/app-release.apk

flavor を付けた場合は app-stg-release.apk のようにファイル名が変わります。配布時に取り違えやすいので、Explorer で実ファイル名まで確認しておくと安全です。

Explorer で build/app/outputs/flutter-apk 配下の APK を確認している画面

5. まずは adb install で配布する

adb install は、開発者や検証担当が最短で差し替える方法です。USB 接続した実機へ入れる場合は、先に端末側で「開発者向けオプション → USB デバッグ」をオンにし、接続時に表示される「USB デバッグを許可しますか?」ダイアログで許可しておきます。ここが済んでいないと、adb devices に端末が出ません。

まずは接続中デバイスを確認します。

adb devices

1 台だけ接続されているなら、次のコマンドで上書きインストールできます。

adb install -r build/app/outputs/flutter-apk/app-release.apk

複数端末がつながっている場合は、device id を付けて明示します。

adb -s <device_id> install -r build/app/outputs/flutter-apk/app-release.apk

-r は既存アプリを残したまま差し替える指定です。同じ packageId で、より新しい versionCode を持つ APK なら更新版として入れ替えられます。

インストール後はホーム画面またはアプリ一覧から起動し、配布チャネルと packageId の表示が想定どおりか確認します。

adb install 後に起動したサンプルアプリ画面

6. 利用者向けの手動サイドロードを確認する

利用者へ渡す場合は、ファイルからインストールする導線も見ておいたほうが実務に合います。まず APK を端末の Download フォルダへ送ります。

adb push build/app/outputs/flutter-apk/app-release.apk /sdcard/Download/

エミュレーターと実機を同時に起動しているなど、複数デバイスが見えている場合は device id を付けて明示します。

adb devices
adb -s <device_id> push build/app/outputs/flutter-apk/app-release.apk /sdcard/Download/

ここでは APK を端末へ送る手段として adb push を使っていますが、手動サイドロード自体に USB デバッグは必須ではありません。メール、チャット、社内ストレージ、ローカル共有などで APK を端末へ渡せるなら、その経路から Download フォルダへ保存してインストールしても進められます。

その後、端末側で Download フォルダを開き、APK をタップしてインストールします。ここで初めて止まりやすいのが「未知のアプリのインストール許可」です。

  • メールアプリやチャットアプリから開くなら、その提供元に対する許可が必要なことがある
  • Files アプリから開くなら、Files に対する許可が必要なことがある
  • 一度許可しても、別のアプリから開くと再び許可が必要になることがある
  • MDM 管理端末では、この許可を利用者が触れないようポリシーで固定していることがある

設定画面を開いて確認したいときは、次のコマンドでも遷移できます。

adb shell am start -a android.settings.MANAGE_UNKNOWN_APP_SOURCES

複数デバイスがある場合は、この画面遷移も device id を付けておくと意図した端末を開けます。

adb -s <device_id> shell am start -a android.settings.MANAGE_UNKNOWN_APP_SOURCES

社内端末でこの設定変更が許可されていないなら、手動サイドロードより MDM 配布のほうが実運用に向きます。利用者に毎回この操作をお願いする運用は、台数が増えるほど維持しにくい形です。

未知のアプリのインストール許可画面 Download フォルダで APK を確認している画面 APK のインストール確認画面

7. 配布後に確認する項目を固定する

インストールできたら終わりではありません。更新版が入るかどうかは、applicationIdversionCode の組み合わせで決まります。

条件結果どう見るか
同じ applicationId で、より新しい versionCode上書き更新できる既存アプリを残したまま差し替えられる
同じ applicationId で、同じまたは古い versionCode更新できないAPK を作り直すか、いったんアンインストールが必要
異なる applicationId別アプリとして並列インストールされるdevstg を共存させたいときに使う

配布後にアプリ情報画面を開いて確認するには、次のコマンドが使えます。

adb shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:com.example.warehouse_distribution_sample

エミュレーターと実機を同時に起動しているなど、複数デバイスが見えている場合は device id を付けて明示します。

adb -s <device_id> shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:com.example.warehouse_distribution_sample

ここで見たい点は 4 つです。

  • アプリ名が対象 build と一致しているか
  • versionNameversionCode が配布予定どおりか
  • 想定していない旧アプリが端末に残っていないか
  • stgprod を取り違えるような並びになっていないか

前の記事で flavor を入れているなら、package: の値を com.example.warehouse_pocket.stg のように置き換えて確認してください。配布後のトラブルは、APK 生成よりここでの取り違えから起きることが多くあります。

Android のアプリ情報画面でアプリ名とバージョンを確認している画面

8. MDM 配布へ広げるときの整理項目

手動サイドロードは 1 台ずつの確認には向きますが、配布台数が増えると限界が見えます。ここでいう MDM は Mobile Device Management の略で、社内端末へアプリ配布や設定反映をまとめて管理する仕組みです。次のような状況なら、MDM 配布へ寄せたほうが管理しやすくなります。

  • 配布先が複数チーム、複数拠点に広がる
  • 利用者へ個別にインストール操作を説明したくない
  • 更新版を一斉に配りたい
  • 端末ごとの許可設定を利用者へ触らせたくない

MDM 担当へ渡す項目は、少なくとも次をそろえると話が早くなります。

項目何を書くか理由
APK配布に使う signed release APK のパスまたは共有先登録対象そのものになる
applicationId例: com.example.warehouse_pocket.stg更新対象の特定に必要
versionName / versionCode例: 1.0.0+3上書き更新可否の判断に必要
配布先対象部署、端末グループ、検証端末か本番端末か誤配布を防ぐ
インストール方式サイレント配布か、利用者承認付きか端末ポリシーと手順が変わる
更新方針強制更新か、検証後に順次更新か配布タイミングの判断に必要
ロールバック方針旧版を残すか、旧 APK を別保管するか不具合時の戻し方を決める

ここで注意したいのは、MDM の製品ごとに受け付ける形式や管理画面の文言が違うことです。直接 APK を登録する製品もあれば、別の保管場所や Android Enterprise 側の仕組みを使う製品もあります。この記事では特定製品の画面操作までは扱わず、製品が変わっても共通で必要になる引き継ぎ項目に絞ります。製品選定の比較に入る前に、社内で必要な項目を上の表で固定しておくと、ベンダー差を後から吸収しやすくなります。

9. まとめ

社内配布で最初に固めたいのは、APK を作ることより、どの経路で誰に渡し、更新判定を何で見るかです。開発者や検証担当には adb install -r が速く、利用者にはファイル経由の手動サイドロードが現実に近くなります。ただし手動サイドロードは導入コストを抑えやすい一方、更新操作そのものは端末ごとに残ります。配布台数が増えたら、APK に加えて applicationIdversionName / versionCode、配布先、更新方針をそろえたうえで MDM へ移ると整理しやすくなります。

配布前の flavor や release build を見直したい場合は Flutterの環境切替と配布前チェック(flavor / release build / 権限確認) を参照してください。署名設定をやり直したい場合は Flutterアプリのネイティブ設定を整える(アプリ名 / アイコン / スプラッシュ / 署名) を、端末側での回帰確認を自動化したい場合は FlutterのIntegration Test入門(ログインから一覧表示まで確認する) を参照してください。

シリーズ 37/38

このシリーズ

Flutter導入と基礎

  1. 1. Windows 11で始めるFlutter開発環境:Android Emulatorで動かすまで
  2. 2. Flutter + FVM で開発環境のバージョンを固定する
  3. 3. Flutterで画像・SVG・アイコンを管理する(flutter_gen最小構成)
  4. 4. Flutterで最初に詰まりやすいDartの書き方:final・const・null safety・async/await を最初に整理する
  5. 5. DartのStream入門(非同期データの流れをつかむ)
  6. 6. FlutterのWidgetライフサイクル入門(initState / dispose で詰まらないために)
  7. 7. FlutterでBuildContextとKeyを理解する
  8. 8. Flutterのレイアウト入門(Column / Row / Stack の使い分け)
  9. 9. Flutterのテーマ設計入門(ThemeData + Theme Extension)
  10. 10. FlutterでMediaQueryとLayoutBuilderを使って画面サイズに対応する(スマホ・タブレット両対応)
  11. 11. FlutterのContainerとSizedBoxを使いこなす(余白・サイズ・装飾の基本)
  12. 12. FlutterのListViewとGridViewで一覧画面を作る(基本パターン)
  13. 13. Flutterのダイアログ・スナックバー・ボトムシートを使う(確認・通知UIの基本)
  14. 14. FlutterのTabBarとBottomNavigationBarで複数画面を切り替える
  15. 15. Flutterでカスタムウィジェットを作る入門(StatelessWidget の分割と再利用)
  16. 16. Flutterのルーティング入門(Navigator と go_router の使い分け)
  17. 17. FlutterからREST APIを呼ぶ最小構成(JSON通信 + エラー処理)
  18. 18. Flutterでjson_serializable + build_runnerを使ってJSONモデルを型安全に扱う
  19. 19. Flutterの状態管理入門(Riverpod最小構成)
  20. 20. Flutterでローディング・空状態・エラー表示を整える
  21. 21. Dart 3のsealed classとパターンマッチングで分岐を安全に書く
  22. 22. Flutterでgo_routerの認証ガードを実装する(redirect最小構成)
  23. 23. Flutterで端末設定と利用者設定を保存する(SharedPreferencesとsecure storageの使い分け)
  24. 24. Flutterでログイン状態を保持する(JWT + secure storage 最小構成)
  25. 25. Flutterアプリを日本語化する(l10n + arb 最小構成)
  26. 26. Flutterで業務用バーコード読み取りアプリを作る(最小構成)
  27. 27. Flutterでスキャン入力を受けて処理する
  28. 28. FlutterでGS1-128バーコードを解析する
  29. 29. Flutterで単一画面の入力フローを作る
  30. 30. Flutterで複数画像の添付UIを作る
  31. 31. Flutterでデータをファイルに書き出す
  32. 32. permission_handler でAndroid権限を実践的に扱う(カメラ・ストレージ・Bluetooth)
  33. 33. Flutterアプリのネイティブ設定を整える(アプリ名 / アイコン / スプラッシュ / 署名)
  34. 34. Flutterの環境切替と配布前チェック(flavor / release build / 権限確認)
  35. 35. FlutterのWidgetテスト入門(画面ロジックを壊さない最小構成)
  36. 36. FlutterのIntegration Test入門(ログインから一覧表示まで確認する)
  37. 37. Flutterアプリを社内配布する(Android APK サイドロード + MDM 概要) 現在の記事
  38. 38. Sentryでクラッシュとエラーを検知する(Flutter最小構成)