Привет! Меня зовут Костя, я Android-разработчик в онлайн-кинотеатре PREMIER. Среди прочего у нас есть приложение для Android TV. Работая над ним, я столкнулся с тем, что система фокусов в Jetpack Compose устроена сложнее, чем кажется на первый взгляд. Поскольку для ТВ-приложений и устройств без сенсорного управления работа с фокусом играет ключевую роль в навигации, мне понадобилось детально разобраться в теме. Результатами делюсь в статье.

Документаций по работе с фокусом в Compose в открытом доступе немного, а специфичных сценариев ещё меньше. Поэтому я решил собрать всё, что узнал и применил на практике, в серию статей, которые помогут вам глубже понять внутренние механизмы, научиться эффективно управлять фокусом и избежать распространённых ошибок.
В этой статье разберём основы: что такое фокус, как он работает, какие модификаторы и инструменты предоставляет Jetpack Compose, а также как использовать их для построения удобных интерфейсов. В следующей — углубимся в технические детали. Давайте начнём! 🚀
⚠️ Важно: Статья написана для Compose версии 1.8, но все инструменты и практики актуальны и для более ранних версий.
Содержание
Основы работы с фокусом в Jetpack Compose
Концепция уровней в работе фокусов в Compose
Управление фокусом и настройка поведения
Modifier.focusable()
иModifier.focusTarget()
— в чём разница?FocusManager и FocusOwner — глобальное управление фокусом
FocusRequester — запрос фокуса программно
Modifier.focusProperties
— настройка навигацииModifier.focusGroup
— группировка элементов для фокусировкиModifier.focusRestorer()
— восстановление состояния фокуса
Modifier.onFocusChanged
— отслеживание изменений фокусаInteractionSource
— подписка на изменение состояния фокуса
Введение в фокус в Compose
Представьте, что вы управляете интерфейсом не пальцами, а только клавиатурой, пультом или геймпадом. Как понять, где вы сейчас находитесь? Как выбрать нужный элемент? Это и есть работа механизма фокуса — он определяет, какой элемент активен и готов принимать ввод.

Пример выше показывает, как пользователь перемещается по интерфейсу с помощью пульта: активный элемент подсвечивается, указывая текущее положение фокуса. Благодаря этому управление остаётся чётким и последовательным, а переходы между элементами происходят ожидаемым образом.
Грамотно настроенный фокус особенно важен в средах без сенсорного ввода, например на Android TV, Wear OS и в интерфейсах с поддержкой accessibility. Он обеспечивает:
управляемую навигацию — позволяет перемещаться по интерфейсу с помощью клавиатуры или пульта;
доступность (accessibility) — определяет, какие элементы будут озвучены скринридерами, и помогает людям с ограниченными возможностями здоровья взаимодействовать с приложением;
интуитивный UI — визуальная подсветка текущего элемента (рамка, анимация, изменение цвета) помогает ориентироваться в интерфейсе.
Основы работы с фокусом в Jetpack Compose
Чтобы понять, как работает фокус, можно представить его как курсор, который всегда находится в интерфейсе. Вы можете перемещать этот курсор с помощью навигации (например, клавиатурой или пультом) или установить его программно на нужный элемент.
В Jetpack Compose система работы с фокусом во многом повторяет подход, знакомый разработчикам Android View: чтобы элемент мог участвовать в навигации и реагировать на фокус, его нужно явно пометить как фокусируемый. В View это делалось через атрибуты XML (android:focusable="true"
) или методы (setFocusable(true)
), а в Compose — через модификаторы focusable()
или focusTarget()
.
Основное отличие в том, что в Compose управление фокусом стало декларативным: фокусное поведение описывается как часть дерева UI через модификаторы, а не через императивные вызовы в коде.
Modifier.focusable и Modifier.focusTarget — модифаеры, которые делают элемент доступным для навигации и обработки фокусных событий.
FocusManager — отвечает за управление фокусом на уровне всего приложения. Позволяет перемещать фокус в определённом направлении или очищать его.
FocusRequester — используется для программного запроса фокуса.
Modifier.focusProperties — позволяет кастомизировать навигацию фокуса.
Modifier.focusGroup — группирует элементы так, чтобы внутри группы можно было переопределять порядок навигации фокуса.
Modifier.focusRestorer — сохраняет и восстанавливает фокус.
Modifier.onFocusChanged — позволяет отслеживать изменения состояния фокуса элемента.
В следующих разделах статьи мы разберём каждый из этих инструментов подробнее и рассмотрим их практическое применение.
Концепция уровней в работе фокусов в Compose
Перед тем как погружаться в то, как мы можем управлять фокусом в Compose, важно разобраться с концепцией уровней в иерархии Composable-функций.
Элементы, находящиеся внутри одного родительского компонента, считаются находящимися на одном уровне. Например, все дочерние элементы внутри Column
или Row
располагаются на одном уровне иерархии.
Навигация по пользовательскому интерфейсу с использованием фокуса:
Одномерная навигация (TAB-навигация). Предполагает последовательное перемещение вперёд и назад между элементами интерфейса. По умолчанию порядок фокусировки соответствует последовательности объявления элементов в коде. Одномерная навигация осуществляется при нажатии клавиши Tab на клавиатуре или с помощью прокрутки на таких устройствах как умные часы. В этом режиме фокусируется каждый элемент на экране.
Двумерная навигация. Позволяет перемещаться влево, вправо, вверх или вниз по интерфейсу. Осуществляется с помощью клавиш со стрелками или кнопок на пультах управления. Клавиши Enter и Esc (или кнопки Ok и Back на пульте) позволяют переходить на уровень ниже или возвращаться на уровень выше в иерархии функций-композиций.
Рассмотрим несколько примеров, чтобы лучше понять концепцию.
Пример одномерной навигации
В этом примере показана последовательная (одномерная) навигация между четырьмя кнопками, расположенными в одной Column
. Jetpack Compose позволяет перемещаться между элементами с помощью клавиши Tab или стрелок вверх/вниз.
Column {
TextButton({ }) { Text("FIRST") }
TextButton({ }) { Text("SECOND") }
TextButton({ }) { Text("THIRD") }
TextButton({ }) { Text("FOURTH") }
}

