Slide 1

Slide 1 text

Androidアプリの UIバリエーションを あの手この手で確認する Nozomi Takuma DroidKaigi 2024

Slide 2

Slide 2 text

自己紹介 ● Nozomi Takuma ● DeNA SWETグループ ● Androidとテストが好き

Slide 3

Slide 3 text

今日話すことのゴール

Slide 4

Slide 4 text

今日話すことのゴール ● Androidアプリが対応すべきデバイスの環境が増えている 中で、UIの動作確認を楽にする機能・ツールを紹介する ● 日々の動作確認が楽になったり、網羅的にできるようにな ることを目指す

Slide 5

Slide 5 text

目次 ● 環境のバリエーションを整理する ● Compose Previewで確認する ● デバイスで確認する ● 自動テストで確認する

Slide 6

Slide 6 text

環境のバリエーションを整理する

Slide 7

Slide 7 text

環境のバリエーション ● 画面サイズ・画面密度 ● 端末の種類 ● 画面の状態 ● フォントスケール・表示サイズ ● テーマ ● 言語・地域

Slide 8

Slide 8 text

環境のバリエーション ● 画面サイズ・画面密度 ● 端末の種類 ● 画面の状態 ● フォントスケール・表示サイズ ● テーマ ● 言語・地域 それぞれのバリエーションを確認

Slide 9

Slide 9 text

画面サイズ・画面密度 ● 画面サイズ ○ small・normal・large・xlarge ● 画面密度 ○ ldpi・mdpi・hdpi・xhdpi・xxhdpi・xxxhdpi ● ウィンドウサイズクラス ○ 幅と高さそれぞれにcompact・medium・expanded

Slide 10

Slide 10 text

画面サイズ Small 最小レイアウトサイズ: 約320 x 426 dp Normal 最小レイアウトサイズ: 約320 x 470 dp Large 最小レイアウトサイズ: 約480 x 640 dp XLarge 最小レイアウトサイズ: 約720 x 960 dp https://developer.android.com/guide/topics/resources/p roviding-resources.html

Slide 11

Slide 11 text

画面密度 ldpi(低密度) 〜120dpi mdpi(中密度) 〜160dpi hdpi(高密度) 〜240dpi xhdpi(超高密度) 〜320dpi xxhdpi(超超高密度) 〜480dpi xxxhdpi(超超超高密度) 〜680dpi https://developer.android.com/training/multiscreen/scr eendensities

Slide 12

Slide 12 text

画面サイズと画面密度のマトリクス ldpi mdpi tvdpi hdpi xhdpi xxhdpi Total Small 0.50% 0.10% 0.60% Normal 0.10% 0.30% 4.30% 44.60% 23.80% 73.10% Large 1.00% 4.00% 1.00% 9.10% 1.20% 16.30% Xlarge 5.60% 0.10% 4.00% 0.30% 10.00% Total 0.00% 6.70% 4.40% 9.30% 54.50% 25.10% https://developer.android.com/about/dashboards/index.html Distribution dashboard > Screen sizes and densities

Slide 13

Slide 13 text

https://developer.android.com/about/dashboards/index.html Distribution dashboard > Screen sizes and densities 画面サイズと画面密度のマトリクス ldpi mdpi tvdpi hdpi xhdpi xxhdpi Total Small 0.50% 0.10% 0.60% Normal 0.10% 0.30% 4.30% 44.60% 23.80% 73.10% Large 1.00% 4.00% 1.00% 9.10% 1.20% 16.30% Xlarge 5.60% 0.10% 4.00% 0.30% 10.00% Total 0.00% 6.70% 4.40% 9.30% 54.50% 25.10% 組み合わせを選択する際に参考になる 表には含まれていないxxxhdpi(Pixel 7 Pro等)も忘れずに

Slide 14

Slide 14 text

ウィンドウサイズクラス ● ブレイクポイント(レイアウトを決定する分岐条件)のセット ● 端末の画面サイズではなく、アプリが利用可能な表示領域から算出さ れる ● 同じ端末であってもアプリのライフサイクルの中で表示領域は変更さ れる可能性があるため、ウィンドウサイズクラスを利用してレスポン シブなレイアウトを設計する

Slide 15

Slide 15 text

ウィンドウサイズクラス Width Compact 600dp未満 縦向きのスマートフォンの 99.96% Medium 600dp以上 840dp未満 縦向きのタブレットの 93.73% 開いた状態の最も大きなインナー ディスプレイ(縦向き) Expand 840dp以上 横向きのタブレットの 97.22% 横向きの最も大きな展開インナー ディスプレイ Height Compact 480dp未満 横向きのスマートフォンの 99.78% Medium 480dp以上 900dp未満 横向きのタブレットの 96.56% 縦向きのスマートフォンの 97.59% Expand 900dp以上 縦向きのタブレットの 94.25% https://developer.android.com/develop/ui/compose/layouts/adaptive/window-size-classes

