Изменение размера 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-студию
  • Выберите «Импортировать проект» или «Файл->Создать->Импортировать проект».
  • Перейдите туда, где вы клонировали или извлекли проект.
  • Откройте папку изменения размера .
  • Откройте проект в стартовой папке. Он содержит стартовый код.

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

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

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

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

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

Если вы посмотрите файл 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")
}

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

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

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

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, показывающий методы жизненного цикла активности, вызываемые при изменении размера

Теперь посмотрите на 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 помогает сохранять состояние при рекомпозиции, но не при обновлении активности или процесса. Обычно используется подъем состояния , перемещение состояния к вызывающей стороне составного объекта, чтобы сделать составные объекты без состояния, что позволяет полностью избежать этой проблемы. С учетом вышесказанного вы можете использовать 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-файл репозитория и распакуйте его