Если ваш макет содержит несколько строк (Row
), Jetpack Compose позволяет перемещаться между элементами без дополнительного кода.
При нажатии клавиши Tab фокус автоматически перемещается по элементам в порядке их объявления — от первого к четвёртому. Использование клавиш со стрелками позволяет перемещаться в двухмерном пространстве, двигаясь влево, вправо, вверх или вниз.
Горизонтальное расположение
В приведённом ниже примере TextButton
объявлены внутри двух Row
. Порядок фокусировки при нажатии Tab идёт сверху вниз и слева направо.
Column {
Row {
TextButton({ }) { Text("First field") }
TextButton({ }) { Text("Second field") }
}
Row {
TextButton({ }) { Text("Third field") }
TextButton({ }) { Text("Fourth field") }
}
}

Вертикальное расположение
В следующем примере вместо Row
используются Column
, но принцип работы фокуса остаётся таким же. Теперь фокус сначала перемещается вертикально внутри колонок, а затем горизонтально между колонками.
Row {
Column {
TextButton({ }) { Text("First field") }
TextButton({ }) { Text("Second field") }
}
Column {
TextButton({ }) { Text("Third field") }
TextButton({ }) { Text("Fourth field") }
}
}

Хотя в этих примерах направление одномерной навигации разное, в двухмерной системе они ведут себя одинаково. Поскольку элементы на экране занимают одинаковые позиции, перемещение вправо из первой колонки ведёт ко второй, а вниз из первой строки — ко второй строке.
В Compose система фокусов работает «из коробки» и позволяет перемещаться между элементами без дополнительного кода. Однако в реальных приложениях часто требуется более гибкий контроль: программная установка фокуса, изменение порядка навигации или обработка событий при получении/потере фокуса.
В следующих разделах разберём инструменты для работы с фокусом и способы настройки его поведения под конкретные сценарии.
Управление фокусом и настройка поведения
Modifier.focusable() и Modifier.focusTarget() — в чём разница?
Оба модификатора делают элемент фокусируемым, но разница в том, как они обрабатывают accessibility:
focusable()
— это более высокий уровень абстракции. Он не только делает элемент доступным для фокуса, но и добавляет нужную семантику для accessibility.focusTarget()
— низкоуровневый модификатор, который просто регистрирует элемент как цель для фокуса, но ничего не делает для доступности. Если вам нужна accessibility, придётся вручную добавитьsemantics
.
Пример с focusable
:
Box(
modifier = Modifier
.onFocusChanged { state ->
if (state.isFocused) {
println("Элемент в фокусе!")
}
}
.focusable()
)
➡️ Фокус работает, и элемент будет распознаваться скринридером.
Пример с focusTarget
:
Box(
modifier = Modifier
.onFocusChanged { state ->
if (state.isFocused) {
println("Элемент в фокусе!")
}
}
.focusTarget()
)
➡️ Фокус тоже работает, но скринридер проигнорирует элемент, потому что нет семантики. Если нужно, можно добавить её вручную:
Box(
modifier = Modifier
.focusTarget()
.semantics { contentDescription = "Фокусируемый элемент" }
)
💡 Если не знаете, какой модификатор выбрать — берите
focusable
. Он покрывает 90% сценариев. А если нужна полная кастомизация или вам не важна доступность — тогдаfocusTarget
.
FocusManager и FocusOwner: Глобальное управление фокусом
В дополнение к управлению фокусом на уровне отдельных элементов, Jetpack Compose предоставляет механизм глобального контроля с помощью FocusManager
. Этот интерфейс позволяет программно перемещать фокус между элементами и очищать его, если это необходимо.
Основные функции FocusManager
moveFocus(focusDirection: FocusDirection): Boolean
Позволяет перемещать фокус в заданном направлении. Например,FocusDirection.Next
переводит фокус к следующему элементу, аFocusDirection.Previous
— к предыдущему.clearFocus(force: Boolean = false)
Снимает фокус с текущего элемента и возвращает его на корневой фокусируемый элемент. Если передан параметрforce = true
, фокус будет снят даже в случае, если элемент ранее захватил его (captureFocus()
).
Для получения экземпляра FocusManager
в пределах композиции используется LocalFocusManager.current
:
val focusManager = LocalFocusManager.current
focusManager.moveFocus(FocusDirection.Next) // Перемещаем фокус на следующий элемент
FocusOwner: кто управляет фокусом?
Интерфейс FocusManager
реализуется классом FocusOwner
. Именно он управляет распределением фокуса в приложении и определяет, какой элемент должен его получить. Хотя в большинстве случаев взаимодействие с FocusOwner
напрямую не требуется, понимание его роли помогает разобраться в механизме фокусировки в Jetpack Compose.
В результате FocusManager
становится важным инструментом для управления фокусом на уровне всего приложения, позволяя программно менять его состояние и поддерживать удобную навигацию в интерфейсе.
FocusRequester: запрос фокуса программно
Эта функция позволяет программно запрашивать фокус у компонента. Полезно, если нужно сфокусироваться на элементе при определённом событии — например, при нажатии на другой элемент.
Пример программного запроса фокуса:
val focusRequester = remember { FocusRequester() }
var color by remember { mutableStateOf(Color.Black) }
Box(
Modifier
.clickable { focusRequester.requestFocus() } // Клик запрашивает фокус
.border(2.dp, color)
// focusRequester должен быть ДО focusable
.focusRequester(focusRequester)
// onFocusChanged должен быть ДО focusable для корректной обработки фокуса
.onFocusChanged { color = if (it.isFocused) Color.Green else Color.Black }
.focusable()
)
Когда вызывается focusRequester.requestFocus()
, элемент пытается получить фокус. Если он является фокусируемым, то изменение состояния фиксируется в onFocusChanged
, что позволяет, например, изменить его внешний вид.
Важно учитывать порядок модификаторов: сначала применяется focusRequester
, затем onFocusChanged
и только после этого focusable
, чтобы корректно обработать получение фокуса.
Функция requestFocus
возвращает значения:
true
, если фокус успешно установлен.false
, если элемент не смог получить фокус.Ошибку, если
focusRequester
не был привязан к Composable компоненту с помощьюModifier.focusRequester()
или если запрос фокуса был выполнен во время композиции.
С версии Compose 1.8 можно запросить фокус в определенном направлении с помощью focusRequester.requestFocus(FocusDirection.Up)
. Это полезно, когда у нас есть модификатор focusProperties
, в котором переопределено нужное направление, например, Up
.
Помимо базового примера с Box
, рассмотрим, как FocusRequester
используется в реальном приложении.
Пример использования FocusRequester в PREMIER

