Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AndroidアプリのUIバリエーションをあの手この手で確認する / Check UI var...

tkmnzm
September 13, 2024

AndroidアプリのUIバリエーションをあの手この手で確認する / Check UI variations of Android apps by various means

DroidKaigi 2024
「AndroidアプリのUIバリエーションをあの手この手で確認する 」の発表スライドです。

スライドに出てくるドキュメントのリンク
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-classes
ユーザー補助検証ツール: 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

tkmnzm

September 13, 2024
Tweet

More Decks by tkmnzm

Other Decks in Programming

Transcript

  1. 画面サイズ 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
  2. 画面密度 ldpi(低密度) 〜120dpi mdpi(中密度) 〜160dpi hdpi(高密度) 〜240dpi xhdpi(超高密度) 〜320dpi xxhdpi(超超高密度)

    〜480dpi xxxhdpi(超超超高密度) 〜680dpi https://developer.android.com/training/multiscreen/scr eendensities
  3. 画面サイズと画面密度のマトリクス 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
  4. 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等)も忘れずに
  5. ウィンドウサイズクラス 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
  6. 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
  7. 画面サイズ・画面密度 @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, )
  8. プリセットのデバイスを利用する 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
  9. リファレンスデバイスを利用する 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" .. }
  10. リファレンスデバイスを利用する 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" .. } 様々なデバイスに対応したアプリを開発するのを サポートするために定義されたデバイスのセット
  11. リファレンスデバイスを利用する @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
  12. リファレンスデバイスを利用する @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をまとめた マルチプレビューテンプレート
  13. カスタムのデバイスを作成する 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" .. } リファレンスデバイスに追加して 画面サイズが小さい端末も欲しいなあ
  14. フォントスケール @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, )
  15. フォントスケール @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
  16. フォントスケール @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のパターンを まとめたマルチプレビューテンプレート
  17. 言語・地域 @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, )
  18. 言語・地域 @Preview( device = "id:pixel_4", locale = "en" ) @Preview(

    device = "id:pixel_4", locale = "ja" ) @Preview( device = "id:pixel_4", locale = "ar" ) @Composable
  19. @Preview( device = "id:pixel_4", locale = "en" ) @Preview( device

    = "id:pixel_4", locale = "ja" ) @Preview( device = "id:pixel_4", locale = "ar" ) @Composable 言語・地域 RTLレイアウトを確認したい場合は RTL言語を指定する
  20. UI Check Mode • Preview右上「Start UI Check Mode」から起動 • マルチプレビューテンプレートのPreview全種類を自動で作成

    ◦ PreviewScreenSizes・PreviewFontScale・PreviewLightDark・Previ ewDynamicColors • 作成したPreviewに対してユーザー補助検証ツールとVisual Lintを実 行した結果を教えてくれる
  21. UI Check Mode TextField( modifier = Modifier.fillMaxWidth(), value = value,

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

    placeholder = { Text("Input you name") }, ) Button(modifier = Modifier .fillMaxWidth()) { Text(text = "Button") }
  23. Visual Linting • ドキュメントはないが、VisualLintServiceのソースコードから チェック項目を確認できる ◦ Android Code Search >

    VisualLintService.kt • さきほどの例だとButtonSizeAnalyzerで引っかかった ◦ 実装されているチェック項目の数は多くはないが、Material Designの推奨にそっているかを見ているものが半分ほど
  24. 画面サイズ・画面密度の変更 // 現在のサイズを取得する $ adb shell wm size // サイズを変更する(dp指定)

    $ adb shell wm size 320dpx480dp // サイズを変更する(ピクセル指定) $ adb shell wm size 840x1260 // 設定をもとに戻す $ adb shell wm size reset
  25. 画面サイズ・画面密度の変更 $ adb shell wm size -> Physical size: 1080x2400

    $ adb shell wm size -> Physical density: 420 $ adb shell wm size 320dpx480dp
  26. adbコマンドでの言語変更 $ adb root $ adb shell // 言語タグを指定 $

    setprop persist.sys.locale ja-JP;stop;sleep 5;start https://developer.android.com/guide/topics/resources/localization #emulator
  27. 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
  28. DeviceConfigurationOverride • Composeを様々なConfigurationでテストできるように環境をエミュ レートしてくれる ◦ 元TestHarness library • Local Test(Robolectric)とInstrumentation

    Testどちらでも利用可 • テスト開始前にsetContent済みのActivityを起動する場合は利用不可 • Compose 1.7.0-alpha03以上
  29. Composeの自動テストの雛形 @RunWith(AndroidJUnit4::class) class ComposeUITest { @get:Rule val composeTestRule = createComposeRule()

    @Test fun testYourCompose() { composeTestRule.setContent { // set your composable function } } }
  30. DeviceConfigurationOverride @Test fun testYourCompose() { composeTestRule.setContent { DeviceConfigurationOverride( DeviceConfigurationOverride.ForcedSize( DpSize(480.dp,

    320.dp) ) ) { MyCompose() } } } テスト対象のCompose関数を DeviceConfigurationOverrideで囲む そのため、テストコードから setContentできる必要がある
  31. Window Testing library @RunWith(AndroidJUnit4::class) class ComposeUITest { @get:Rule(order = 1)

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

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

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

    val composeTestRule = createAndroidComposeRule<ComponentActivity>() @get:Rule(order = 2) val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() .. } 
  35. 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() }
  36. 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() } テーブルトップモード
  37. 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の上書き
  38. 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では必要
  39. 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, ) ) ) }
  40. 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)
  41. 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
  42. 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, ) ) ) } テーブルトップモード
  43. 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, ) ) ) } テーブルトップモード
  44. 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") }
  45. 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") }
  46. 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する
  47. 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に対して操作し 初期状態から変更
  48. 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の エミュレート
  49. 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していると テストが失敗する
  50. 参考 • 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