Держатели государства и государство по безработице

В руководстве по уровню пользовательского интерфейса рассматривается однонаправленный поток данных (UDF) как средство создания и управления состоянием пользовательского интерфейса для уровня пользовательского интерфейса.

Данные передаются однонаправленно от уровня данных к пользовательскому интерфейсу.
Рисунок 1. Однонаправленный поток данных.

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

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

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

Элементы производственного конвейера состояния пользовательского интерфейса

Состояние пользовательского интерфейса и логика, которая его создает, определяют уровень пользовательского интерфейса.

состояние пользовательского интерфейса

Состояние пользовательского интерфейса — это свойство, описывающее пользовательский интерфейс. Существует два типа состояния пользовательского интерфейса:

  • Состояние пользовательского интерфейса экрана — это то, что вам нужно отобразить на экране. Например, класс NewsUiState может содержать новостные статьи и другую информацию, необходимую для отображения пользовательского интерфейса. Это состояние обычно связано с другими уровнями иерархии, поскольку оно содержит данные приложения.
  • Состояние элемента пользовательского интерфейса относится к свойствам, присущим элементам пользовательского интерфейса, которые влияют на то, как они отображаются. Элемент пользовательского интерфейса может быть показан или скрыт и может иметь определенный шрифт, размер шрифта или цвет шрифта. В представлениях Android представление само управляет этим состоянием, поскольку оно по своей сути имеет состояние, предоставляя методы для изменения или запроса своего состояния. Примером этого являются методы get и set класса TextView для его текста. В Jetpack Compose состояние является внешним по отношению к составному объекту, и вы даже можете поднять его из непосредственной близости от составного объекта в вызывающую составную функцию или в держатель состояния. Примером этого является ScaffoldState для компонуемого Scaffold .

Логика

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

Логика создает состояние пользовательского интерфейса
Рисунок 2. Логика как производитель состояния пользовательского интерфейса.

Логика в приложении может быть либо бизнес-логикой, либо логикой пользовательского интерфейса:

  • Бизнес-логика — это реализация требований продукта к данным приложения. Например, добавление статьи в закладки в приложении для чтения новостей, когда пользователь нажимает кнопку. Эта логика сохранения закладки в файл или базу данных обычно размещается на уровнях домена или данных. Владелец состояния обычно делегирует эту логику этим уровням, вызывая методы, которые они предоставляют.
  • Логика пользовательского интерфейса связана с тем , как отображать состояние пользовательского интерфейса на экране. Например, получение правильной подсказки панели поиска, когда пользователь выбирает категорию, прокрутки к определенному элементу в списке или логики навигации к определенному экрану, когда пользователь нажимает кнопку.

Жизненный цикл Android, типы состояния и логики пользовательского интерфейса

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

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

Вышеизложенное можно обобщить с помощью таблицы ниже:

Независимость от жизненного цикла пользовательского интерфейса Зависит от жизненного цикла пользовательского интерфейса
Бизнес-логика Логика пользовательского интерфейса
Состояние пользовательского интерфейса экрана

Конвейер производства состояний пользовательского интерфейса

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

То есть допустимы следующие изменения конвейера уровня пользовательского интерфейса:

  • Состояние пользовательского интерфейса, создаваемое и управляемое самим пользовательским интерфейсом. Например, простой многоразовый базовый счетчик:

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • Логика пользовательского интерфейса → Пользовательский интерфейс. Например, отображение или скрытие кнопки, позволяющей пользователю перейти к началу списка.

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • Бизнес-логика → Пользовательский интерфейс. Элемент пользовательского интерфейса, отображающий фотографию текущего пользователя на экране.

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • Бизнес-логика → Логика пользовательского интерфейса → Пользовательский интерфейс. Элемент пользовательского интерфейса, который прокручивается для отображения на экране нужной информации для данного состояния пользовательского интерфейса.

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

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

Данные передаются с уровня создания данных в пользовательский интерфейс.
Рисунок 3. Применение логики на уровне пользовательского интерфейса.

Государственные держатели и их обязанности

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