В примере пользователь переключается между ТВ-каналами. Когда он выходит из режима просмотра и возвращается в раздел ТВ-каналов, мы программно вызываем focusRequester.requestFocus()
на нужном ТВ-канале — на том, который пользователь смотрел сейчас. Благодаря этому навигация получается предсказуемой, иначе приходилось бы каждый раз начинать листать каналы с начала — представляете, как неудобно.
Сохранение последнего сфокусированного элемента через focusRequester.saveFocusedChild() и focusRequester.restoreFocusedChild()
Эти функции позволяют сохранять и восстанавливать фокус внутри компонента. Особенно полезно при навигации или смене экранов.
Пример сохранения и восстановления фокуса в LazyRow
:
val focusRequester = remember { FocusRequester() }
LazyRow(
Modifier
.focusRequester(focusRequester)
.focusProperties {
onExit = {
focusRequester.saveFocusedChild() // Сохраняем фокус при выходе
}
onEnter = {
if (focusRequester.restoreFocusedChild()) {
cancelFocusChange() // Восстанавливаем фокус при возврате
}
}
}
) {
item { Button(onClick = {}) { Text("1") } }
item { Button(onClick = {}) { Text("2") } }
item { Button(onClick = {}) { Text("3") } }
item { Button(onClick = {}) { Text("4") } }
}
Когда пользователь покидает LazyRow
, текущий элемент сохраняет своё фокусное состояние. При возврате обратно фокус восстанавливается на последнем активном элементе. В случае успешного восстановления используется cancelFocusChange()
, чтобы предотвратить использование дефолтного FocusRequester-а.
Это особенно полезно в сценариях горизонтальной навигации или при переключении вкладок, так как пользователь возвращается к тому же элементу, на котором он остановился.
Рассмотрим теперь, как этот подход помогает в реальном приложении.
Пример из PREMIER
Для наглядности данного примера я заранее отключил часть функциональности отвечающей за сохранение последнего сфокусированного элемента с использованием функций saveFocusedChild
и restoreFocusedChild
.

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

