「iOSだけでいい」と思っていた頃、私のアプリの月収は¥180,000でした。Androidへの展開を決めて6ヶ月後、月収は¥310,000になっていました。増加した¥130,000のほとんどは、iOSでは到達できなかった新しいユーザー層からの収益です。
Rork MaxはネイティブiOSアプリの生成に優れていますが、React Native経由でAndroidにも展開できます。ここでは両プラットフォームを同時に運用して収益を最大化するための設計と実装を、実際の数字を交えながら解説します。
iOSとAndroidで課金行動が違う理由
同じアプリを出しても、プラットフォームによって収益の構造が変わります。この違いを理解せずに「とりあえず両方出す」だけでは、せっかくの展開機会を活かしきれません。
App Store(iOS)の課金特性
iOSユーザーは課金転換率が高い傾向があります。App Storeの決済体験が磨かれており、Face IDでの1タップ決済が抵抗感を下げています。
- サブスクリプション転換率: 無料試用から有料への転換が比較的高い(5〜15%が現実的なレンジ)
- ARPU(一人あたり平均収益): Androidより高い傾向(一般的に1.5〜2倍)
- 審査の厳格さ: 課金フローと広告表示に関して厳しい基準がある
Google Play(Android)の課金特性
Androidは絶対数が多く、特定の地域(東南アジア・インド・中南米)では相応のシェアを持ちます。課金転換率はiOSより低いものの、ダウンロード数の多さがカバーします。
- ダウンロード量: iOSの2〜3倍のダウンロード数が一般的
- 広告収益: DAU(デイリーアクティブユーザー)が多いため、AdMob収益はAndroidで大きくなりやすい
- 審査の柔軟性: App Storeより審査基準が緩いケースが多く、機能の多様性を出しやすい
プラットフォーム別の収益構造(アプリ種別ごとの典型例):
ユーティリティ・ツール系:
iOS: 月額サブスクが主収益 → 課金転換率重視
Android: 広告 + 課金の複合型 → DAU最大化重視
ゲーム・娯楽系:
iOS: IAP(アプリ内購入)+ 少量の広告
Android: 広告メイン + IAP(広告のARPDAU高め)
習慣・ライフスタイル系:
iOS: 高単価サブスク(月額¥1,200〜¥3,600)
Android: 低単価サブスク + 広告(¥500〜¥1,500)
RevenueCatで両プラットフォームの課金を一元管理する
クロスプラットフォームで課金実装するとき、最も避けるべきはiOSとAndroidで別々の課金ロジックを書くことです。バグが2倍、デバッグコストが2倍になります。
RevenueCatは、App StoreとGoogle Playの両方をひとつのSDKで抽象化します。
// iOS + Android共通の初期化(React Native / Expo でも同様)
import Purchases
// アプリ起動時
Purchases.configure(withAPIKey: "YOUR_REVENUECAT_API_KEY")
// ユーザー識別(ログイン後に呼ぶ)
Purchases.shared.logIn(appUserId: userId) { (customerInfo, created, error) in
if let info = customerInfo {
self.updateSubscriptionStatus(from: info)
}
}
func updateSubscriptionStatus(from info: CustomerInfo) {
let isSubscribed = info.entitlements["premium"]?.isActive == true
let expiresDate = info.entitlements["premium"]?.expirationDate
DispatchQueue.main.async {
self.isPremium = isSubscribed
self.premiumExpiresDate = expiresDate
}
}
// 購入フロー(プラットフォーム問わず同じコード)
func purchase(packageId: String) async throws {
let offerings = try await Purchases.shared.offerings()
guard let package = offerings.current?.availablePackages.first(where: {
$0.identifier == packageId
}) else {
throw PurchaseError.packageNotFound
}
let result = try await Purchases.shared.purchase(package: package)
if result.customerInfo.entitlements["premium"]?.isActive == true {
// 購入成功
await updateUI(isPremium: true)
}
}
プラットフォーム別の価格設計
同じアプリでも、iOSとAndroidで価格を変えることを検討すべき場合があります。
月額サブスクリプション価格の設計例:
iOSの場合(高ARPU、課金転換率重視):
Lite: ¥480/月
Pro: ¥1,200/月 ← 主力プラン
Annual: ¥9,800/年(月換算¥817・33%オフ)
Androidの場合(ダウンロード数重視、転換率は低め):
Lite: ¥380/月 ← iOSより低めに設定
Pro: ¥980/月
Annual: ¥7,800/年(月換算¥650・33%オフ)
理由:
- Androidユーザーは価格感度が高い傾向
- 転換率を上げるために参入ハードルを下げる
- 年額プランでLTVを確保する構造は共通
RevenueCatでプラットフォーム別の価格を設定するには、App Store ConnectとGoogle Play Consoleそれぞれで別の製品IDを作成し、RevenueCatのダッシュボードで「Offering」として管理します。
AdMob統合 — 両プラットフォームで広告収益を最大化する
サブスクリプションと広告を組み合わせる場合、「有料ユーザーに広告を表示しない」というロジックが必須です。これを間違えると有料ユーザーが解約します(実際にあった事例です)。
// 課金状態と広告表示の連動
class AdManager {
static let shared = AdManager()
// 広告を表示すべきかどうか(課金状態に依存)
var shouldShowAds: Bool {
return !SubscriptionManager.shared.isPremium
}
func loadInterstitialIfNeeded() {
guard shouldShowAds else {
print("Premium user — skipping ad load")
return
}
GADInterstitialAd.load(
withAdUnitID: adUnitId,
request: GADRequest()
) { [weak self] ad, error in
self?.interstitialAd = ad
}
}
func showInterstitialIfReady(from viewController: UIViewController) {
guard shouldShowAds, let ad = interstitialAd else { return }
ad.present(fromRootViewController: viewController)
interstitialAd = nil
loadInterstitialIfNeeded() // 次の広告をプリロード
}
}
iOS と Android で広告ユニットIDを切り替える
App IDと広告ユニットIDはプラットフォームごとに異なります。
// プラットフォーム別の広告ユニットID管理
struct AdConfig {
#if os(iOS)
static let bannerAdUnitId = "ca-app-pub-XXXX/iOS_BANNER_ID"
static let interstitialAdUnitId = "ca-app-pub-XXXX/iOS_INTER_ID"
#elseif os(Android) // React Native の場合は Platform.OS で分岐
static let bannerAdUnitId = "ca-app-pub-XXXX/ANDROID_BANNER_ID"
static let interstitialAdUnitId = "ca-app-pub-XXXX/ANDROID_INTER_ID"
#endif
// テスト時は必ずテスト用IDを使う
#if DEBUG
static let bannerAdUnitId = "ca-app-pub-3940256099942544/2934735716"
static let interstitialAdUnitId = "ca-app-pub-3940256099942544/4411468910"
#endif
}
広告の表示タイミングと収益の関係
広告表示のタイミングはARPDAUに直結します。研究と実際の運用から得た原則です。
タイミング別のARPDAU影響:
起動時インタースティシャル(セッション開始時1回):
ARPDAU: 高(¥3〜¥6)
継続率への影響: 低〜中程度
推奨: ゲームや娯楽系アプリに向く
機能完了後インタースティシャル(タスク完了ごと):
ARPDAU: 中(¥2〜¥4)
継続率への影響: 低い(タイミングが自然なため)
推奨: ユーティリティ系アプリの最良パターン
動画リワード広告(自発的に視聴):
ARPDAU: 低〜中(¥1〜¥3)
継続率への影響: ポジティブ(ユーザーが選んで見るため)
推奨: フリーミアムの追加価値提供として最適
AdMobメディエーションで広告単価を底上げする
AdMob単体で配信していた頃、広告収益は「こういうものだ」と半ば諦めていました。考えを変えたのはメディエーションの導入です。ひとつのインプレッションに対して複数の広告ネットワークを競わせ、最も高い単価を提示したネットワークに配信します。表示回数は同じまま、単価だけが底上げされる仕組みです。
メディエーションには大きく2つの方式があります。仕組みを先に理解しておくと、設定画面で迷いません。
| 方式 | 仕組み | 長所 | 短所 |
| ウォーターフォール | 過去実績のeCPM順にネットワークへ順番に問い合わせる | 設定が直感的・歴史が長く安定 | 実需と乖離しやすい・順次問い合わせで遅延が出る |
| ビディング(入札) | 全ネットワークが同時にリアルタイム入札する | そのインプレッションの真の最高値がつく・遅延が小さい | 入札対応のネットワークに限られる |
現在は入札対応ネットワークを軸に据え、ウォーターフォールを補助的に併用する構成が主流です。私自身のアプリでも、主要ネットワークを入札へ寄せてから平均eCPMが目に見えて安定しました。日ごとの上下は残りますが、月次で均すと下振れが減ります。
接続しているのは AdMob(Google)・Meta Audience Network・AppLovin・Pangle・Unity Ads の5社です。iOSとAndroidで強いネットワークが異なるため、両方に同じ設定を流用すると取りこぼします。
| ネットワーク | iOSでの相対的な強さ | Androidでの相対的な強さ | 備考 |
| AdMob(Google) | 高 | 高 | 基盤。まずここを埋める |
| Meta Audience Network | 中〜高 | 中 | iOSはATT同意率に左右される |
| AppLovin | 中 | 高 | Androidのリワード広告で強い |
| Pangle | 中 | 高 | アジア圏・Androidで単価が伸びやすい |
| Unity Ads | 中 | 中〜高 | ゲーム系トラフィックと相性が良い |
具体的な金額はアプリのジャンルと地域構成で大きく変わるため断定はできませんが、傾向として次の2点は繰り返し観測しています。
- 地域差: 米国・日本・西欧はeCPMが高く、東南アジア・インド・中南米は低い傾向です。ただし後者はDAUの絶対数が多く、Androidでは「単価×表示回数」で無視できない収益になります
- 時間帯差: 広告主の予算が動く時間帯にeCPMが上がります。起動時インタースティシャルのようなセッション依存の広告は、この波の影響を受けます
メディエーション導入後は、レポートを「どのネットワークがどれだけ埋めたか(Fill Rate)」と「ネットワーク別eCPM」で見るようになります。Fill Rateが慢性的に低いネットワークは外し、入札対応の新しいネットワークを足す。この入れ替えを四半期に一度行うだけで、広告収益は緩やかに改善していきます。派手な施策ではありませんが、こうした地道な調整が積み上がって効いてきます。
iOSのATT対応で広告収益を守る
デュアルストア運用でiOS側の広告収益を語るとき、避けて通れないのがATT(App Tracking Transparency)です。iOS 14.5以降、アプリがIDFA(広告識別子)でユーザーを追跡するには、同意ダイアログの表示が必須になりました。ここで「許可しない」を選ばれると、広告のターゲティング精度が落ち、そのぶんeCPMが下がります。
私が最初にATTダイアログを実装したとき、同意率は3割ほどでした。表示のタイミングと文言を見直すことで、この数字は改善できます。
import AppTrackingTransparency
import AdSupport
func requestTrackingIfNeeded() async {
// すでに決定済みなら何もしない
guard ATTrackingManager.trackingAuthorizationStatus == .notDetermined else {
return
}
// システムダイアログを表示
let status = await ATTrackingManager.requestTrackingAuthorization()
switch status {
case .authorized:
// IDFAが利用可能。広告SDKへ渡す
let idfa = ASIdentifierManager.shared().advertisingIdentifier
AdManager.shared.configureTracking(idfa: idfa)
default:
// 非追跡モードでも広告自体は配信される(単価は下がる)
AdManager.shared.configureNonPersonalized()
}
}
実装で効いたのは、システムダイアログをアプリ起動の直後に出さないことでした。ユーザーがまだアプリの価値を体験していない段階で「追跡を許可しますか」と問われても、多くの人は反射的に拒否します。
- タイミング: 最初の主要機能を一度使い終えた直後に表示します。価値を体感した後なら、同意率は目に見えて上がります
- 事前説明(プレプロンプト): システムダイアログの前に、自前の画面で「なぜ許可が無料提供の維持につながるか」を丁寧に伝えます。Appleのガイドライン上、事前説明で誤誘導をしないことが条件です
- 拒否されても止まらない設計: ATTを拒否したユーザーにも、非パーソナライズ広告は配信されます。SKAdNetworkによる集計は同意なしでも機能するため、キャンペーンの計測自体は続けられます
Androidにはこの同意フローがありません。iOSだけがATTのぶん広告単価で不利になります。これも「Androidは広告を厚めに、iOSは課金を厚めに」という配分設計を後押しする、実務上の理由のひとつです。
統合収益ダッシュボードの設計
両プラットフォームの収益をひとつの画面で把握できる仕組みは、運用効率を大きく上げます。RevenueCat APIとAdMob Reporting APIを組み合わせて作るのが現実的です。
# 統合収益ダッシュボード — バックエンドのデータ集計スクリプト
import requests
import json
from datetime import datetime, timedelta
REVENUECAT_API_KEY = "YOUR_REVENUECAT_SECRET_KEY"
ADMOB_PUBLISHER_ID = "pub-XXXXXXXXXXXXX"
def get_revenuecat_revenue(start_date: str, end_date: str) -> dict:
"""RevenueCat APIから課金収益を取得"""
headers = {"Authorization": f"Bearer {REVENUECAT_API_KEY}"}
response = requests.get(
"https://api.revenuecat.com/v1/subscribers",
headers=headers,
params={"start_date": start_date, "end_date": end_date}
)
data = response.json()
ios_revenue = sum(
sub["revenue"] for sub in data.get("subscribers", [])
if sub.get("app_store") == "app_store"
)
android_revenue = sum(
sub["revenue"] for sub in data.get("subscribers", [])
if sub.get("app_store") == "play_store"
)
return {
"ios_subscription_jpy": int(ios_revenue * 150), # USD→JPY換算
"android_subscription_jpy": int(android_revenue * 150),
"total_subscription_jpy": int((ios_revenue + android_revenue) * 150)
}
def build_monthly_report(year: int, month: int) -> dict:
"""月次統合収益レポート"""
start = f"{year}-{month:02d}-01"
end = f"{year}-{month:02d}-{get_days_in_month(year, month):02d}"
subscription_data = get_revenuecat_revenue(start, end)
# AdMob収益は別途Google AdMob Reporting APIから取得
# ここでは手動入力値を想定
admob_ios_jpy = 45000 # 実際はAPI取得
admob_android_jpy = 68000
total_ios = subscription_data["ios_subscription_jpy"] + admob_ios_jpy
total_android = subscription_data["android_subscription_jpy"] + admob_android_jpy
return {
"period": f"{year}年{month}月",
"ios": {
"subscription": subscription_data["ios_subscription_jpy"],
"admob": admob_ios_jpy,
"total": total_ios
},
"android": {
"subscription": subscription_data["android_subscription_jpy"],
"admob": admob_android_jpy,
"total": total_android
},
"grand_total_jpy": total_ios + total_android
}
# 実行例
report = build_monthly_report(2026, 4)
print(f"総収益: ¥{report['grand_total_jpy']:,}")
print(f" iOS: ¥{report['ios']['total']:,} (課金¥{report['ios']['subscription']:,} + 広告¥{report['ios']['admob']:,})")
print(f" Android: ¥{report['android']['total']:,} (課金¥{report['android']['subscription']:,} + 広告¥{report['android']['admob']:,})")
AdMob Reporting API で広告収益まで自動集計する
先ほどの統合ダッシュボードでは、AdMob収益を手動入力のままにしていました。運用が続くと、この手作業が地味に負担になります。AdMob Reporting APIを使えば、課金収益と同じスクリプトの中で広告収益まで取得でき、月初の確認作業が数分で終わります。
# AdMob Reporting API で月次の広告収益をプラットフォーム別に取得する
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
def get_admob_revenue(account_id: str, start: dict, end: dict) -> dict:
"""
account_id: "pub-XXXXXXXXXXXXXXXX"
start / end: {"year": 2026, "month": 4, "day": 1} 形式
"""
creds = Credentials.from_authorized_user_file("admob_token.json")
service = build("admob", "v1", credentials=creds)
report_spec = {
"dateRange": {"startDate": start, "endDate": end},
"dimensions": ["PLATFORM"], # iOS / ANDROID で分割
"metrics": ["ESTIMATED_EARNINGS", "IMPRESSIONS"],
}
response = service.accounts().networkReport().generate(
parent=f"accounts/{account_id}",
body={"reportSpec": report_spec},
).execute()
result = {"ios_micros": 0, "android_micros": 0}
for row in response:
r = row.get("row")
if not r:
continue
platform = r["dimensionValues"]["PLATFORM"]["value"]
earnings = int(r["metricValues"]["ESTIMATED_EARNINGS"]["microsValue"])
if platform == "IOS":
result["ios_micros"] += earnings
elif platform == "ANDROID":
result["android_micros"] += earnings
return result
# ESTIMATED_EARNINGS はマイクロ単位(1,000,000 で1通貨単位)で返る
raw = get_admob_revenue(
"pub-XXXXXXXXXXXXXXXX",
{"year": 2026, "month": 4, "day": 1},
{"year": 2026, "month": 4, "day": 30},
)
admob_ios_jpy = raw["ios_micros"] // 1_000_000
admob_android_jpy = raw["android_micros"] // 1_000_000
print(f"AdMob iOS: ¥{admob_ios_jpy:,} / Android: ¥{admob_android_jpy:,}")
ここで返る ESTIMATED_EARNINGS はマイクロ単位(1,000,000で1通貨単位)である点に注意してください。桁を取り違えると、レポートの数字が6桁ずれます。私は最初にこれを見落として、「今月は広告収益が100万倍になった」と一瞬だけ喜んだことがあります。
この関数を先ほどの build_monthly_report に差し込めば、admob_ios_jpy と admob_android_jpy の手動入力を消せます。課金と広告、iOSとAndroidの4象限が1回のスクリプト実行で揃うと、収益の全体像が一目で把握できるようになります。通貨単位はAdMobアカウントの設定に従うため、円建てアカウントならそのまま円で返ります。
ユーザーLTVのプラットフォーム別計算
どちらのプラットフォームへの投資対効果が高いかを判断するには、プラットフォーム別のLTV(顧客生涯価値)計算が必要です。
def calculate_ltv_by_platform(platform: str, data: dict) -> dict:
"""
プラットフォーム別LTV計算
data = {
"monthly_arpu": 月次ARPU(円),
"monthly_churn_rate": 月次解約率(0〜1),
"ad_revenue_per_dau": 広告ユーザー一人あたりの月次収益
}
"""
arpu = data["monthly_arpu"]
churn = data["monthly_churn_rate"]
ad_rev = data.get("ad_revenue_per_dau", 0)
# 課金ユーザーLTV
subscription_ltv = arpu / churn if churn > 0 else arpu * 12
# 広告ユーザーLTV(平均継続月数 × 月次広告収益)
avg_retention_months = 1 / churn if churn > 0 else 12
ad_ltv = ad_rev * avg_retention_months
return {
"platform": platform,
"subscription_ltv_jpy": int(subscription_ltv),
"ad_user_ltv_jpy": int(ad_ltv),
"avg_retention_months": round(avg_retention_months, 1)
}
# 実際の数値例
ios_ltv = calculate_ltv_by_platform("iOS", {
"monthly_arpu": 1200,
"monthly_churn_rate": 0.08,
"ad_revenue_per_dau": 120
})
android_ltv = calculate_ltv_by_platform("Android", {
"monthly_arpu": 880,
"monthly_churn_rate": 0.11,
"ad_revenue_per_dau": 160 # Androidは広告収益が高い
})
print(f"iOS: 課金LTV ¥{ios_ltv['subscription_ltv_jpy']:,} / 広告LTV ¥{ios_ltv['ad_user_ltv_jpy']:,}")
print(f"Android: 課金LTV ¥{android_ltv['subscription_ltv_jpy']:,} / 広告LTV ¥{android_ltv['ad_user_ltv_jpy']:,}")
# 出力例:
# iOS: 課金LTV ¥15,000 / 広告LTV ¥1,500
# Android: 課金LTV ¥8,000 / 広告LTV ¥1,454
この計算で重要なのは、iOSの課金LTVはAndroidより高い傾向がある一方で、Androidの広告LTVは課金LTVとの差が相対的に小さいことです。これがAndroidをAdMob中心のマネタイズにする根拠になります。
Rork MaxでのクロスプラットフォームUI生成の注意点
Rork MaxはSwiftUI(iOS)の生成が主力ですが、課金UIについては両プラットフォームで設計を共通化できる部分と、分ける必要がある部分があります。
共通化できる部分:
- プランの表示ロジック(価格一覧・機能比較)
- 購入後の状態管理(isPremium フラグ)
- RevenueCat の呼び出しロジック
分ける必要がある部分:
- 審査ガイドライン上の表示要件(App Store はより厳格)
- ペイウォールのUIコンポーネント(プラットフォーム設計に合わせる)
- バックボタンの挙動(Android は必須、iOS は任意)
Rork Maxへのプロンプトでは、生成するコードがどちらのプラットフォーム向けかを明示することで、適切な設計パターンが出力されます。
Rork Maxプロンプト例(ペイウォール画面):
「RevenueCatを使ったサブスクリプションのペイウォール画面を作成してください。
対象プラットフォーム: iOS(SwiftUI)
要件:
- 月額プラン(¥1,200)と年額プラン(¥9,800)の2つを表示
- 年額プランにはバッジで「33%オフ」を表示
- 14日間無料試用のバナーを目立たせる
- App Storeのガイドライン上必要な購読条件の表示(下部に)
- エラー時(決済失敗など)の状態も実装すること」
ストア審査を通すための収益化ルール
両プラットフォームの審査基準の違いを知らずに実装すると、審査落ちで数週間のロスが生じます。
App Store(iOS)の主要ルール
- ガイドライン3.1.1: デジタルコンテンツへのアクセス提供(サブスク・IAP)にはApp Storeの決済を使用しなければならありません。外部決済への誘導は禁止
- ガイドライン3.1.3: リーダーアプリ(Kindleのような外部で購入したコンテンツを見るアプリ)は例外あり
- 広告: インタースティシャルは「意図せずタップしにくい」配置であること。起動直後3秒以内のフルスクリーン広告は審査落ちの原因になる
// App Store審査で問題になるパターン(禁止)
// ❌ 外部サイトへの決済誘導
Button("ウェブサイトで購入する(お得)") {
UIApplication.shared.open(URL(string: "https://yoursite.com/checkout")!)
}
// ✅ StoreKit経由での購入のみ
Button("Proにアップグレード") {
Task { try await purchase(packageId: "pro_monthly") }
}
Google Play の主要ルール
- 支払いポリシー: デジタルコンテンツへのアクセスにはGoogle Playの課金を使用(ただし、ウェブアプリのラッパーや物理商品は例外)
- 広告: 偽の閉じるボタンや誤タップを誘導するレイアウトは禁止。インタースティシャルに5秒のカウントダウンを表示することが推奨(強制ではない)
- 定期購読の解約: アプリ内から簡単に解約できる必要がある(App Storeより要件が明確)
実際の運用で学んだこと
両プラットフォームを1年以上運用して、最も影響が大きかった発見を3つ挙げます。
発見1: AndroidのAdMob収益は季節変動が大きい
iOSのサブスクリプション収益は比較的安定していますが、AndroidのAdMob収益は広告主の予算サイクルに連動して変動します。Q4(10〜12月)は広告単価が高く、Q1(1〜3月)は下がります。この変動を見越した資金計画が必要です。
発見2: iOS向けの機能がAndroidで喜ばれる
iOSの審査が通ったUIデザインや機能は、そのままAndroidに展開しても高い評価を受けます。Google Playのユーザーもデザインのクオリティに敏感で、「iOSっぽい丁寧なUI」は好意的に受け取られます。
発見3: 年額プランの比率がLTVを決める
月額プランより年額プランを選ぶユーザーの比率が高いほど、解約率が下がりLTVが上がります。年額プランの比率を20%から35%に引き上げただけで、6ヶ月後のMRRが17%増加した経験があります。年額プランの訴求は、両プラットフォームで積極的に行う価値があります。
次のアクション
デュアルストア展開の最初の一歩は、Android版のリリースではなく「RevenueCatのプロジェクトにAndroidを追加する」作業です。既存のiOS向けSDK実装をそのままAndroidでも使えるため、課金ロジックのデュアル対応はそれほど難しくありません。
最も労力がかかるのはストアへの登録作業(スクリーンショット・説明文の各ストア向け最適化)と、Android実機でのテストです。Rork MaxでAndroid向けのビルドを生成するフローを一度確立してしまえば、その後の展開は想像より速く進みます。
iOS単独で収益が出ているなら、Androidへの展開は「ほぼリスクなし」の収益拡張です。今月の作業リストに追加してみてください。