Это дает следующие преимущества:

  • Простые пользовательские интерфейсы . Пользовательский интерфейс просто связывает свое состояние.
  • Удобство сопровождения : логику, определенную в держателе состояния, можно повторять без изменения самого пользовательского интерфейса.
  • Тестируемость : пользовательский интерфейс и его логика создания состояний могут быть протестированы независимо.
  • Читабельность : читатели кода могут четко видеть различия между кодом представления пользовательского интерфейса и кодом создания состояния пользовательского интерфейса.

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

Типы держателей государства

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

  • Держатель состояния бизнес-логики.
  • Держатель состояния логики пользовательского интерфейса.

В следующих разделах более подробно рассматриваются типы держателей состояний, начиная с держателей состояний бизнес-логики.

Бизнес-логика и ее государственный держатель

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

Свойство Деталь
Создает состояние пользовательского интерфейса Владельцы состояний бизнес-логики несут ответственность за создание состояния пользовательского интерфейса для своих пользовательских интерфейсов. Это состояние пользовательского интерфейса часто является результатом обработки пользовательских событий и чтения данных из уровней домена и данных.
Сохраняется за счет активного отдыха Владельцы состояний бизнес-логики сохраняют свои состояния и конвейеры обработки состояний во время воссоздания Activity , помогая обеспечить бесперебойную работу пользователей. В тех случаях, когда держателя состояния невозможно сохранить и он создается заново (обычно после смерти процесса ), держатель состояния должен иметь возможность легко воссоздать свое последнее состояние, чтобы обеспечить единообразие взаимодействия с пользователем.
Обладать долгоживущим государством Держатели состояний бизнес-логики часто используются для управления состоянием пунктов назначения навигации. В результате они часто сохраняют свое состояние при изменении навигации до тех пор, пока не будут удалены из графа навигации.
Является уникальным для своего пользовательского интерфейса и не подлежит повторному использованию. Держатели состояний бизнес-логики обычно создают состояние для определенной функции приложения, например TaskEditViewModel или TaskListViewModel , и поэтому применимы только к этой функции приложения. Один и тот же государственный владелец может поддерживать эти функции приложения в разных форм-факторах. Например, версии приложения для мобильных устройств, телевизоров и планшетов могут повторно использовать один и тот же держатель состояния бизнес-логики.

Например, рассмотрим пункт назначения навигации автора в приложении «Сейчас в Android »:

Приложение «Сейчас в Android» демонстрирует, как пункт назначения навигации, представляющий основную функцию приложения, должен иметь свой собственный уникальный держатель состояния бизнес-логики.
Рис. 4. Приложение «Сейчас в Android»

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

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = 

    // Business logic
    fun followAuthor(followed: Boolean) {
      
    }
}

Обратите внимание, что AuthorViewModel имеет атрибуты, описанные ранее:

Свойство Деталь
Производит AuthorScreenUiState AuthorViewModel считывает данные из AuthorsRepository и NewsRepository и использует эти данные для создания AuthorScreenUiState . Он также применяет бизнес-логику, когда пользователь хочет подписаться на Author или отказаться от него, делегировав его AuthorsRepository .
Имеет доступ к уровню данных Экземпляры AuthorsRepository и NewsRepository передаются ему в его конструкторе, что позволяет реализовать бизнес-логику отслеживания Author .
Выживает Activity отдых Поскольку он реализован с помощью ViewModel , он будет сохранен при быстром воссоздании Activity . В случае смерти процесса объект SavedStateHandle можно прочитать, чтобы предоставить минимальный объем информации, необходимый для восстановления состояния пользовательского интерфейса с уровня данных.
Обладает долгоживущим государством ViewModel привязан к навигационному графу, поэтому, если пункт назначения автора не удален из навигационного графа, состояние пользовательского интерфейса в uiState StateFlow остается в памяти. Использование StateFlow также дает преимущество, заключающееся в том, что приложение бизнес-логики, создающей состояние, становится ленивым, поскольку состояние создается только в том случае, если есть сборщик состояния пользовательского интерфейса.
Уникально для своего пользовательского интерфейса AuthorViewModel применим только к месту назначения навигации автора и не может быть повторно использован где-либо еще. Если существует какая-либо бизнес-логика, которая повторно используется в разных пунктах назначения навигации, эта бизнес-логика должна быть инкапсулирована в компонент уровня данных или уровня домена.