Теперь при возврате на экран Коллекций фокус остается на последнем активном элементе — навигация стала понятной и предсказуемой.
Захват фокуса с помощью focusRequester.captureFocus()
Функция captureFocus
позволяет компоненту захватить фокус и отклонять все внешние запросы на его смену. Это полезно в ситуациях, когда нужно зафиксировать фокус на элементе, например, при валидации ввода.
Когда компонент находится в захваченном состоянии, он перестаёт реагировать на запросы фокуса от других элементов. Освободить фокус можно вручную с помощью функции freeFocus
.
Пример захвата и освобождения фокуса в зависимости от длины текста:
val focusRequester = remember { FocusRequester() }
var value by remember { mutableStateOf("apple") }
var borderColor by remember { mutableStateOf(Color.Transparent) }
TextField(
value = value,
onValueChange = {
value = it
// Захватываем или освобождаем фокус в зависимости от длины текста
if (it.length > 5) {
focusRequester.captureFocus()
} else {
focusRequester.freeFocus()
}
},
modifier = Modifier
.border(2.dp, borderColor)
.focusRequester(focusRequester)
.onFocusChanged { focusState ->
// Обновляем цвет границы в зависимости от состояния фокуса
borderColor = if (focusState.isCaptured) Color.Red else Color.Transparent
}
)
При вводе текста длиннее 5 символов компонент автоматически захватывает фокус, меняя рамку на красную. Если текст становится короче 5 символов, фокус освобождается, а рамка исчезает.
Этот механизм полезен при реализации строгих правил ввода или при блокировке взаимодействия, пока не выполнены определённые условия.
💡 Хотим заблокировать смену фокуса —
captureFocus()
, хотим вернуть всё обратно —freeFocus()
.
Modifier.focusProperties: настройка навигации
В Jetpack Compose модификатор Modifier.focusProperties
предоставляет мощный инструмент для детальной настройки поведения фокуса. С его помощью можно не только определить, как фокус перемещается между элементами, но и задать кастомную логику при входе и выходе из фокусируемых областей.
Важно: focusProperties
применяется к модификатору focusTarget
или focusable
(который является реализацией focusTarget
). Использование focusProperties
без focusTarget
не имеет смысла, так как переопределённые свойства не будут присвоены.
Параметр canFocus
Параметр canFocus
определяет, может ли компонент получать фокус. Установив его в false
, можно исключить элемент из фокусной навигации:
Box(
modifier = Modifier
.focusProperties { canFocus = false }
.focusTarget()
)
В этом примере Box
будет пропускаться при перемещении фокуса с клавиатуры или пульта.
Зачем нужен canFocus
, если есть Modifier.focusable(enabled = false)
?
На первый взгляд, Modifier.focusable(enabled = false)
и Modifier.focusProperties { canFocus = false }
выполняют одну и ту же задачу — делают элемент нефокусируемым. Однако между ними есть важное различие.
Modifier.focusable(enabled = false)
буквально ничего не делает. Он просто возвращает изначальный Modifier
, не добавляя элемент в систему фокусировки. Если запросить фокус на таком элементе, то ничего не произойдёт.
Modifier.focusProperties { canFocus = false }
, в свою очередь, лишь устанавливает флаг, который проверяется при запросе фокуса. Если фокус будет запрашиваться на такой элемент, система попытается перевести его на первый доступный дочерний элемент, который может принять фокус.
Эти различия важно учитывать при выборе модификатора. Если элемент содержит дочерние компоненты, способные принимать фокус, Modifier.focusProperties { canFocus = false }
может быть предпочтительнее, так как не исключает его из общей системы фокусировки.
Параметры для управления направлением фокуса
Modifier.focusProperties
позволяет задать явные правила перемещения фокуса по интерфейсу:
next
— следующий элемент для фокуса (например, при нажатии "Tab");previous
— предыдущий элемент для фокуса (например, при "Shift + Tab»);up
,down
,left
,right
— элементы для перемещения фокуса по соответствующим направлениям.
Такой подход особенно полезен для устройств с аппаратной навигацией, например, Android TV.
Пример настройки порядка фокуса:
val (first, second, third) = FocusRequester.createRefs()
Column {
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.focusRequester(first)
.focusProperties { next = second }
.focusTarget()
)
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.focusRequester(second)
.focusProperties {
previous = first
next = third
}
.focusTarget()
)
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.focusRequester(third)
.focusProperties { previous = second }
.focusTarget()
)
}
В этом примере фокус будет перемещаться по текстовым полям в заданном порядке: первый → второй → третий, а по Shift + Tab — в обратном направлении.
Параметры для обработки входа и выхода фокуса
В сложных интерфейсах иногда необходимо не просто перемещать фокус, но и реагировать на его вход и выход из компонента. Для этого есть параметры onEnter
и onExit
.
onEnter
— срабатывает при входе фокуса в компонент. Можно, например, автоматически перенаправить фокус на вложенный элемент или отменить стандартное поведение.onExit
— срабатывает при выходе фокуса из компонента. Это полезно для сохранения текущего состояния или выполнения завершающих действий.
Пример использования onEnter
и onExit
:
val buttonFocusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
.focusProperties {
onExit = {
println("Фокус вышел из элемента")
}
onEnter = {
println("Фокус вошёл в элемент")
buttonFocusRequester.requestFocus()
// Перенаправляем фокус на кнопку
}
}
.focusTarget()
) {
Button(
modifier = Modifier.focusRequester(buttonFocusRequester),
onClick = {}
) {
}
}
В этом примере при входе фокуса в Box
срабатывает onEnter
, где можно перенаправить фокус с помощью focusRequester.requestFocus()
. При выходе фокуса — onExit
просто фиксирует событие.
Сохранять фокус на текущем элементе нужно, чтобы пользователь мог вернуться ровно туда, где он был, а не начинал листать заново. Например, когда в списках или каруселях контента на Android TV временно покидаешь раздел, например, открываешь карточку фильма, то естественно ожидать, что по возвращению фокус останется на том же элементе. То есть если фокус не сохранять, он может сбрасываться на первый элемент списка или переходить в неожиданное место, что сделает UX довольно раздражающим.
В Compose эту проблему можно решить с помощью saveFocusedChild()
и restoreFocusedChild()
, которые позволяют сохранить и восстановить последний сфокусированный элемент:
val focusRequester = remember { FocusRequester() }
LazyRow(
modifier = Modifier
.focusRequester(focusRequester)
.focusProperties {
onExit = { focusRequester.saveFocusedChild() }
onEnter = {
if (focusRequester.restoreFocusedChild()) {
cancelFocusChange()
}
}
}
) {
items(listOf("1", "2", "3", "4")) { item ->
Button(onClick = {}) { Text(item) }
}
}
В этом примере при выходе фокуса из LazyRow
сохраняется текущий сфокусированный элемент. При повторном входе фокус восстанавливается на этом элементе.
Этот подход особенно полезен при работе с навигацией между разными секциями приложения, как в следующем примере из PREMIER.
Пример из PREMIER
Для наглядности я заранее отключил часть функциональности, отвечающей за переход между сайдбаром и разделами приложения.