Slide 16

Slide 16 text

端末の種類 ● Phone ● Foldable(折りたたみ式)デバイス ● Tablet ● Chromebook

Slide 17

Slide 17 text

画面の状態 ● 画面の向き(Orientation) ● マルチウィンドウモード ○ 分割画面 ○ ピクチャーインピクチャー ○ フリーフォーム ● Foldableデバイスの折りたたみ状態

Slide 18

Slide 18 text

マルチウィンドウモード 分割画面 フリー フォーム ウィンドウ PIP

Slide 19

Slide 19 text

Foldableデバイスの折りたたみ状態 ● 折りたたみ ● 展開 ● 半開き ○ テーブルトップモード ○ ブックモード

Slide 20

Slide 20 text

フォントスケール・表示サイズ ● フォントスケール ○ Android14の場合: 85%・100%・115%・130%・150%・180%・200% ● 表示サイズ ○ 最小幅が変更される ○ Pixel8(Android14)の場合: 485dp・411dp(デフォルト) ・375dp・345dp・320dp

Slide 21

Slide 21 text

テーマ ● ダークモード ● ダイナミックカラー

Slide 22

Slide 22 text

テーマ ダークモード ダイナミック カラー

Slide 23

Slide 23 text

環境のバリエーション ● 画面サイズ・画面密度 ● 端末の種類 ● 画面の状態 ● フォントスケール・表示サイズ ● テーマ ● 言語・地域

Slide 24

Slide 24 text

Compose Previewで確認する

Slide 25

Slide 25 text

Compose Previewで確認する ● 画面サイズ・画面密度 ● 画面の向き ● フォントスケール ● 言語・地域 ● ダークテーマ ● UI Check Mode

Slide 26

Slide 26 text

Compose Preview @Preview @Composable fun GreetingPreview() { MyApplicationTheme { Greeting("Android") } }

Slide 27

Slide 27 text

