explicit backing fields: как убрать пару _state и state из каждой ViewModel
В любой ViewModel живёт одна и та же конструкция: приватный MutableStateFlow и публичный StateFlow поверх него. Две строки вместо одной, подчёркивание в имени, о котором когда-то договорились на ревью, и постоянный риск отдать наружу мутабельный тип по невнимательности.
class ProfileViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow = _uiState
}
В Kotlin 2.4 explicit backing fields стали стабильными, и эта пара схлопывается в одно свойство. Публичный тип объявляется как обычно, а ключевое слово field задаёт реальный тип хранения:
class ProfileViewModel : ViewModel() {
val uiState: StateFlow
field = MutableStateFlow(UiState.Loading)
fun retry() {
uiState.value = UiState.Loading // внутри класса это MutableStateFlow
}
}
Дальше всё решает точка обзора. Внутри класса компилятор видит поле и отдаёт MutableStateFlow, поэтому значение меняется привычным .value. Снаружи то же имя имеет тип StateFlow, и присвоить в него не выйдет. Гарантия осталась прежней, а лишнее имя ушло.
С коллекциями приём работает так же, и там он избавляет сразу от двух костылей: не нужен ни toList() на каждом обращении, ни отдельное неизменяемое представление.
class Cart {
val items: List
field = mutableListOf()
fun add(item: String) = items.add(item)
}
Требований к такому свойству немного, но соблюдаются они строго. Оно обязано быть val, без собственного геттера, не open и не делегированное. Тип поля должен быть подтипом типа свойства, а видимость у поля всегда приватная. Для var синтаксис не работает, так что изменяемые свойства остаются на старой схеме.
Снаружи не меняется ничего: collectAsStateWithLifecycle() в Compose и подписки в тестах видят прежний StateFlow. Под ту же схему попадают события через MutableSharedFlow и вообще любые пары, где внутри нужен изменяемый тип, а наружу отдаётся его неизменяемый родитель.
Сам синтаксис появился ещё в 2.3.0 в декабре 2025, но собирался только с флагом -Xexplicit-backing-fields. Теперь опт-ин не нужен, достаточно поднять версию Kotlin в проекте.
Переписывать разом весь модуль смысла мало.
Больше всего выигрывают классы, где таких пар несколько: экранные ViewModel с отдельными потоками для контента, ошибок и прогресса. Там уходит половина объявлений, а вместе с ними и путаница, к какому из двух имён обращаться.
#mobilevkhub #kotlin
В любой ViewModel живёт одна и та же конструкция: приватный MutableStateFlow и публичный StateFlow поверх него. Две строки вместо одной, подчёркивание в имени, о котором когда-то договорились на ревью, и постоянный риск отдать наружу мутабельный тип по невнимательности.
class ProfileViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow = _uiState
}
В Kotlin 2.4 explicit backing fields стали стабильными, и эта пара схлопывается в одно свойство. Публичный тип объявляется как обычно, а ключевое слово field задаёт реальный тип хранения:
class ProfileViewModel : ViewModel() {
val uiState: StateFlow
field = MutableStateFlow(UiState.Loading)
fun retry() {
uiState.value = UiState.Loading // внутри класса это MutableStateFlow
}
}
Дальше всё решает точка обзора. Внутри класса компилятор видит поле и отдаёт MutableStateFlow, поэтому значение меняется привычным .value. Снаружи то же имя имеет тип StateFlow, и присвоить в него не выйдет. Гарантия осталась прежней, а лишнее имя ушло.
С коллекциями приём работает так же, и там он избавляет сразу от двух костылей: не нужен ни toList() на каждом обращении, ни отдельное неизменяемое представление.
class Cart {
val items: List
field = mutableListOf()
fun add(item: String) = items.add(item)
}
Требований к такому свойству немного, но соблюдаются они строго. Оно обязано быть val, без собственного геттера, не open и не делегированное. Тип поля должен быть подтипом типа свойства, а видимость у поля всегда приватная. Для var синтаксис не работает, так что изменяемые свойства остаются на старой схеме.
Снаружи не меняется ничего: collectAsStateWithLifecycle() в Compose и подписки в тестах видят прежний StateFlow. Под ту же схему попадают события через MutableSharedFlow и вообще любые пары, где внутри нужен изменяемый тип, а наружу отдаётся его неизменяемый родитель.
Сам синтаксис появился ещё в 2.3.0 в декабре 2025, но собирался только с флагом -Xexplicit-backing-fields. Теперь опт-ин не нужен, достаточно поднять версию Kotlin в проекте.
Переписывать разом весь модуль смысла мало.
Больше всего выигрывают классы, где таких пар несколько: экранные ViewModel с отдельными потоками для контента, ошибок и прогресса. Там уходит половина объявлений, а вместе с ними и путаница, к какому из двух имён обращаться.
#mobilevkhub #kotlin