Пользователь переключается между сайдбаром и разделами, но при каждом возврате фокус переходит не на ту вкладку, на которой был ранее, а на ближайшую. Согласитесь, это сбивет с толку и путает.
Чтобы исправить это, в focusProperties
следует переопределить параметры onExit
и onEnter
, вызвав requestFocus()
на нужных FocusRequester
-ах, чтобы фокус перемещался на корректные элементы, а не на ближайшие.

После внедрения этих изменений навигация между сайдбаром и разделами стала предсказуемой и удобной.
Modifier.focusGroup
: группировка элементов для фокусировки
Модификатор Modifier.focusGroup
позволяет объединять компоненты в группы фокуса, обеспечивая приоритетное перемещение фокуса внутри этих групп перед переходом к элементам вне их.
Создание группы фокуса
Чтобы создать группу фокуса, применяем модификатор Modifier.focusGroup()
к контейнеру, содержащему фокусируемые элементы. Это гарантирует, что при навигации с помощью клавиатуры, Dpad или программно с использованием FocusManager.moveFocus()
, фокус будет перемещаться между элементами внутри группы перед переходом к элементам вне её.
LazyVerticalGrid(columns = GridCells.Fixed(4)) {
item(span = { GridItemSpan(maxLineSpan) }) {
Row(modifier = Modifier.focusGroup()) {
FilterChipA()
FilterChipB()
FilterChipC()
}
}
items(contentCardsList) {
ContentCard(contentCard = it)
}
}
В этом примере в LazyVerticalGrid
содержатся карточки контента (ContentCard
) и фильтры (FilterChipA
, FilterChipB
, FilterChipC
). Фильтры объединены в focusGroup
, поэтому при перемещении фокуса, например, с помощью клавиатуры, он сначала будет переходить между фильтрами, а затем переключаться на карточки контента.
Поведение группы фокуса
Применение Modifier.focusGroup
делает всю группу воспринимаемой как единое целое с точки зрения фокуса, однако сама группа не получает фокус; вместо этого фокус переходит к ближайшему дочернему элементу. Это гарантирует, что даже если некоторые элементы внутри группы частично видимы или скрыты, фокус будет перемещаться по ним перед переходом к элементам вне группы.
Использование focusGroup
особенно полезно в сценариях, где важно обеспечить приоритетное перемещение фокуса внутри группы. Например, в экранных клавиатурах или других сложных виджетах.
Пример из PREMIER
Хорошим примером использования этого модификатора может послужить экранная клавиатура, которая используется в разделе «Поиск» нашего приложения.
Внешний контейнер клавиатуры имеет модификатор focusGroup
, который гарантирует, что при перемещении фокуса приоритет будет у элементов находящихся внутри группы. Это особенно важно при Tab-навигации.

