Изменение размера Android-приложения

1. Введение

Экосистема устройств Android постоянно развивается. От первых встроенных аппаратных клавиатур до современного ландшафта раскладных, складных устройств, планшетов и окон со свободным изменением размера — приложения Android никогда не работали на таком разнообразном наборе устройств, как сегодня.

Хотя это отличная новость для разработчиков, для соответствия ожиданиям пользователей и обеспечения превосходного пользовательского опыта на экранах разных размеров требуется определенная оптимизация приложения. Вместо того чтобы ориентироваться на каждое новое устройство по отдельности, адаптивный пользовательский интерфейс и надежная архитектура помогут вашему приложению отлично выглядеть и работать везде, где находятся ваши нынешние и будущие пользователи — на устройствах любого размера и формы!

Внедрение свободно изменяемых по размеру сред Android — отличный способ проверить адаптивный пользовательский интерфейс на прочность и подготовить его к работе на любом устройстве. В этом практическом занятии вы поймете последствия изменения размера, а также освоите лучшие практики для обеспечения надежного и простого изменения размера приложения.

Что вы построите

Вы изучите последствия произвольного изменения размера элементов и оптимизируете Android-приложение, чтобы продемонстрировать лучшие практики изменения размера. Ваше приложение будет:

Необходимо иметь совместимый манифест.

  • Удалите ограничения, которые мешают приложению свободно изменять размер экрана.

Сохранять состояние при изменении размера

  • Сохраняет состояние пользовательского интерфейса при изменении размера с помощью функции rememberSaveable.
  • Избегайте ненужного дублирования фоновой работы по инициализации пользовательского интерфейса.

Что вам понадобится

  1. Знание процесса создания базовых приложений для Android.
  2. Знание ViewModel и State в Compose.
  3. Тестовое устройство, поддерживающее изменение размера окон в произвольном порядке, например, одно из следующих:

Если во время выполнения этого практического задания вы столкнетесь с какими-либо проблемами (ошибками в коде, грамматическими неточностями, неясными формулировками и т. д.), пожалуйста, сообщите о них, используя ссылку «Сообщить об ошибке» в левом нижнем углу задания.

2. Начало работы

Клонируйте репозиторий с GitHub .

git clone https://github.com/android/large-screen-codelabs/

...или скачайте ZIP-архив репозитория и распакуйте его.

Импортный проект

  • Откройте Android Studio
  • Выберите «Импорт проекта» или «Файл» -> «Создать» -> «Импорт проекта».
  • Перейдите в папку, куда вы клонировали или распаковали проект.
  • Откройте папку, изменяющую размер .
  • Откройте проект в папке "Start" . Там находится стартовый код.

Попробуйте приложение

  • Соберите и запустите приложение.
  • Попробуйте изменить размер приложения.

Что вы думаете?

В зависимости от совместимости вашего тестового устройства, вы, вероятно, заметили, что пользовательский опыт не идеален. Приложение не может изменить размер и застревает в исходном соотношении сторон. Что происходит?

Манифестные ограничения

Если вы посмотрите файл AndroidManifest.xml приложения, вы увидите, что в него добавлены несколько ограничений, которые мешают нашему приложению корректно работать в среде со свободным изменением размера окна.

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

Попробуйте удалить эти три проблемные строки из манифеста, пересоберите приложение и попробуйте снова на тестовом устройстве. Вы заметите, что приложение больше не ограничено в возможности произвольного изменения размера окна. Удаление подобных ограничений из манифеста — важный шаг в оптимизации приложения для произвольного изменения размера окна.

3. Изменение конфигурации при изменении размера.

При изменении размера окна вашего приложения обновляются его параметры конфигурации . Эти обновления влияют на работу приложения. Понимание и прогнозирование этих изменений поможет обеспечить пользователям отличный опыт. Наиболее очевидные изменения касаются ширины и высоты окна приложения, но они также влияют на соотношение сторон и ориентацию.

Отслеживание изменений конфигурации

