2本目を出す朝に、いちばん答えにくかった問い
新しい壁紙アプリのビルドを提出しようとして、Play Console の前で手が止まった朝がありました。技術的な不備は見当たりません。それでも指が動かなかったのは、「このアプリは、もう1本のほうと何が違うのですか」と聞かれたときに、自分の言葉で答えられる気がしなかったからです。
コードベースは共通です。画面の骨格も、課金の導線も、設定画面も同じものを使い回しています。違うのは配信している画像の中身だけ——そう説明しようとして、それは説明になっていないのではないかと気づきました。
個人開発でアプリを何本も並行して運用していると、この問いは定期的に戻ってきます。そして今月、Google Play が「低価値アプリ」の線引きを明確にしたことで、問いの温度が少し上がりました。ここでは、私自身が提出前に回している確認の手順を、そのまま書き残します。
Play が言う「低価値」には枝が2本あります
今回はっきりしたのは、低価値と見なされるものの内訳です。ひとつは既存アプリの焼き直しであること。もうひとつは、サードパーティのモデルを包んだだけで固有の有用性がないことです。
この2本の枝は、当たる人がまったく違います。
枝 当たりやすい人 問われているもの
既存アプリの焼き直し 1つのコードベースから兄弟アプリを出している運用者 2本目が1本目と別の体験になっているか
モデルを包んだだけ AI ビルダーで素早く形にした人 モデルを外したときに何が残るか
Rork のようなツールで作り始めた方は、後者の枝を先に意識されると思います。ただ、アプリが2本目・3本目と増えていくと、いつのまにか前者の枝にも足がかかります。私が引っかかったのは、まさにその移り変わりのところでした。
あわせて、サードパーティの AI 連携にもユーザーデータ要件が適用されることが明確になりました。外部のモデルへ何を送っているかを開示する責任は、ビルダーの提供元ではなく、アプリを公開している側にあります。ここは後段でもう一度触れます。
重なりを、感覚ではなく4つの面で数えます
最初のうち、私は差別化をスクリーンショットで考えていました。並べたときに違って見えれば大丈夫だろう、という程度の見立てです。結果は芳しくありませんでした。見た目は変えられても、審査で問われるのは「そのアプリでしか得られないものが一つでもあるか」であって、印象の話ではないのです。
いまは、2本のアプリを次の4つの面に分解して、それぞれの重なりを数えています。
カタログ — 配信している素材そのものの集合
文言 — 同じキーに同じ文字列が入っているかどうか
画面 — ルート画面の構成
機能 — そのアプリでできることの一覧
数えるだけであれば、集合の重なりで足ります。カタログは兄弟アプリなら重なって当然ですから上限を緩く、体験に近い面ほど厳しく置きます。この「面ごとにしきい値を変える」ところが要点で、全部を一律で見ると、変えるべきでない部分まで無理に変えることになります。
提出前に走らせている60行
以下が、実際に回しているスクリプトです。2本分のマニフェスト(JSON)を渡すと、面ごとの重なりと、B 側にしかない機能を出します。
#!/usr/bin/env python3
"""distinct_value.py — 兄弟アプリ2本の「固有価値」の重なりを提出前に数える。
使い方:
python3 distinct_value.py app_a.json app_b.json
終了コード 1 = しきい値超過(提出前に手を入れる)
"""
import json
import sys
# しきい値。カタログは重なって当然なので緩く、体験に近いものほど厳しく。
THRESHOLDS = { "catalog" : 0.60 , "copy" : 0.35 , "screens" : 0.50 , "features" : 0.70 }
def load (path):
with open (path, encoding = "utf-8" ) as f:
m = json.load(f)
# 欠けている面は「空集合」ではなく明示的な失敗にする
for key in ( "catalog" , "copy" , "screens" , "features" ):
if key not in m:
raise KeyError ( f " { path } : ' { key } ' がありません" )
return m
def jaccard (a, b):
sa, sb = set (a), set (b)
if not sa and not sb:
return 0.0
return len (sa & sb) / len (sa | sb)
def copy_overlap (a, b):
"""文言は「同じキーに同じ文字列が入っているか」で見る。
キーだけ揃っていても中身が違えば別のアプリとして読める。"""
keys = set (a) & set (b)
if not keys:
return 0.0
same = sum ( 1 for k in keys if a[k].strip() == b[k].strip())
return same / len ( set (a) | set (b))
def main (path_a, path_b):
a, b = load(path_a), load(path_b)
scores = {
"catalog" : jaccard(a[ "catalog" ], b[ "catalog" ]),
"copy" : copy_overlap(a[ "copy" ], b[ "copy" ]),
"screens" : jaccard(a[ "screens" ], b[ "screens" ]),
"features" : jaccard(a[ "features" ], b[ "features" ]),
}
failed = []
print ( f " { '面' :<10 }{ '重なり' :>8 }{ '上限' :>8 } 判定" )
for key, value in scores.items():
limit = THRESHOLDS [key]
ok = value <= limit
if not ok:
failed.append(key)
print ( f " { key :<10 }{ value :>8.2f }{ limit :>8.2f } { 'ok' if ok else 'OVER' } " )
only_a = sorted ( set (a[ "features" ]) - set (b[ "features" ]))
only_b = sorted ( set (b[ "features" ]) - set (a[ "features" ]))
print ( f " \n A だけの機能: { only_a or '(なし)' } " )
print ( f "B だけの機能: { only_b or '(なし)' } " )
if not only_b:
failed.append( "features:B-side-empty" )
print ( "B に固有の機能がありません。提出時に説明できる材料が手元にない状態です。" )
if failed:
print ( f " \n NG: { ', ' .join(failed) } " )
return 1
print ( " \n OK: 4 面すべてしきい値内です。" )
return 0
if __name__ == "__main__" :
if len (sys.argv) != 3 :
print ( __doc__ )
sys.exit( 2 )
sys.exit(main(sys.argv[ 1 ], sys.argv[ 2 ]))
渡すマニフェストは、この程度の粒度で足ります。ビルド設定から機械的に吐き出せる形にしておくと、更新のたびに手で書き直さずに済みます。
{
"catalog" : [ "ukiyoe-01" , "ukiyoe-02" , "ukiyoe-03" , "sky-03" , "sky-04" ],
"copy" : {
"home.title" : "今日の一枚" ,
"paywall.headline" : "広告なしで使う" ,
"empty.state" : "まだ集めた作品がありません"
},
"screens" : [ "home" , "category" , "detail" , "favorite" , "settings" , "artist" ],
"features" : [ "set-as-lockscreen" , "category-browse" , "artist-notes" , "restoration-before-after" ]
}
only_b が空のときにわざと失敗させているところは、本番の提出で一度ひやりとしてから足した判定です。数字がしきい値内に収まっていても、B 側に固有の機能が一つもなければ、審査で問われたときに差し出す材料が手元にありません。数えるための道具が、答えるための道具も兼ねるようにしています。
予想と逆でした。差は素材ではなく文言と画面の並びに出ます
素材を総入れ替えすれば十分だろう、というのが私の見立てでした。実際に走らせた結果は、その見立てを裏返すものでした。
面 重なり 上限 判定
catalog 0.17 0.60 ok
copy 0.67 0.35 OVER
screens 0.83 0.50 OVER
features 0.33 0.70 ok
A だけの機能: ['daily-pick', 'hdr-preview']
B だけの機能: ['artist-notes', 'restoration-before-after']
NG: copy, screens
カタログの重なりは 0.17 まで下がっているのに、文言が 0.67、画面構成が 0.83 で引っかかりました。素材を入れ替えても、同じ骨格から生えた2本目は、言葉と導線がほとんど元のままなのです。
考えてみれば当たり前でした。画像を差し替える作業は目に見えますから真っ先に手を付けます。一方で home.title や課金画面の見出しは、コードベースの共通部分にあるので、差し替える発想そのものが出てきません。共通レイヤーに置いた文言は、兄弟アプリを増やすときの落とし穴になりやすい部分なのだと思います。目に見える部分から直していると、いちばん重なっている部分が最後まで残ります。 この一文を貼っておくために、私はこのスクリプトを提出フローに入れました。
そこから、2本目の画面構成と文言に手を入れました。作品ごとの注記と、修復前後を並べる画面を足し、共通で使い回していた見出しを書き直しています。同じ判定を通した結果が次です。
面 重なり 上限 判定
catalog 0.00 0.60 ok
copy 0.00 0.35 ok
screens 0.38 0.50 ok
features 0.14 0.70 ok
A だけの機能: ['category-browse', 'daily-pick', 'hdr-preview']
B だけの機能: ['artist-notes', 'print-provenance', 'restoration-before-after']
OK: 4 面すべてしきい値内です。
画面の重なりは 0.38 で残っています。設定画面や詳細画面まで別物にする必要はありませんし、そこを無理に変えると使いにくくなるだけです。上限を面ごとに変えておいたのは、このためでした。
外部モデルを1つ足すと、宣言の1枚に行が増えます
もう1本の枝、「モデルを包んだだけ」のほうにも触れておきます。
固有価値のマニフェストは、そのまま開示のたたき台にもなります。外部のモデルを呼ぶ機能を1つ足すと、features に1行増えるだけでは済みません。何を送っているか、どこへ送っているか、保存されるのか——この3つが同時に増えます。そして開示する責任は、生成してくれたツールの側ではなく、公開している私の側にあります。
そこで、機能の行にデータの出入りを併記する形にしています。
項目 書く内容 いつ書くか
宛先 呼び出す外部サービスの名前 実装した当日
送るもの ユーザー由来のデータが混ざるか(入力文・画像・識別子) 実装した当日
残るもの 先方に保存されるか、学習に使われるか 実装した当日
抜け道 その機能を使わずにアプリが成立するか 提出の前日
最後の「抜け道」の行が、2本目の枝への答えになります。その機能を外してもアプリとして成立するのであれば、それは包んだだけではありません。逆にここが空欄のままなら、提出前に考え直す合図として受け取っています。
ビルドが通ることと、期限や要件を満たしていることは別の話です。この感覚は、target API 36 の8月31日は、更新を出す人と出さない人で意味が違います で書いた「提出できるか」と「見えるか」の切り分けと、根が同じだと感じています。
この1枚が効かない場面
万能ではありません。使いながら見えてきた限界を3つ書いておきます。
数字は根拠ではなく着眼点です。 重なりが低いことは、固有価値があることの証明にはなりません。あくまで「どこを見るべきか」を教えてくれるだけです。
カタログの中身までは見ていません。 識別子が違っても、素材の傾向がそっくりであれば読み手には同じに映ります。この場合は数字を信じ込まずに、2本のストア掲載画像を並べて自分の目で確かめるようにしています。
申告のもとになる SDK の実態は別の道具で確かめます。 自分で書いていないコードが何を送っているかは、依存の一覧を機械で走査する必要があります。この点はPlay Policy Insights を Expo プロジェクトに通すと、検出が0件になる条件が3つあります で整理しました。
もうひとつ、更新を止めているアプリには、この確認自体が届きません。手を入れないアプリをどう扱うかは、Google Play の期限後、更新を止めた Android アプリを上げるか据え置くかの線引き のほうに書きました。
次の一歩
まずは、いま運用しているアプリのうち2本を選んで、features の配列だけを手で書き出してみていただければと思います。カタログや文言まで揃える必要はありません。片方にしか無い機能を挙げようとして手が止まったなら、そこが提出前に埋めておくべき場所です。
私自身、この確認を始めてから、2本目を出す朝の手の止まり方が変わりました。答えを用意してから提出する、というだけのことなのですが、審査を待つあいだの落ち着きがまるで違うのです。
お読みいただきありがとうございました。