На примере видно, что с focusGroup
клавиши клавиатуры корректно получают фокус и фокус остается в пределах виджета, пока не будут обработаны все его элементы.
Без модификатора focusGroup
переместить фокус с сайдбара на экранную клавиатуру будет невозможно, так как система не сможет определить подходящий элемент для фокусировки и фокус останется на сайдбаре.
Modifier.focusRestorer()
: восстановление состояния фокуса
В Jetpack Compose модификатор Modifier.focusRestorer
помогает сохранять и восстанавливать фокус внутри группы элементов. Это особенно полезно в динамически изменяющихся интерфейсах, где элементы могут добавляться или удаляться, и важно, чтобы фокус возвращался к предыдущему активному элементу.
Создание группы фокуса с восстановлением
Чтобы настроить восстановление фокуса, модификаторы Modifier.focusGroup()
и Modifier.focusRestorer()
применяются к контейнеру, содержащему фокусируемые элементы. При этом можно указать fallback
, чтобы задать поведение на случай, если восстановление фокуса не удалось.
val focusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.focusGroup()
.focusRestorer(fallback = focusRequester)
) {
Button(
onClick = {},
modifier = Modifier.focusRequester(focusRequester)
) {
Text("Кнопка 1")
}
Button(onClick = {}) {
Text("Кнопка 2")
}
}
В этом примере фокус сохраняется на активном элементе. Если восстановление не удаётся, фокус по умолчанию вернётся к первой кнопке через указанный focusRequester
.
Как работает восстановление фокуса
Modifier.focusRestorer
сохраняет элемент с фокусом внутри фокус-группы. Если интерфейс перестроится или элемент временно исчезнет, Compose попытается вернуть фокус на тот же элемент после его повторного появления. Если восстановление невозможно, например, элемент удалён, срабатывает fallback
, в котором можно указать резервный элемент для фокусировки.
Пример из PREMIER
Для демонстрации работы модификатора в разделе «Коллекции» в PREMIER механизм сохранения последнего сфокусированного элемента был переписан: вместо FocusRequester.saveFocusedChild()
и FocusRequester.restoreFocusedChild()
теперь используется Modifier.focusRestorer
.