ViewModel как держатель состояния бизнес-логики

Преимущества ViewModels в разработке Android делают их пригодными для предоставления доступа к бизнес-логике и подготовки данных приложения для представления на экране. Эти преимущества включают следующее:

  • Операции, запускаемые ViewModels, сохраняются при изменении конфигурации.
  • Интеграция с навигацией :
    • Навигация кэширует ViewModels, пока экран находится в заднем стеке. Это важно, чтобы ранее загруженные данные были мгновенно доступны по возвращении в пункт назначения. Это сложнее сделать с держателем состояния, который следует жизненному циклу составного экрана.
    • ViewModel также очищается, когда место назначения извлекается из заднего стека, гарантируя автоматическую очистку вашего состояния. Это отличается от прослушивания составного удаления, которое может произойти по нескольким причинам, например переход на новый экран, изменение конфигурации или другие причины.
  • Интеграция с другими библиотеками Jetpack, такими как Hilt .

Логика пользовательского интерфейса и ее держатель состояния

Логика пользовательского интерфейса — это логика, которая работает с данными, которые предоставляет сам пользовательский интерфейс. Это может быть состояние элементов пользовательского интерфейса или источники данных пользовательского интерфейса, такие как API разрешений или Resources . Держатели состояний, использующие логику пользовательского интерфейса, обычно имеют следующие свойства:

  • Создает состояние пользовательского интерфейса и управляет состоянием элементов пользовательского интерфейса .
  • Не сохраняется воссоздание Activity : держатели состояний, размещенные в логике пользовательского интерфейса, часто зависят от источников данных из самого пользовательского интерфейса, и попытка сохранить эту информацию при изменении конфигурации чаще всего приводит к утечке памяти. Если держателям состояний необходимо, чтобы данные сохранялись при изменении конфигурации, им необходимо делегировать их другому компоненту, лучше подходящему для выживания при воссоздании Activity . Например, в Jetpack Compose состояния элементов Composable пользовательского интерфейса, созданные с помощью remembered функций, часто делегируют rememberSaveable для сохранения состояния во время воссоздания Activity . Примеры таких функций включают rememberScaffoldState() и rememberLazyListState() .
  • Имеет ссылки на источники данных в области пользовательского интерфейса . На источники данных, такие как API жизненного цикла и ресурсы, можно безопасно ссылаться и читать, поскольку держатель логического состояния пользовательского интерфейса имеет тот же жизненный цикл, что и пользовательский интерфейс.
  • Можно повторно использовать в нескольких пользовательских интерфейсах . Различные экземпляры одного и того же держателя логического состояния пользовательского интерфейса могут повторно использоваться в разных частях приложения. Например, держатель состояния для управления событиями пользовательского ввода для группы чипов может использоваться на странице поиска чипов фильтра, а также для поля «кому» для получателей электронной почты.

Держатель состояния логики пользовательского интерфейса обычно реализуется с помощью простого класса. Это связано с тем, что сам пользовательский интерфейс отвечает за создание держателя логического состояния пользовательского интерфейса, а держатель логического состояния пользовательского интерфейса имеет тот же жизненный цикл, что и сам пользовательский интерфейс. Например, в Jetpack Compose держатель состояния является частью композиции и следует за ее жизненным циклом.

Это можно проиллюстрировать на следующем примере из примера Now in Android :

Теперь в Android используется простой держатель состояния класса для управления логикой пользовательского интерфейса.
Рис. 5. Пример приложения Now in Android.

В примере «Сейчас в Android» отображается либо нижняя панель приложения, либо навигационная панель для его навигации, в зависимости от размера экрана устройства. На экранах меньшего размера используется нижняя панель приложения, а на экранах большего размера — панель навигации.

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

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