Чтобы увидеть эти изменения самостоятельно в приложении, созданном с использованием системы представлений Android, вы можете переопределить View.onConfigurationChanged . В Jetpack Compose у нас есть доступ к LocalConfiguration.current , который автоматически обновляется при каждом вызове View.onConfigurationChanged .

Чтобы увидеть эти изменения конфигурации в вашем тестовом приложении, добавьте в приложение компонент, отображающий значения из LocalConfiguration.current , или создайте новый тестовый проект с таким компонентом. Пример пользовательского интерфейса для просмотра этих изменений может выглядеть примерно так:

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

Пример реализации можно найти в папке проекта observation-configuration-changes . Попробуйте добавить его в пользовательский интерфейс вашего приложения, запустите на тестовом устройстве и понаблюдайте за обновлением интерфейса по мере изменения конфигурации приложения.

При изменении размера приложения информация об изменениях конфигурации отображается в интерфейсе приложения в режиме реального времени.

Эти изменения в конфигурации вашего приложения позволяют имитировать быстрый переход от крайних вариантов, ожидаемых при использовании разделенного экрана на небольшом мобильном устройстве, к полноэкранному режиму на планшете или настольном компьютере. Это не только хороший способ проверить компоновку вашего приложения на разных экранах, но и позволяет оценить, насколько хорошо приложение справляется с быстрыми изменениями конфигурации.

4. Журналирование событий жизненного цикла активности

Еще одно последствие изменения размера окна в произвольном порядке для вашего приложения — это различные изменения жизненного цикла Activity , которые будут происходить в вашем приложении. Чтобы увидеть эти изменения в реальном времени, добавьте наблюдатель жизненного цикла в ваш метод onCreate и регистрируйте каждое новое событие жизненного цикла, переопределив onStateChanged .

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

После включения этой функции логирования запустите приложение на тестовом устройстве еще раз и посмотрите на logcat , когда будете пытаться свернуть приложение и снова вывести его на передний план.

Обратите внимание, что ваше приложение приостанавливается при сворачивании, а затем возобновляет работу при выводе на передний план. Это имеет последствия для вашего приложения, которые вы изучите в следующем разделе этого практического занятия, посвященном обеспечению непрерывности работы.

В логах отображается информация о методах жизненного цикла активности, вызываемых при изменении размера окна.

Теперь посмотрите в Logcat, какие коллбэки жизненного цикла активности вызываются при изменении размера приложения от минимального до максимально возможного.

В зависимости от используемого тестового устройства, вы можете наблюдать различное поведение, но, вероятно, вы заметили, что ваша активность уничтожается и воссоздается при значительном изменении размера окна приложения, но не при незначительном. Это связано с тем, что в API 24 и выше только значительные изменения размера приводят к воссозданию Activity .

Вы уже ознакомились с некоторыми распространенными изменениями конфигурации, которые можно ожидать в среде со свободным выбором окон, но есть и другие изменения, о которых следует помнить. Например, если к вашему тестовому устройству подключен внешний монитор , вы можете увидеть, что ваша Activity уничтожается и создается заново с учетом изменений конфигурации, таких как плотность пикселей.

Чтобы упростить процесс внесения изменений в конфигурацию, используйте API более высокого уровня, такие как WindowSizeClass , для реализации адаптивного пользовательского интерфейса. (См. также Поддержка различных размеров экрана .)

5. Непрерывность — сохранение внутреннего состояния компонуемых элементов при изменении их размера.

В предыдущем разделе вы рассмотрели некоторые изменения конфигурации, которые могут произойти в вашем приложении при свободном изменении размера окна. В этом разделе вы будете поддерживать непрерывность состояния пользовательского интерфейса вашего приложения на протяжении всех этих изменений.

Для начала сделайте так, чтобы функция NavigationDrawerHeader (находящаяся в ReplyHomeScreen.kt ) разворачивалась и отображала адрес электронной почты при нажатии.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