На примере видно, что Modifier.focusRestorer
корректно сохраняет последний сфокусированный элемент. Он работает аналогично FocusRequester.saveFocusedChild()
и FocusRequester.restoreFocusedChild()
.
На примере видно, что Modifier.focusRestorer
корректно сохраняет последний сфокусированный элемент. Он работает аналогично FocusRequester.saveFocusedChild()
и FocusRequester.restoreFocusedChild()
.
💡 Если вам нужно более точное управление фокусировкой или дополнительная логика при переходе фокуса между секциями, используйте методы
FocusRequester
. В остальных случаях достаточноModifier.focusRestorer
.
Реакция на фокус
Modifier.onFocusChanged
: отслеживание изменений фокуса
Модификатор onFocusChanged
позволяет отслеживать изменения фокуса у компонентов. Это особенно полезно, когда нужно визуально выделять сфокусированный элемент или выполнять дополнительные действия при получении или потере фокуса.
Модификатор принимает лямбду с параметром FocusState
, содержащим информацию о текущем состоянии фокуса.
Поля FocusState
Объект FocusState
, передаваемый в функцию onFocusChanged
, предоставляет следующие свойства:
isFocused
: возвращаетtrue
, если данный компонент в данный момент имеет фокус;hasFocus
: возвращаетtrue
, если фокус находится на этом компоненте или на одном из его дочерних элементов;isCaptured
: возвращаетtrue
, если компонент удерживает фокус, предотвращая его передачу другим элементам.
Эти свойства позволяют точно определить текущее состояние фокуса и соответствующим образом отреагировать на его изменения.
Пример отслеживания фокуса через Modifier.onFocusChanged
var color by remember { mutableStateOf(Color.Black) }
Box(
modifier = Modifier
.onFocusChanged { focusState ->
color = if (focusState.isFocused) Color.Green else Color.Black
}
.focusable()
.size(100.dp)
.background(color)
)
В этом примере цвет Box
меняется на зелёный при фокусе и возвращается к чёрному при его потере.
⚠️ Важно:
onFocusChanged
отслеживает состояние первогоfocusTarget
после себя в цепочке модификаторов. Чтобы правильно обрабатывать фокус, используйтеonFocusChanged
перед модификаторомfocusable
.
Пример для Android TV
На Android TV или устройствах с пультом управления подсвечивать фокус особенно важно, так как пользователи перемещаются по интерфейсу с помощью клавиш навигации Dpad. Можно визуально выделить сфокусированный элемент или отображать дополнительную информацию.
@Composable
fun TvMenuItem(text: String) {
var isFocused by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
}
.focusable()
.padding(16.dp)
.background(if (isFocused) Color.Blue else Color.Gray)
.border(2.dp, if (isFocused) Color.White else Color.Transparent)
.size(200.dp, 60.dp)
) {
Text(
text = text,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
@Composable
fun TvMenu() {
Column {
TvMenuItem("Главная")
TvMenuItem("Фильмы")
TvMenuItem("Сериалы")
TvMenuItem("Настройки")
}
}
В этом примере:
фокусированный элемент окрашивается в синий цвет с белой рамкой;
при потере фокуса цвет меняется на серый, а рамка исчезает;
пользователь может перемещаться по элементам с помощью пульта или клавиатуры.
Применение в сложных интерфейсах
В реальных проектах onFocusChanged
можно использовать для:
реакции интерфейса на сфокусированный элемент;
показа/скрытия всплывающих подсказок при фокусе;
сохранения текущего состояния фокуса и возвращения к нему при повторном открытии экрана.
InteractionSource
: подписка на изменение состояния фокуса
InteractionSource
— это инструмент для отслеживания взаимодействий пользователя с компонентом, включая фокусировку. Чтобы получать события изменения фокуса, необходимо передать InteractionSource
в параметр focusable(interactionSource = interactionSource)
, после чего можно подписаться на состояние фокуса с помощью collectIsFocusedAsState()
. Этот метод возвращает State
, обновляющееся только при фактическом изменении фокуса, что предотвращает лишние вызовы и перерисовки.
Отличие от onFocusChanged
:
collectIsFocusedAsState()
обновляет значение только при изменении состояния фокуса, что помогает избежать ненужных вызовов и повышает производительность;onFocusChanged
вызывается при каждом изменении состояния фокуса, включая первую композицию. Это может привести к неожиданному поведению, если фокусное состояние не изменилось, но функция всё равно сработала.
Пример отслеживания фокуса через InteractionSource
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
Box(
modifier = Modifier
.focusable(interactionSource = interactionSource)
.background(if (isFocused) Color.Green else Color.Gray)
.size(100.dp)
) {
Text(
text = if (isFocused) "В фокусе" else "Нет фокуса",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
В этом примере:
Box
меняет цвет на зелёный, когда он в фокусе, и на серый при потере фокуса;использование
InteractionSource
предотвращает лишние вызовы при первой композиции.
Пример из PREMIER
В PREMIER onFocusChanged
и InteractionSource
используются для визуального выделения элементов, принимающих фокус.

На примере видно, как пользователь перемещается по интерфейсу и каждый элемент, попавший в фокус, изменяет внешний вид. Это касается как кнопок, так и карточек фильмов или сериалов. Благодаря onFocusChanged
и InteractionSource
интерфейс оперативно реагирует на фокусировку, делая навигацию интуитивно понятной.
Рекомендации по работе с фокусами
Работа с фокусом особенно важна при разработке приложений для Android TV и при обеспечении доступности (accessibility) пользовательских интерфейсов. Ошибки в обработке фокуса могут приводить к неожиданному поведению UI — как мы уже видели в предыдущих примерах, когда фокус перескакивал или не мог найти целевой элемент. Но проблемы на этом не заканчиваются: фокус может застревать на одном элементе, уходить в "пустоту" или даже приводить к крашам, если система не сможет корректно обработать его перемещение. Всё это ухудшает пользовательский опыт и делает навигацию неудобной, особенно на устройствах без сенсорного ввода.
Вот несколько ключевых рекомендаций, которые помогут избежать проблем.
1. Следите за порядком модификаторов
Механизм фокуса в Jetpack Compose чувствителен к порядку модификаторов, и неправильное их расположение может привести к неожиданному поведению или багам.
✅ Правильный порядок:
Modifier
.focusRequester(focusRequester) // Сначала назначаем запросчик фокуса
.focusable() // Затем делаем элемент фокусируемым
❌ Неправильный порядок:
Modifier
.focusable() // Элемент становится фокусируемым до назначения запросчика
.focusRequester(focusRequester) // Это может привести к багам
Всегда проверяйте порядок модификаторов при работе с фокусом, особенно если элемент не получает или реагирует на фокус так, как вы ожидаете.
2. Тестируйте навигацию фокусов на реальных устройствах
В реальной практике фокус может работать по-разному в зависимости от мощности устройства. Если на производительных девайсах все выглядит корректно, то на слабых могут возникнуть проблемы:
фокус может слетать или не устанавливаться на нужные элементы;
возможны даже краши, если нужный для фокуса элемент не успевает прогрузиться.
Такие проблемы сложно выявить в эмуляторе или на флагманских устройствах, поэтому обязательно тестируйте приложение на реальных девайсах, включая слабые модели. Это позволит заранее обнаружить крайние случаи и сделать навигацию фокуса стабильной.
3. Используйте модификаторы осознанно: какие и когда применять
Jetpack Compose предлагает гибкий набор инструментов для работы с фокусом, но их важно правильно комбинировать. Собрал их в таблицы-подсказки к которым можно обращаться, чтобы освежить память и выбрать подходящее решение.
Основные модификаторы фокуса в Jetpack Compose:
Модификатор | Описание |
---|---|
| Делает элемент доступным для фокуса, позволяя ему получать и обрабатывать события фокуса. |
| Связывает элемент с объектом |
| Позволяет отслеживать изменения состояния фокуса элемента и реагировать на получение или потерю фокуса. |
| Настраивает поведение фокуса при перемещении между элементами, позволяя изменить порядок обхода фокуса и задать действия при входе или выходе фокуса. |
| Определяет область внутри компонента, которая может получать фокус, обычно используется для указания конкретной части компонента, принимающей фокус. |
| Сохраняет и восстанавливает фокус внутри группы элементов, что полезно для поддержания состояния фокуса при динамических изменениях интерфейса. |
Свойства focusProperties:
Свойство | Описание |
---|---|
| Определяет, может ли элемент получать фокус. Если установлено в |
| Указывает элемент, который должен получить фокус при переходе вперёд (например, при нажатии клавиши "Tab"). |
| Указывает элемент, который должен получить фокус при переходе назад (например, при нажатии клавиши "Shift+Tab"). |
| Определяют элементы, которые должны получать фокус при перемещении в соответствующих направлениях с помощью Dpad. |
| Позволяет задать поведение при выходе фокуса из элемента в определённом направлении. |
| Позволяет задать поведение при входе фокуса в элемент из определённого направления. |
Методы FocusRequester:
Метод | Описание |
---|---|
| Запрашивает фокус для связанного элемента. |
| Принудительно захватывает фокус для элемента, даже если он не может быть сфокусирован в текущем состоянии. |
| Освобождает фокус с элемента, позволяя системе определить следующий элемент для фокуса. |
| Сохраняет информацию о дочернем элементе, который в данный момент имеет фокус. |
| Восстанавливает фокус на ранее сохранённом дочернем элементе. |
Заключение
Фокус в Jetpack Compose — это мощный инструмент, который определяет удобство и доступность пользовательского интерфейса. Мы рассмотрели ключевые механизмы работы с фокусом, разобрались, как управлять им программно, настраивать навигацию и учитывать нюансы платформы, будь то мобильные устройства, ТВ или устройства без сенсорного управления.
Если вам понравился разбор и вы хотите больше материалов по Android-разработке, я завёл telegram-канал , в котором делюсь трендами, реальными кейсами и личным опытом. А в канале Смотри за IT можно больше узнать о том, как разрабатывается PREMIER, RUTUBE и другие медиасервисы.
До встречи в следующих статьях!