В приведенном выше примере примечательны следующие детали, касающиеся NiaAppState :

  • Не сохраняется при воссоздании Activity : NiaAppState remembered в композиции путем создания ее с помощью составной функции rememberNiaAppState в соответствии с соглашениями об именах Compose. После воссоздания Activity предыдущий экземпляр теряется и создается новый экземпляр со всеми переданными зависимостями, соответствующий новой конфигурации воссозданного Activity . Эти зависимости могут быть новыми или восстановленными из предыдущей конфигурации. Например, rememberNavController() используется в конструкторе NiaAppState и делегирует функцию rememberSaveable для сохранения состояния при воссоздании Activity .
  • Имеет ссылки на источники данных в области пользовательского интерфейса : ссылки на navigationController , Resources и другие подобные типы с областью жизненного цикла можно безопасно хранить в NiaAppState , поскольку они имеют одну и ту же область жизненного цикла.

Выбирайте между ViewModel и простым классом для держателя состояния.

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

Вкратце, на диаграмме ниже показано положение держателей состояний в производственном конвейере состояний пользовательского интерфейса:

Данные передаются с уровня создания данных на уровень пользовательского интерфейса.
Рисунок 6 : Держатели состояний в конвейере производства состояний UI. Стрелки означают поток данных.

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

Государственные держатели могут быть объединены

Держатели состояний могут зависеть от других держателей состояний, если зависимости имеют равный или более короткий срок действия. Примеры этого:

  • держатель логического состояния пользовательского интерфейса может зависеть от другого держателя логического состояния пользовательского интерфейса.
  • держатель состояния уровня экрана может зависеть от держателя логического состояния пользовательского интерфейса.

В следующем фрагменте кода показано, как DrawerState Compose зависит от другого внутреннего держателя состояния, SwipeableState , и как держатель логического состояния пользовательского интерфейса приложения может зависеть от DrawerState :

@Stable
class DrawerState(/* ... */) {
  internal val swipeableState = SwipeableState(/* ... */)
  // ...
}

@Stable
class MyAppState(
  private val drawerState: DrawerState,
  private val navController: NavHostController
) { /* ... */ }

@Composable
fun rememberMyAppState(
  drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
  navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
  MyAppState(drawerState, navController)
}

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

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

class MyScreenViewModel(/* ... */) {
  val uiState: StateFlow<MyScreenUiState> = /* ... */
  fun doSomething() { /* ... */ }
  fun doAnotherThing() { /* ... */ }
  // ...
}

@Stable
class MyScreenState(
  // DO NOT pass a ViewModel instance to a plain state holder class
  // private val viewModel: MyScreenViewModel,

  // Instead, pass only what it needs as a dependency
  private val someState: StateFlow<SomeState>,
  private val doSomething: () -> Unit,

  // Other UI-scoped types
  private val scaffoldState: ScaffoldState
) {
  /* ... */
}

@Composable
fun rememberMyScreenState(
  someState: StateFlow<SomeState>,
  doSomething: () -> Unit,
  scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
  MyScreenState(someState, doSomething, scaffoldState)
}

@Composable
fun MyScreen(
  modifier: Modifier = Modifier,
  viewModel: MyScreenViewModel = viewModel(),
  state: MyScreenState = rememberMyScreenState(
    someState = viewModel.uiState.map { it.toSomeState() },
    doSomething = viewModel::doSomething
  ),
  // ...
) {
  /* ... */
}

На следующей диаграмме представлены зависимости между пользовательским интерфейсом и различными держателями состояний предыдущего фрагмента кода:

Пользовательский интерфейс зависит как от держателя состояния логики пользовательского интерфейса, так и от держателя состояния уровня экрана.
Рис. 7. Пользовательский интерфейс в зависимости от держателей различных состояний. Стрелки означают зависимости.

Образцы

Следующие примеры Google демонстрируют использование держателей состояний на уровне пользовательского интерфейса. Изучите их, чтобы увидеть это руководство на практике:

{% дословно %} {% дословно %} {% дословно %} {% дословно %}