Compose Preview @Repeatable annotation class Preview( val name: String = "", val group: String = "", @IntRange(from = 1) val apiLevel: Int = -1, // TODO(mount): Make this Dp when they are inline classes val widthDp: Int = -1, // TODO(mount): Make this Dp when they are inline classes val heightDp: Int = -1, val locale: String = "", @FloatRange(from = 0.01) val fontScale: Float = 1f, val showSystemUi: Boolean = false, val showBackground: Boolean = false, val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, @Device val device: String = Devices.DEFAULT, @Wallpaper val wallpaper: Int = Wallpapers.NONE, ) https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling-preview/sr c/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.android.kt

Slide 28

Slide 28 text

画面サイズ・画面密度 @Repeatable annotation class Preview( val name: String = "", val group: String = "", @IntRange(from = 1) val apiLevel: Int = -1, val widthDp: Int = -1, val heightDp: Int = -1, val locale: String = "", @FloatRange(from = 0.01) val fontScale: Float = 1f, val showSystemUi: Boolean = false, val showBackground: Boolean = false, val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, @Device val device: String = Devices.DEFAULT, @Wallpaper val wallpaper: Int = Wallpapers.NONE, )

Slide 29

Slide 29 text

プリセットのデバイスを利用する object Devices { const val DEFAULT = "" const val NEXUS_7 = "id:Nexus 7" const val NEXUS_7_2013 = "id:Nexus 7 2013" .. const val PIXEL_7 = "id:pixel_7" const val PIXEL_7_PRO = "id:pixel_7_pro" const val PIXEL_7A = "id:pixel_7a" const val PIXEL_FOLD = "id:pixel_fold" const val PIXEL_TABLET = "id:pixel_tablet" .. } https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling-preview/sr c/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.android.kt

Slide 30

Slide 30 text

プリセットのデバイスを利用する @Preview(device = "id:pixel_4") @Preview(device = "id:pixel_7_pro") @Composable

Slide 31

Slide 31 text

リファレンスデバイスを利用する object Devices { .. // Reference devices const val PHONE = "spec:id=reference_phone,shape=Normal,width=411,height=891,unit=dp,dpi=420"    const val FOLDABLE = "spec:id=reference_foldable,shape=Normal,width=673,height=841,unit=dp,dpi=420" const val TABLET = "spec:id=reference_tablet,shape=Normal,width=1280,height=800,unit=dp,dpi=240" const val DESKTOP = "spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=dp,dpi=160" .. }

Slide 32

Slide 32 text

リファレンスデバイスを利用する object Devices { .. // Reference devices const val PHONE = "spec:id=reference_phone,shape=Normal,width=411,height=891,unit=dp,dpi=420"    const val FOLDABLE = "spec:id=reference_foldable,shape=Normal,width=673,height=841,unit=dp,dpi=420" const val TABLET = "spec:id=reference_tablet,shape=Normal,width=1280,height=800,unit=dp,dpi=240" const val DESKTOP = "spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=dp,dpi=160" .. } 様々なデバイスに対応したアプリを開発するのを サポートするために定義されたデバイスのセット

Slide 33

Slide 33 text

リファレンスデバイスを利用する @Retention(AnnotationRetention.BINARY) @Target( AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION ) @Preview(name = "Phone", device = PHONE, showSystemUi = true) @Preview(name = "Phone - Landscape", device = "spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420", showSystemUi = true) @Preview(name = "Unfolded Foldable", device = FOLDABLE, showSystemUi = true) @Preview(name = "Tablet", device = TABLET, showSystemUi = true) @Preview(name = "Desktop", device = DESKTOP, showSystemUi = true) annotation class PreviewScreenSizes https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling-preview/sr c/androidMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.android.kt

Slide 34

Slide 34 text

リファレンスデバイスを利用する @Retention(AnnotationRetention.BINARY) @Target( AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION ) @Preview(name = "Phone", device = PHONE, showSystemUi = true) @Preview(name = "Phone - Landscape", device = "spec:width = 411dp, height = 891dp, orientation = landscape, dpi = 420", showSystemUi = true) @Preview(name = "Unfolded Foldable", device = FOLDABLE, showSystemUi = true) @Preview(name = "Tablet", device = TABLET, showSystemUi = true) @Preview(name = "Desktop", device = DESKTOP, showSystemUi = true) annotation class PreviewScreenSizes https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling-preview/sr c/androidMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.android.kt リファレンスデバイスのPreviewをまとめた マルチプレビューテンプレート

Slide 35

Slide 35 text

リファレンスデバイスを利用する @PreviewScreenSizes @Composable

Slide 36

Slide 36 text

カスタムのデバイスを作成する object Devices { .. // Reference devices const val PHONE = "spec:id=reference_phone,shape=Normal,width=411,height=891,unit=dp,dpi=420"    const val FOLDABLE = "spec:id=reference_foldable,shape=Normal,width=673,height=841,unit=dp,dpi=420" const val TABLET = "spec:id=reference_tablet,shape=Normal,width=1280,height=800,unit=dp,dpi=240" const val DESKTOP = "spec:id=reference_desktop,shape=Normal,width=1920,height=1080,unit=dp,dpi=160" .. } リファレンスデバイスに追加して 画面サイズが小さい端末も欲しいなあ

Slide 37

Slide 37 text

カスタムのデバイスを作成する

Slide 38

Slide 38 text

カスタムのデバイスを作成する

Slide 39

Slide 39 text

カスタムのデバイスを作成する @PreviewScreenSizes @Preview(device = "spec:width=320dp,height=426dp,dpi=420") @Composable

Slide 40

Slide 40 text

画面の向き

Slide 41

Slide 41 text

画面の向き @Preview(device = "id:pixel_4") @Preview(device = "spec:parent=pixel_4,orientation=landscape") @Composable

Slide 42

Slide 42 text

フォントスケール @Repeatable annotation class Preview( val name: String = "", val group: String = "", @IntRange(from = 1) val apiLevel: Int = -1, val widthDp: Int = -1, val heightDp: Int = -1, val locale: String = "", @FloatRange(from = 0.01) val fontScale: Float = 1f, val showSystemUi: Boolean = false, val showBackground: Boolean = false, val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, @Device val device: String = Devices.DEFAULT, @Wallpaper val wallpaper: Int = Wallpapers.NONE, )

Slide 43

Slide 43 text

フォントスケール

Slide 44

Slide 44 text

フォントスケール @Preview( device = "id:pixel_4", fontScale = 2.0f ) @Preview( device = "id:pixel_4", fontScale = 1.0f ) @Preview( device = "id:pixel_4", fontScale = 0.85f ) @Composable

Slide 45

Slide 45 text

フォントスケール @Retention(AnnotationRetention.BINARY) @Target( AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION ) @Preview(name = "85%", fontScale = 0.85f) @Preview(name = "100%", fontScale = 1.0f) @Preview(name = "115%", fontScale = 1.15f) @Preview(name = "130%", fontScale = 1.3f) @Preview(name = "150%", fontScale = 1.5f) @Preview(name = "180%", fontScale = 1.8f) @Preview(name = "200%", fontScale = 2f) annotation class PreviewFontScale https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling-preview/sr c/androidMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.android.kt 設定可能なfontScaleのパターンを まとめたマルチプレビューテンプレート

Slide 46

Slide 46 text

言語・地域 @Repeatable annotation class Preview( val name: String = "", val group: String = "", @IntRange(from = 1) val apiLevel: Int = -1, val widthDp: Int = -1, val heightDp: Int = -1, val locale: String = "", @FloatRange(from = 0.01) val fontScale: Float = 1f, val showSystemUi: Boolean = false, val showBackground: Boolean = false, val backgroundColor: Long = 0, @UiMode val uiMode: Int = 0, @Device val device: String = Devices.DEFAULT, @Wallpaper val wallpaper: Int = Wallpapers.NONE, )

Slide 47

Slide 47 text

言語・地域 アプリのロケールディレクトリ(例:values-ja)から 選択肢を出してくれる

Slide 48

Slide 48 text

言語・地域 @Preview( device = "id:pixel_4", locale = "en" ) @Preview( device = "id:pixel_4", locale = "ja" ) @Preview( device = "id:pixel_4", locale = "ar" ) @Composable

Slide 49

Slide 49 text

@Preview( device = "id:pixel_4", locale = "en" ) @Preview( device = "id:pixel_4", locale = "ja" ) @Preview( device = "id:pixel_4", locale = "ar" ) @Composable 言語・地域 RTLレイアウトを確認したい場合は RTL言語を指定する

Slide 50

Slide 50 text

ダークモード @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable

Slide 51

Slide 51 text

ダークモード @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable isSystemInDarkThemeがtrueになる

Slide 52

Slide 52 text

UI Check Mode ● Preview右上「Start UI Check Mode」から起動 ● マルチプレビューテンプレートのPreview全種類を自動で作成 ○ PreviewScreenSizes・PreviewFontScale・PreviewLightDark・Previ ewDynamicColors ● 作成したPreviewに対してユーザー補助検証ツールとVisual Lintを実 行した結果を教えてくれる

Slide 53

Slide 53 text

UI Check Mode TextField( modifier = Modifier.fillMaxWidth(), value = value, placeholder = { Text("Input you name") }, ) Button(modifier = Modifier .fillMaxWidth()) { Text(text = "Button") }

Slide 54

Slide 54 text

UI Check Mode TextField( modifier = Modifier.fillMaxWidth(), value = value, placeholder = { Text("Input you name") }, ) Button(modifier = Modifier .fillMaxWidth()) { Text(text = "Button") }

Slide 55

Slide 55 text

UI Check Mode

Slide 56

Slide 56 text

UI Check Mode

Slide 57

Slide 57 text

UI Check Mode ユーザー補助検証ツールで検出された問題

Slide 58

Slide 58 text

UI Check Mode Visual Lintで検出された問題

Slide 59

Slide 59 text

ユーザー補助検証ツール ● 画面をスキャンし、ユーザー補助機能を改善するための提案をする ○ コンテンツサイズ ○ タップターゲットのサイズ ○ クリック可能アイテム ○ テキストと画像のコントラスト https://support.google.com/accessibility/android/faq/6 376582

Slide 60

Slide 60 text

Visual Linting ● ドキュメントはないが、VisualLintServiceのソースコードから チェック項目を確認できる ○ Android Code Search > VisualLintService.kt ● さきほどの例だとButtonSizeAnalyzerで引っかかった ○ 実装されているチェック項目の数は多くはないが、Material Designの推奨にそっているかを見ているものが半分ほど

Slide 61

Slide 61 text

Compose Previewで確認する ● 画面サイズ・画面密度 ● 画面の向き ● フォントスケール ● 言語・地域 ● ダークテーマ ● UI Check Mode

Slide 62

Slide 62 text

デバイスで確認する

Slide 63

Slide 63 text

デバイスで確認する ● デバイスの確認はComposeのPreviewほど楽に環境のバリエーションを 作成できないが、デバイスでないと確認が難しいものも多々ある ○ マルチウィンドウモードの時の動作等 ● できるだけ1つのデバイスでバリエーションを確認できたり、設定の 変更を少ない手順でできると嬉しい

Slide 64

Slide 64 text

デバイスで確認する ● Resizable Emulator ● 画面サイズ・画面密度 ● Device setting shortcut ● 画面の状態 ● 言語・地域

Slide 65

Slide 65 text

Resizable Emulator ● Phone・Foldable・Tabletの3つのリファレンスデバイスに切替可能な エミュレーター ● Foldable選択時に開閉状態を変更することも可 ● システムイメージはAPIレベル34以降のみ

Slide 66

Slide 66 text

画面サイズ・画面密度の変更 ● Resizable Emulatorは便利だが、次のデメリットもある ○ 利用できるAPIレベルが限定される ○ 小さい画面がない ○ まだExperimentalで、動作が不安定な場合がある

Slide 67

Slide 67 text

画面サイズ・画面密度の変更 ● Resizable Emulatorは便利だが、次のデメリットもある ○ 利用できるAPIレベルが限定される ○ 小さい画面がない ○ まだExperimentalで、動作が不安定な場合がある adbコマンドで任意の値に変更可

Slide 68

Slide 68 text

画面サイズ・画面密度の変更 // 現在のサイズを取得する $ adb shell wm size // サイズを変更する(dp指定) $ adb shell wm size 320dpx480dp // サイズを変更する(ピクセル指定) $ adb shell wm size 840x1260 // 設定をもとに戻す $ adb shell wm size reset

Slide 69

Slide 69 text

画面サイズ・画面密度の変更 // 現在の画面密度を取得する $ adb shell wm density // 画面密度を変更する $ adb shell wm density 480 // 設定をもとに戻す $ adb shell wm density reset

Slide 70

Slide 70 text

画面サイズ・画面密度の変更 $ adb shell wm size -> Physical size: 1080x2400 $ adb shell wm size -> Physical density: 420 $ adb shell wm size 320dpx480dp

Slide 71

Slide 71 text

Device setting shortcut ● ダークテーマ・フォントスケール・表示サイズをAndroid Studioから 変更できる ● デバイスミラーリングをした実機でも使用可能。実機の場合はさらに 次の機能のON/OFFも変更可 ○ トークバック ○ Select to speak

Slide 72

Slide 72 text

Device setting shortcut

Slide 73

Slide 73 text

Device setting shortcut

Slide 74

Slide 74 text

Device setting shortcut

Slide 75

Slide 75 text

画面の状態 ● 画面回転・Foldable端末の折りたたみ状態はメニューから変更可能 ● Foldable端末はVisual Sensorsからさらに柔軟な設定が可能

Slide 76

Slide 76 text

言語・地域 ● エミュレーターなどadb rootができる場合はadbコマンドで変更可で きるが、利用できるシーンが限られる ● そうでない場合はAppiumの設定アプリ経由でadbコマンドから変更で きるが、やや手間 ● 一部のエミュレーターはDevice setting shortcutにも設定項目があ るが、利用できる条件は不明 ● RTLレイアウトへの変更は開発者オプションから変更可

Slide 77

Slide 77 text

adbコマンドでの言語変更 $ adb root $ adb shell // 言語タグを指定 $ setprop persist.sys.locale ja-JP;stop;sleep 5;start https://developer.android.com/guide/topics/resources/localization #emulator

Slide 78

Slide 78 text

Appiumの設定アプリからの変更 github.com/appium/io.appium.settingsのリリースからapkをダウンロー ドし端末にインストール後、次のコマンドを実行 // Appiumの設定アプリにパーミッション付与 $ adb shell pm grant io.appium.settings android.permission.CHANGE_CONFIGURATION // API Level34以上の場合、非 SDK インターフェースへのアクセスを有効にする $ adb shell settings put global hidden_api_policy 1 // Appiumの設定アプリにBroadcastを送信して言語と地域を変更 $ adb shell am broadcast -a io.appium.settings.locale \ -n io.appium.settings/.receivers.LocaleSettingReceiver \ --es lang ja --es country JP

Slide 79

Slide 79 text

RTLレイアウトへの変更 ● 「開発者向けオプション > RTLレイアウト方向を使用」から変更可能 ○ RTL言語への変更でも可能だが、読めない言語にすると端末が 使いづらくなるため ● クイック設定開発者用タイルから、ショートカットの追加も可

Slide 80

Slide 80 text

クイック設定開発者用タイル 開発者向けオプション > クイック設定開発者用タイル

Slide 81

Slide 81 text

デバイスで確認する ● Resizable Emulator ● 画面サイズ・画面密度 ● Device setting shortcut ● 画面の状態 ● 言語・地域

Slide 82

Slide 82 text

自動テストで確認する

Slide 83

Slide 83 text

UIバリエーションを確認する自動テストの種類 ● UIテスト ● スクリーンショットテスト ● Compose Previewのスクリーンショットテスト

Slide 84

Slide 84 text

UIバリエーションを確認する自動テストの種類 ● UIテスト ● スクリーンショットテスト ● Compose Previewのスクリーンショットテスト テストコード上でUIを表示・操作した後、 UIの要素に対してAssertionをする

Slide 85

Slide 85 text

UIバリエーションを確認する自動テストの種類 ● UIテスト ● スクリーンショットテスト ● Compose Previewのスクリーンショットテスト テストコード上でUIを表示・操作した後、ス クリーンショットを取得する また、コードの変更前と変更後のスクリーン ショット同士を比較してVisual Regression Testをする UIテストとの組み合わせも可

Slide 86

Slide 86 text

UIバリエーションを確認する自動テストの種類 ● UIテスト ● スクリーンショットテスト ● Compose Previewのスクリーンショットテスト スクリーンショットテストの1種 Compose Previewの実装を利用すること でテストコードの実装するコストを 大きく下げることができる

Slide 87

Slide 87 text

UIバリエーションを確認する自動テストの種類 ● UIテスト ● スクリーンショットテスト ● Compose Previewのスクリーンショットテスト 今回は主に2つのテストで利用できる 環境のバリエーションを作成するため のツールを紹介

Slide 88

Slide 88 text

Compose Previewのスクリーンショットテスト 「仕組みから理解する!Composeプレビューを様々なバリエーションで スクリーンショットテストしよう」 https://2024.droidkaigi.jp/timetable/692264/

Slide 89

Slide 89 text

自動テストで確認する ● DeviceConfigurationOverride ● Window Testing Library ● StateRestorationTester ● RobolectricのQualitifier

Slide 90

Slide 90 text

自動テストで確認する ● DeviceConfigurationOverride ● Window Testing Library ● StateRestorationTester ● RobolectricのQualitifier

Slide 91

Slide 91 text

DeviceConfigurationOverride ● Composeを様々なConfigurationでテストできるように環境をエミュ レートしてくれる ○ 元TestHarness library ● Local Test(Robolectric)とInstrumentation Testどちらでも利用可 ● テスト開始前にsetContent済みのActivityを起動する場合は利用不可 ● Compose 1.7.0-alpha03以上

Slide 92

Slide 92 text

DeviceConfigurationOverride DeviceConfigurationOverride.ForcedSize 画面サイズ DeviceConfigurationOverride.FontScale フォントスケール DeviceConfigurationOverride.FontWeightAdjustment フォントの太さ DeviceConfigurationOverride.DarkMode ダークモード DeviceConfigurationOverride.Locales 言語・地域 DeviceConfigurationOverride.LayoutDirection レイアウト方向(LTR or RTL) DeviceConfigurationOverride.WindowInsets ウィンドウインセット

Slide 93

Slide 93 text

Composeの自動テストの雛形 @RunWith(AndroidJUnit4::class) class ComposeUITest { @get:Rule val composeTestRule = createComposeRule() @Test fun testYourCompose() { composeTestRule.setContent { // set your composable function } } }

Slide 94

Slide 94 text

DeviceConfigurationOverride @Test fun testYourCompose() { composeTestRule.setContent { DeviceConfigurationOverride( DeviceConfigurationOverride.ForcedSize( DpSize(480.dp, 320.dp) ) ) { MyCompose() } } }

Slide 95

Slide 95 text

DeviceConfigurationOverride @Test fun testYourCompose() { composeTestRule.setContent { DeviceConfigurationOverride( DeviceConfigurationOverride.ForcedSize( DpSize(480.dp, 320.dp) ) ) { MyCompose() } } } テスト対象のCompose関数を DeviceConfigurationOverrideで囲む そのため、テストコードから setContentできる必要がある

Slide 96

Slide 96 text

DeviceConfigurationOverride DeviceConfigurationOverride( DeviceConfigurationOverride.ForcedSize(DpSize(480.dp, 320.dp)) then DeviceConfigurationOverride.FontScale(2f) then DeviceConfigurationOverride.Locales(LocaleList("ja")) ) { MyCompose() }

Slide 97

Slide 97 text

DeviceConfigurationOverride DeviceConfigurationOverride( DeviceConfigurationOverride.ForcedSize(DpSize(480.dp, 320.dp)) then DeviceConfigurationOverride.FontScale(2f) then DeviceConfigurationOverride.Locales(LocaleList("ja")) ) { MyCompose() } thenで連結可能

Slide 98

Slide 98 text

DeviceConfigurationOverride

Slide 99

Slide 99 text

Window Testing library ● 折りたたみ機能などウインドウ管理に関連する機能のテストをできる ようする ● 任意のFoldingFeatureインスタンスを作成できるAPIがあり、Preview にも使用可能 ● Local TestとInstrumentaion Test両方で使用可

Slide 100

Slide 100 text

Window Testing library ブックモード テーブルトップモード ヒンジ ヒンジ

Slide 101

Slide 101 text

Window Testing library @RunWith(AndroidJUnit4::class) class ComposeUITest { @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @get:Rule(order = 2) val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() .. } 

Slide 102

Slide 102 text

Window Testing library @RunWith(AndroidJUnit4::class) class ComposeUITest { @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @get:Rule(order = 2) val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() .. } 

Slide 103

Slide 103 text

Window Testing library @RunWith(AndroidJUnit4::class) class ComposeUITest { @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @get:Rule(order = 2) val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() .. }  Activityのインスタンスを 利用したいので

Slide 104

Slide 104 text

Window Testing library @RunWith(AndroidJUnit4::class) class ComposeUITest { @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @get:Rule(order = 2) val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() .. } 

Slide 105

Slide 105 text

Window Testing library @Test fun foldingFeature() { composeTestRule.setContent { MainContent() } val foldingFeature = FoldingFeature( activity = composeTestRule.activity, state = HALF_OPENED, orientation = HORIZONTAL, ) val info = TestWindowLayoutInfo(listOf(foldingFeature)) windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(info) composeTestRule.waitForIdle() }

Slide 106

Slide 106 text

Window Testing library @Test fun foldingFeature() { composeTestRule.setContent { MainContent() } val foldingFeature = FoldingFeature( activity = composeTestRule.activity, state = HALF_OPENED, orientation = HORIZONTAL, ) val info = TestWindowLayoutInfo(listOf(foldingFeature)) windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(info) composeTestRule.waitForIdle() } テーブルトップモード

Slide 107

Slide 107 text

Window Testing library @Test fun foldingFeature() { composeTestRule.setContent { MainContent() } val foldingFeature = FoldingFeature( activity = composeTestRule.activity, state = HALF_OPENED, orientation = HORIZONTAL, ) val info = TestWindowLayoutInfo(listOf(foldingFeature)) windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(info) composeTestRule.waitForIdle() } DisplayFeatureの上書き

Slide 108

Slide 108 text

Window Testing library @Test fun foldingFeature() { composeTestRule.setContent { MainContent() } val foldingFeature = FoldingFeature( activity = composeTestRule.activity, state = HALF_OPENED, orientation = HORIZONTAL, ) val info = TestWindowLayoutInfo(listOf(foldingFeature)) windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(info) composeTestRule.waitForIdle() } 再描画終わるまで待機 Instrumentation Testでは必要

Slide 109

Slide 109 text

Window Testing library HALF_OPEND & VERTICAL HALF_OPEND & HORIZONTAL

Slide 110

Slide 110 text

Window Testing library - Previewでの利用 @Preview(device = "spec:width=480dp,height=480dp,dpi=160") @Composable fun PreviewTwoPaneLayoutHorizontal() { TwoPaneLayout( displayFeatures = listOf( FoldingFeature( Rect(0, 240, 480, 240), orientation = FoldingFeature.Orientation.HORIZONTAL, state = FoldingFeature.State.HALF_OPENED, ) ) ) }

Slide 111

Slide 111 text

Window Testing library - Previewでの利用 @Preview(device = "spec:width=480dp,height=480dp,dpi=160") @Composable fun PreviewTwoPaneLayoutHorizontal() { TwoPaneLayout( displayFeatures = listOf( FoldingFeature( Rect(0, 240, 480, 240), orientation = FoldingFeature.Orientation.HORIZONTAL, state = FoldingFeature.State.HALF_OPENED, ) ) ) } 画面サイズが480dpの正方形 計算しやすいようにdpiは160(倍率1.0)

Slide 112

Slide 112 text

Window Testing library - Previewでの利用 @Preview(device = "spec:width=480dp,height=480dp,dpi=160") @Composable fun PreviewTwoPaneLayoutHorizontal() { TwoPaneLayout( displayFeatures = listOf( FoldingFeature( Rect(0, 240, 480, 240), orientation = FoldingFeature.Orientation.HORIZONTAL, state = FoldingFeature.State.HALF_OPENED, ) ) ) } ヒンジの位置が Left: 0, top: 240 right: 480 bottom: 240

Slide 113

Slide 113 text

Window Testing library - Previewでの利用 @Preview(device = "spec:width=480dp,height=480dp,dpi=160") @Composable fun PreviewTwoPaneLayoutHorizontal() { TwoPaneLayout( displayFeatures = listOf( FoldingFeature( Rect(0, 240, 480, 240), orientation = FoldingFeature.Orientation.HORIZONTAL, state = FoldingFeature.State.HALF_OPENED, ) ) ) } テーブルトップモード

Slide 114

Slide 114 text

Window Testing library - Previewでの利用 @Preview(device = "spec:width=480dp,height=480dp,dpi=160") @Composable fun PreviewTwoPaneLayoutHorizontal() { TwoPaneLayout( displayFeatures = listOf( FoldingFeature( Rect(0, 240, 480, 240), orientation = FoldingFeature.Orientation.HORIZONTAL, state = FoldingFeature.State.HALF_OPENED, ) ) ) } テーブルトップモード

Slide 115

Slide 115 text

StateRestorationTester ● Configuration Changeをシミュレートして、Configuration Change時 にUIの状態が復元できるかをテストする ● Activityのrecreateをするよりも高速で安定するらしい ● Local TestとInstrumentaion Test両方で使用可

Slide 116

Slide 116 text

StateRestorationTester @Test fun savedState() { val stateRestorationTester = StateRestorationTester(composeTestRule) stateRestorationTester.setContent { Counter() } composeTestRule.onNodeWithText("increment").performClick() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") stateRestorationTester.emulateSavedInstanceStateRestore() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") }

Slide 117

Slide 117 text

StateRestorationTester @Test fun savedState() { val stateRestorationTester = StateRestorationTester(composeTestRule) stateRestorationTester.setContent { Counter() } composeTestRule.onNodeWithText("increment").performClick() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") stateRestorationTester.emulateSavedInstanceStateRestore() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") }

Slide 118

Slide 118 text

StateRestorationTester @Test fun savedState() { val stateRestorationTester = StateRestorationTester(composeTestRule) stateRestorationTester.setContent { Counter() } composeTestRule.onNodeWithText("increment").performClick() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") stateRestorationTester.emulateSavedInstanceStateRestore() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") } StateRestorationTesterの インスタンスを作成して setContentする

Slide 119

Slide 119 text

StateRestorationTester @Test fun savedState() { val stateRestorationTester = StateRestorationTester(composeTestRule) stateRestorationTester.setContent { Counter() } composeTestRule.onNodeWithText("increment").performClick() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") stateRestorationTester.emulateSavedInstanceStateRestore() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") } Composeに対して操作し 初期状態から変更

Slide 120

Slide 120 text

StateRestorationTester @Test fun savedState() { val stateRestorationTester = StateRestorationTester(composeTestRule) stateRestorationTester.setContent { Counter() } composeTestRule.onNodeWithText("increment").performClick() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") stateRestorationTester.emulateSavedInstanceStateRestore() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") } Configuration Changeの エミュレート

Slide 121

Slide 121 text

StateRestorationTester @Test fun savedState() { val stateRestorationTester = StateRestorationTester(composeTestRule) stateRestorationTester.setContent { Counter() } composeTestRule.onNodeWithText("increment").performClick() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") stateRestorationTester.emulateSavedInstanceStateRestore() composeTestRule.onNodeWithTag("counter").assertTextEquals("1") } 状態が復元できているかを確認する 例えば値をrememberしていると テストが失敗する

Slide 122

Slide 122 text

おすすめのドキュメント Libraries and tools to test different screen sizes https://developer.android.com/training/testing/different-screens/ tools

Slide 123

Slide 123 text

自動テストで確認する ● DeviceConfigurationOverride ● Window Testing Library ● StateRestorationTester

Slide 124

Slide 124 text

まとめ

Slide 125

Slide 125 text

今日話したこと ● 環境のバリエーションを整理する ● Compose Previewで確認する ● デバイスで確認する ● 自動テストで確認する

Slide 126

Slide 126 text

参考 ● App resources overview ○ https://developer.android.com/guide/topics/resources/providing-resources.html ● Support different pixel densities ○ https://developer.android.com/training/multiscreen/screendensities ● Distribution Dashboard ○ https://developer.android.com/about/dashboards/index.html ● Window size classes ○ https://developer.android.com/develop/ui/compose/layouts/adaptive/window-size-cla sses ● ユーザー補助検証ツール ○ https://support.google.com/accessibility/android/faq/6376582 ● Localize your app ○ https://developer.android.com/guide/topics/resources/localization#emulator ● Io.appium.settings ○ https://github.com/appium/io.appium.settings ● Libraries and tools to test different screen sizes ○ https://developer.android.com/training/testing/different-screens/tools

Slide 127

Slide 127 text

ご清聴ありがとうございました!