2026年のAndroid SplashScreen API: 白いフラッシュのないコールドスタート
Android SplashScreen APIの実践ガイド — テーマの設定、誰も読まないアニメーションアイコンのサイズ制限、まだ準備できていないデータのためのkeepOnScreenCondition。
すべてのAndroidアプリは、望むと望まざるとにかかわらず、Android 12以降スプラッシュ画面を表示します。ユーザーがランチャーをタップした瞬間、システムはアプリのアイコンとテーマの色から自動的にそれを描画します — 実際に持っている唯一の選択肢は、デフォルトを受け入れるか、androidx.core.splashscreenを設定して本来出荷するつもりだったものを描画するかです。私が見てきた出来の悪いスプラッシュ画面の多くは、誰かが下手にデザインしたから悪いのではありません。誰も何も設定しなかったため、システムの最良の推測がその隙間を埋めてしまったから悪いのです。
このガイドのすべての落とし穴は、Granynを作る過程で経験しました。そのダッシュボードは、最初のフレームが表示に値する前に、Roomから読み込んだ最初の残高数値を必要とします。これが白いフラッシュを止め、スプラッシュ画面が一拍長く居座るのを止めた設定です。
望まなくてもスプラッシュ画面が表示される理由
Android 12(API 31)以降、プラットフォームはすべてのコールドスタートを捕捉し、Activityがコンテンツビューをアタッチする前にシステムが描画するスプラッシュウィンドウを表示します。これはスキップできるライブラリ機能ではなく、フレームワークのデフォルト動作です。androidx.core.splashscreenが提供するのは、そのシステムウィンドウをAPI 23まで一貫して設定できる互換性シムです。これがなければ、12以降では本物が得られますが、それ以外のすべてでは単なる白または黒の四角形になってしまいます。
設定をスキップすると、システムのデフォルトが得られます。テーマから引き出された単色の背景の上にランチャーアイコンが表示され、アニメーションはなく、表示時間の制御もありません。それは壊れているわけではありませんが、ブランドを意識したアプリが望むものであることはほとんどなく、互換ライブラリのない12未満のデバイスでは、テーマが適用される前に不快な白いフラッシュになることがあります。
テーマの配線
スプラッシュ画面は、setContentViewが実行される前に適用されるテーマとして、ほぼ完全にXMLで設定されます。
<!-- res/values/themes.xml -->
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/brand_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="windowSplashScreenAnimationDuration">500</item>
<item name="postSplashScreenTheme">@style/Theme.App</item>
</style>
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContent { AppTheme { GranynApp() } }
}
}
installSplashScreen()はsuper.onCreate()より前に実行される必要があります — 後で呼び出すと互換ライブラリが間に合ってウィンドウをインターセプトできず、静かにシステムのデフォルトに戻ってしまいます。postSplashScreenThemeは、スプラッシュ画面が閉じられた後にウィンドウが切り替わるテーマです。これを設定し忘れると、Activityが実際のコンテンツにスプラッシュウィンドウの属性が適用されたまま、一時的にレンダリングで詰まってしまいます。
痛い目を見るまで誰も読まないアイコンサイズの制限
windowSplashScreenAnimatedIconには厳格な制約があります。drawableは240×240dpの円の中に描画され、それより大きいものはスケーリングされるのではなく、静かにトリミングされます。これは私が見る中で最も一般的なスプラッシュ画面のバグです — 誰かが、72×72dpの表示可能な円の中に収まる108×108dpのフォアグラウンドレイヤー用に設計された適応アイコンを再利用してしまい、スプラッシュの境界を超えてはみ出すか、まさに1つの画面密度でだけ壊れて見える形でトリミングされてしまいます。
解決策は、ランチャーのアセットを流用するのではなく、240dp制約のために独自にサイズ調整され、中央に配置された専用のスプラッシュアイコンです。
res/drawable/splash_icon.xml (単一のベクター、適応レイヤーなし、240x240dpに収まる)
アイコンをアニメーション化する必要がある場合(AnimatedVectorDrawable)、同じサイズの上限が適用され、windowSplashScreenAnimationDurationはプラットフォームがそれを待つ時間を制限します — API 31以降では最大1000msですが、古いAPIの互換ライブラリはその上限を同じように強制しないため、1つのOSバージョンしか確認しない場合、それ自体がテストの不整合の原因になります。
データがまだ準備できていない場合: keepOnScreenCondition
Granynにとって本当に重要だった部分はアイコンではなく、タイミングでした。スプラッシュ画面は最初のフレームが描画準備できた瞬間に閉じますが、Composeの画面ではViewModelが表示するものを持つ前にそれが起こることがあります。放っておくと、スプラッシュ画面が消えてから残高が半秒後に読み込まれるまでの間に空のダッシュボードのフラッシュが発生します — これは、スプラッシュ画面がそのわずかな時間だけ余分に表示され続けるよりも悪い結果です。
SplashScreen.setKeepOnScreenConditionは、条件がクリアされるまでスプラッシュウィンドウを開いたままにします。
class MainActivity : ComponentActivity() {
private val viewModel: DashboardViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
splashScreen.setKeepOnScreenCondition {
!viewModel.isReady.value
}
setContent { AppTheme { GranynApp(viewModel) } }
}
}
ここでのisReadyは、ダッシュボードの残高に対する最初のRoomクエリが実際に結果を返した時点でtrueに切り替わるStateFlow<Boolean>です。この条件は毎フレームポーリングされるため、安価なブール読み取りにとどめておいてください — ラムダの中で直接suspend呼び出しやデータベースアクセスを行ってはいけません。
これを無期限にアプリをハングさせる方法にしないために、2つのことが重要です。まず、上限を設定してください — isReadyが決してtrueにならない場合(破損したデータベース、ハングしたクエリ)、スプラッシュ画面はタイムアウトし、永遠にフリーズしたままになるのではなく、UIが独自の読み込み状態やエラー状態を表示できるようにするべきです。
private val isReady = MutableStateFlow(false)
init {
viewModelScope.launch {
withTimeoutOrNull(2000) {
dashboardFlow.first { it != null }
}
isReady.value = true
}
}
次に、setKeepOnScreenConditionは終了を遅らせるだけです — 入力やアプリの残りの起動をブロックすることはないので、ツリーのさらに下にある実際の読み込み状態の代替として使わないでください。これはアプリがハングしているように見え始める前に、せいぜい数百ミリ秒の忍耐を買うだけです。
重要なら終了アニメーションを
デフォルトでは、スプラッシュビューは単に削除されるだけです。終了時のカスタムフェードやスケーリングがブランドの雰囲気にとって重要な場合、setOnExitAnimationListenerは、自分で破棄する前にアニメーションさせる実際のスプラッシュビューを提供します。
splashScreen.setOnExitAnimationListener { splashView ->
splashView.view.animate()
.alpha(0f)
.setDuration(200)
.withEndAction { splashView.remove() }
.start()
}
最後にsplashView.remove()を省略すると、スプラッシュビューがコンテンツの上に無期限にアタッチされたままになります — ホットリロード開発ループでは問題なく見えるのに、恒久的にフリーズした起動画面を出荷してしまう、見落としやすい一行のミスです。
本当に重要だったこと
何も設定しない場合にシステムが与えるデフォルトは壊れてはいませんが、意図的でもありません — super.onCreate()より前にスプラッシュテーマをインストールし、再利用した適応ランチャーアイコンではなく実際の240dp制約に合わせてアイコンをサイズ調整し、keepOnScreenConditionは最初のフレームが本当に必要とする特定の時間指定されたデータがある場合にのみ使用してください。終了アニメーションは完全に省略しやすい部分であり、ほとんどのアプリにとってそれが正しい判断です — ここでの成果は、モーションそのものを加えることではなく、白いフラッシュと空のダッシュボードのちらつきを取り除くことです。
// 関連記事
ジャーナルの他の記事
Jetpack ComposeでのMaterial Youダイナミックカラー:壁紙が勝ったときにブランドカラーを守る方法
dynamicColorScheme()はユーザーの壁紙から生成されたパレットであなたのパレットを置き換える。ブランドカラーを失うのではなく調和させるための実践ガイド。
Android の In-App Review API:うっとうしくならずに評価を頼む方法
Google の In-App Review API の実践ガイド。実際の仕組み、表示すべきタイミング、そしてありがちな『評価してください』ポップアップが Play ストアの評価を静かに損なっている理由。
2026年のAndroidアクセシビリティ:実際に使えるTalkBackとCompose semanticsのチェックリスト
2026年版、実践的なAndroidアクセシビリティチェックリスト——TalkBack、Compose semantics、タップ領域、そしてリリース前に毎回行うテストの手順。