После добавления расширяемого заголовка в ваше приложение,

  1. Запустите приложение на своем тестовом устройстве.
  2. Нажмите на заголовок, чтобы развернуть его.
  3. Попробуйте изменить размер окна.

Вы заметите, что заголовок теряет своё состояние при значительном изменении его размера.

При нажатии на заголовок в боковой панели навигации приложения он разворачивается, но сворачивается после изменения размера приложения.

Состояние пользовательского интерфейса теряется из-за того, что remember помогает сохранять состояние при перекомпозициях, но не при перезапуске активности или процесса. Часто используется поднятие состояния (state hoisting) , когда состояние передается вызывающей стороне компонуемого объекта, чтобы сделать компонуемые объекты без состояния, что позволяет полностью избежать этой проблемы. Тем не менее, remember можно использовать в тех случаях, когда состояние элементов пользовательского интерфейса сохраняется внутри функций компонуемого объекта.

Чтобы решить эти проблемы, замените remember на rememberSaveable . Это работает, потому что rememberSaveable сохраняет и восстанавливает запомненное значение в savedInstanceState . Замените remember на rememberSaveable , запустите приложение на тестовом устройстве и попробуйте снова изменить размер приложения. Вы заметите, что состояние расширяемого заголовка сохраняется при изменении размера, как и должно быть.

6. Избегать ненужного дублирования подготовительной работы.

Вы уже видели, как с помощью rememberSaveable можно сохранять внутреннее состояние пользовательского интерфейса составных элементов при изменениях конфигурации, которые могут происходить часто в результате произвольного изменения размера окна. Однако приложение часто должно переносить состояние и логику пользовательского интерфейса из составных элементов . Перенос владения состоянием в ViewModel — один из лучших способов сохранить состояние при изменении размера. При переносе состояния в ViewModel могут возникнуть проблемы с длительными фоновыми процессами, такими как интенсивный доступ к файловой системе или сетевые вызовы, необходимые для инициализации экрана.

Чтобы увидеть пример возможных проблем, добавьте оператор логирования в метод initializeUIState в ReplyViewModel .

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

Теперь запустите приложение на тестовом устройстве и попробуйте несколько раз изменить размер окна приложения.

Если вы посмотрите в Logcat, то заметите, что ваше приложение показывает, что метод инициализации выполнялся несколько раз. Это может стать проблемой для задач, которые вы хотите выполнить только один раз для инициализации пользовательского интерфейса. Дополнительные сетевые вызовы, операции ввода-вывода файлов или другая работа могут снизить производительность устройства и вызвать другие непредвиденные проблемы.

Чтобы избежать ненужной фоновой работы, удалите вызов метода initializeUIState() из метода onCreate() вашей активности. Вместо этого инициализируйте данные в методе init объекта ViewModel . Это гарантирует, что метод инициализации будет выполнен только один раз, при первом создании экземпляра ReplyViewModel :

init {
    initializeUIState()
}

Попробуйте запустить приложение ещё раз, и вы увидите, что ненужная задача имитированной инициализации выполняется только один раз, независимо от того, сколько раз вы изменяете размер окна приложения. Это происходит потому, что ViewModels сохраняются после завершения жизненного цикла Activity . Выполняя код инициализации только один раз при создании ViewModel , мы отделяем его от любых повторных запусков Activity и предотвращаем ненужную работу. Если бы это был действительно дорогостоящий серверный вызов или ресурсоемкая операция ввода-вывода файлов для инициализации пользовательского интерфейса, вы бы значительно сэкономили ресурсы и улучшили пользовательский опыт.

7. ПОЗДРАВЛЯЕМ!

Вы это сделали! Отличная работа! Теперь вы внедрили ряд передовых методов, позволяющих приложениям Android корректно изменять размер на ChromeOS и в других многооконных средах с несколькими экранами.

Пример исходного кода

Клонируйте репозиторий с GitHub.

git clone https://github.com/android/large-screen-codelabs/

...или скачайте ZIP-архив репозитория и распакуйте его.