Android & モバイル UI/UX エンジニアリングガイド
Jetpack Compose、Material 3、オフラインファーストの復元力、人間中心のモバイル人間工学を用いたネイティブAndroidアプリケーションの設計と構築のための、実戦でテストされた運用フレームワーク。
1. コアモバイル人間工学と親指ゾーン
モバイルデバイスは70%以上の時間、片手で保持されます。親指の到達範囲、触覚フィードバック、最小限の物理的摩擦を考慮してインターフェースを設計します。
1.1 人間工学に基づく到達可能性の階層
+---------------------------+ <- 画面上部(表示専用)
| ステータスとヘッダー | 最も届きにくい。パッシブな
| タイトル、クォータメーター | ステータス、検索バー、ライブメーター用。
+---------------------------+
| | <- 中間リーチゾーン
| スクロール可能コンテンツ | リストアイテム、詳細、ブラウジング可能な
| 受信トレイカード、スニペット | コンテンツカードに最適。
| |
+---------------------------+
| プライマリアクションターゲット| <- プライム親指ゾーン(下部40%)
| 送信ボタン、タブ | 最もタップしやすい。ナビゲーション、CTA、
| ボトムシート、フィルター | インタラクティブなコントロールはここに配置。
+---------------------------+
1.2 必須のタッチターゲット標準
- 最小インタラクティブターゲット: すべてのクリック可能な要素は、視覚的なアイコンが24dpであっても、
$48 \times 48\text{dp}$(Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp))以上である必要があります。 - エッジインセットとセーフエリア: ルートのComposeレイアウトは常に
Scaffoldでラップするか、Modifier.windowInsetsPadding(WindowInsets.safeDrawing)を適用して、カメラの切り欠き、ジェスチャー、ナビゲーションバーに対応します。 - フォームコントロールは下部に: テキスト入力、送信ボタン、確認ダイアログは
ModalBottomSheetまたは下部固定カードに配置し、ソフトウェアキーボード(imePadding())がアクティブなフィールドを隠さないようにします。
2. 速度と知覚パフォーマンス
モバイルユーザーは遅延を即座に認識します。以前にフェッチしたデータが利用可能な場合は、ナビゲーションをブロックしたり、空のローディング状態を表示したりしないでください。
2.1 ゼロスケルトンルール(Stale-While-Revalidate)
- 問題点: ユーザーが受信トレイタブをタップするたびに全ページのスケルトンローダーや円形スピナーを表示すると、動作が遅く、ぎこちなく、洗練されていない印象を与えます。
- 解決策: Stale-While-Revalidate(SWR)キャッシュパターンを実装します。
- フェッチしたアイテムをローカルの暗号化ストレージ(
EncryptedSharedPreferencesまたはSQLite/Room)に保存します。 - 画面に入るとき、キャッシュされたアイテムを即座にレンダリングします(0msの遅延)。
- キャッシュが5〜10分以上古い場合、バックグラウンドで再検証します。
- 更新されたデータが到着したら、リストをクリアしたり空白のプレースホルダーを表示したりせずに、アイテムをスムーズに追加/更新します。
- バックグラウンド更新中は、控えめなインジケーター(プル・トゥ・リフレッシュスピナーや小さな上部プログレスバーなど)のみを表示します。
- フェッチしたアイテムをローカルの暗号化ストレージ(
2.2 オフラインファーストアウトボックスパターン
モバイルデバイスのセルラー接続が不安定であったり、一時的に機内モードであったりしても、ユーザーアクションを失敗させないでください。
flowchart TD
UserAction["ユーザーがファイルを共有/メモを入力"] --> LocalDraft["1. ローカルアウトボックス下書きに保存"]
LocalDraft --> RenderOutbox["2. アウトボックスキューカードに即座にレンダリング"]
RenderOutbox --> CheckNet{"ネットワークに接続中?"}
CheckNet -- はい --> DirectUpload["3. 直接アップロード"]
CheckNet -- いいえ --> EnqueueWork["3. WorkManagerジョブを登録(NetworkType.CONNECTED)"]
EnqueueWork --> WaitConn["4. Wi-Fi/セルラーを待機"]
WaitConn --> AutoDispatch["5. 指数バックオフ付きバックグラウンドアップロード"]
DirectUpload --> Success["6. UI状態を更新し、トースト通知"]
AutoDispatch --> Success
アウトボックスUI状態:
- 保留中: 「ネットワークを待機中…」と一時停止したクラウドアイコン。
- アップロード中: 「送信中 42%…」と線形プログレスインジケーター。
- 失敗 / 再試行中: 明確なステータスインジケーターと明示的な再試行および破棄ボタン。
3. モーション、トランジション、触覚ハプティクス
モダンなAndroidデザイン(Android 14/15以降)では、動的で物理ベースのモーションと触覚的感覚フィードバックが必要であり、ネイティブで応答性が高いと感じさせるために不可欠です。
3.1 エッジツーエッジとシステムバーの処理
MainActivity.ktで常にシステムバーの背後に描画します。
enableEdgeToEdge()
setContent {
VaultTheme {
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
AppNavHost(Modifier.padding(innerPadding))
}
}
}
3.2 予測的戻るジェスチャー
AndroidManifest.xmlで予測的戻るを有効にし(android:enableOnBackInvokedCallback="true")、ボトムシートや詳細ドロワーにはPredictiveBackHandlerを使用します。
PredictiveBackHandler(enabled = isDetailOpen) { progressFlow ->
try {
progressFlow.collect { backEvent ->
// backEvent.progress (0f から 1f) に比例して詳細シートを拡大縮小または移動
detailScale = 1f - (backEvent.progress * 0.1f)
}
// ユーザーが戻るジェスチャーを確定
closeDetail()
} catch (e: CancellationException) {
// ユーザーが戻るジェスチャーを中止、1f にスプリングバック
detailScale = 1f
}
}
3.3 共有要素トランジション
受信トレイリスト行とアイテム詳細ビューの間で遷移する場合、画面をSharedTransitionLayoutでラップします。
SharedTransitionLayout {
NavHost(...) {
composable("inbox") {
InboxScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
composable("detail/{id}") {
DetailScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this
)
}
}
}
サムネイルカード、アバターアイコン、タイトルに.sharedElement()または.sharedBounds()を使用して、シームレスなトランジションを作成します。
3.4 触覚ハプティクス
物理的なマイルストーンが発生したときに、ユーザーアクションを無音のままにしないでください。
- 閾値スワイプ(スワイプ・トゥ・ディスミス): スワイプがアクション閾値を超えたときに
HapticFeedbackConstantsCompat.GESTURE_STARTをトリガーします。 - クリップボードにコピー / 送信完了:
HapticFeedbackConstantsCompat.CONFIRMをトリガーします。 - バリデーション失敗 / ネットワークエラー:
HapticFeedbackConstantsCompat.REJECTをトリガーします。
val view = LocalView.current
fun triggerHaptic(feedbackConstant: Int) {
view.performHapticFeedback(feedbackConstant)
}
3.5 リニアイージングよりもスプリング物理
硬直したリニアアニメーションを避け、ジェスチャー、シート、ディスミサルには常にスプリング物理を使用します。
val offsetAnim by animateFloatAsState(
targetValue = targetOffset,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
4. スマートワンタップアクションとシステム統合
モバイルインターフェースは、ペイロードタイプを認識し、最も関連性の高いネイティブシステムアクションを提供することで、認知負荷を最小限に抑える必要があります。
4.1 コンテキスト認識アクションマトリックス
| ペイロードタイプ | プライマリアクション | セカンダリアクション | システム統合 |
|---|---|---|---|
| URL / Webリンク | ブラウザで開く(Intent.ACTION_VIEW) | リンクをコピー | Chromeカスタムタブまたはシステムデフォルトブラウザ |
| テキスト / スニペット | テキストをコピー | 共有(Intent.ACTION_SEND) | Android ClipboardManagerと即時トースト/スナックバー |
| ファイル / メディア | フォルダにダウンロード | ファイルを開く | ストレージアクセスフレームワーク(SAF)+ FileProvider |
4.2 カスタムデフォルトディレクトリを使用した直接ファイルダウンロード
ファイルをダウンロードするたびにシステムファイルピッカーをユーザーに表示すると、不要な摩擦が生じます。
- ユーザーが設定/ハブで
ActivityResultContracts.OpenDocumentTree()を使用してデフォルトのダウンロードフォルダを一度だけ選択できるようにします。 - パーミッションを永続的に保持します。
context.contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - ダウンロードをタップしたとき:
- デフォルトディレクトリが設定されている場合:
DocumentFile.createFile()を使用してバイトを直接ストリーミングし、ライブのバイト進行状況を表示し、ネイティブの完了トースト(「document.pdf を Vault にダウンロードしました」)を発行します。 - デフォルトが設定されていない場合:
ActivityResultContracts.CreateDocument()にフォールバックします。
- デフォルトディレクトリが設定されている場合:
4.3 FileProviderを使用した安全なファイルオープン
安全でないraw file:// URIを使用しないでください。ネイティブAndroidアプリケーションへのファイル表示は常にFileProviderを使用して委任します。
<!-- res/xml/file_paths.xml -->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cached_files" path="." />
</paths>
val contentUri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
tempFile
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(contentUri, mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, "次で開く..."))
5. 摩擦のないモバイル認証
モバイルのタッチスクリーンでサーバーURLや長いパスワードを入力すると、タイプミスが発生しやすくなります。
5.1 ログイン優先順位階層
- 優先度1: QRコードカメラスキャン(1秒ペアリング)
- デスクトップウェブアプリに一時的な署名済みペアリングQRコードを表示します。
- モバイルユーザーがアプリを開き、QRコードをスキャンをタップし、カメラを画面に向けます。
- アプリがオリジン、トークン、デバイス名を1回のパスで解析し、即座に認証されます。
- 優先度2: 折りたたみ可能なサーバーURLを使用したユーザー名とパスワード
- 99%のログインはデフォルト設定のホストを使用します。
- デフォルトではユーザー名とパスワードフィールドのみを表示します。
- サーバーURLは控えめな折りたたみ可能なトグル「サーバー設定(上級者向け)」の背後に隠します。
5.2 14日間スライディングセッション永続化
- アクセストークン(
va1_)とリフレッシュトークン(vr1_)をAndroidEncryptedSharedPreferencesに保存します。 - アクティブな使用時にリフレッシュトークンを自動的にスライド/ローテーションします。
- ユーザーが14日以内にアプリを開く限り、再認証なしでログイン状態を維持します。
6. アダプティブレイアウト(折りたたみデバイスとタブレット)
WindowSizeClassを使用して、ウィンドウサイズクラス全体でスムーズにスケーリングする画面を構築します。
- コンパクト(
<600dp): シングルカラム、ボトムナビゲーションバー、モーダルボトムシート。 - ミディアム(
600dp - 840dp): ナビゲーションレール、2カラムグリッド。 - 拡張(
>840dp): 永続的なマスター・ディテールの2ペインレイアウトを並べて配置(左にリスト、右にインスペクター)。
7. Material 3 カラーコントラストと状態アクセシビリティ(WCAG 2.2 AA / AAA)
モバイルデバイスは、可変な周囲光(直射日光、屋外のまぶしさ、OLEDバッテリーセーバーによる調光)により、コントラスト障害が発生しやすいという独自の課題があります。WCAG 2.2 AAAテキストコントラスト($\ge 7:1$)、およびWCAG 1.4.11(非テキスト境界コントラスト$\ge 3:1$)、WCAG 1.4.1(色の使用)の厳格な準拠を目指します。
7.1 Material 3 ダークモードFilterChipの罠
- 欠陥: デフォルトでは、Material 3の
FilterChipはダークモードで、選択された背景にsecondaryContainer(#37474F)を、暗いsurface(#14181E)に対して使用します。これにより、許容できない1.83:1のコントラスト比が生じます。低輝度やまぶしさの中では、ユーザーはどのチップがアクティブか判別できません。 - 修正方法:
selectedBorderColor = MaterialTheme.colorScheme.primary(ダークモードで9.88:1のコントラスト比を生成)を使用して、高コントラストの境界定義を強制します。selectedBorderWidth = 1.5.dpを設定します。- WCAG 1.4.1(色のみに依存しない状態)に準拠するために、先頭にチェックマークアイコンを提供します。
FilterChip( selected = isSelected, onClick = { onSelect() }, label = { Text(label) }, leadingIcon = if (isSelected) { { Icon(Icons.Filled.Done, contentDescription = null, Modifier.size(16.dp)) } } else null, border = FilterChipDefaults.filterChipBorder( enabled = true, selected = isSelected, selectedBorderColor = MaterialTheme.colorScheme.primary, selectedBorderWidth = 1.5.dp ) )
7.2 日光とまぶしさでの可読性基準
- プライマリテキストとサーフェス: ダークモードでは、高コントラストテキスト(
#14181E上のonSurface#E1E2E8または#F8FAFC)を標準化し、**$\ge 13:1$**のコントラスト(WCAG AAAを超過)を実現します。 - フォームコントロールのアウトライン:
OutlinedTextFieldおよびラジオボタン/チェックボックスの境界線は、コンテナカード(Color(0xFF8D9199)または明示的なrgba(255, 255, 255, 0.20))に対して少なくとも**$3:1$**のコントラストを維持するoutlineトークンを使用する必要があります。 - コンパクトプリセット行: コンパクトなチップ行(例: 保持プリセット
1h、4h、12h、1d)をレイアウトする場合、ターゲットサイズを維持し、maxLines = 1をselectedBorderColorとともに適用して、チップが折り返されたり曖昧に見えたりしないようにします。
8. リファレンスオープンソースリポジトリ
設計と実装のインスピレーションとして、これらの業界をリードするオープンソースのAndroidプロジェクトを調査してください。
android/nowinandroid: 動的テーマ設定、アダプティブレイアウト、クリーンアーキテクチャを示すGoogleの主力Material 3 Composeサンプル。skydoves/Pokedex-Compose: プロダクショングレードの共有要素トランジション(.sharedBounds/.sharedElement)とスムーズな詳細シートジェスチャー。plainhub/plain-app: ローカル電話管理、Material Youテーマ設定、レスポンシブなクライアント・サーバー同期の優れた実装。Gurupreet/ComposeCookBook: プロダクションComposeアニメーション、ボトムバー、カスタムキャンバスマイクロインタラクションの網羅的なカタログ。signalapp/Signal-Android: ローカルの暗号化ストレージ、バックグラウンドアウトボックスワーカー、ネットワーク復元力のゴールドスタンダード。