Привет, Хабр!
Сегодня разберёмся, почему попытка «избавиться от ?
любой ценой» приводит к проблемам, и как жить с этим вообще жить.
Зачем вообще lateinit, и почему он кусает?
Kotlin дал нам нулевую безопасность, но иногда объект попросту нельзя инициализировать сразу — например, его втыкает DI‑контейнер или он зависит от жинджера Андроид‑жизненного цикла. В Java мы бы держали @Nullable
, в Kotlin ставят ?
, но IDE начинает требовать safe calls, Elvis‑операторы и прочее. И обычно разраб достаёт последнюю карту:
@Inject lateinit var repository: UserRepository // ну вот и без «?»
Ноль ?
, ноль NPE… и ровно до первого обращения раньше времени.
lateinit
— это обещание; JVM поверила, IDE поверила, а вы забыли. Получили UninitializedPropertyAccessException
.
5 классических ошибок
Ошибка 1. Доступ к полю в init{} до того, как DI-фреймворк сделал инъекцию
class UserPresenter @Inject constructor() {
@Inject lateinit var repo: UserRepository
init {
// Хочется бодро дернуть репозиторий
repo.warmUp() // UninitializedPropertyAccessException
}
}
Hilt/Dagger делает field‑injection после вызова конструктора, а init{}
― это часть конструктора. В результате промах по времени.
Как чинится:
Переходим на constructor injection — передаем всё нужное прямо в параметрах. Тогда объект гарантированно готов.
Если поле действительно должно быть
var
, то перекладываем раннюю логику в@PostConstruct
‑подобный метод или вonCreate()
/onStart()
(при Android).Крайняя страховка —
lateinit::repo.isInitialized
(но это только для логирования: лучше не плодитьif
вокруг каждой переменной).
Ошибка 2. lateinit var во ViewModel + recreation Activity
class DashboardViewModel : ViewModel() {
lateinit var analytics: AnalyticsService
fun trackScreen() = analytics.hit("dashboard")
}
При повороте экрана система пересоздаёт активити, но ViewModel остаётся. Если сервис инжектится снаружи активити, новый экран получит старую ViewModel, в которой analytics
ещё не проставлен.
Как фиксить:
Constructor + Hilt @HiltViewModel — пусть сервис придёт через конструктор.
Или
by lazy { AnalyticsService(context) }
— создаём при первом вызове, потокобезопасно.Для редких случаев —
SavedStateHandle
+ восстановление вinit{}
: не самый красивый, но рабочий обход.
Ошибка 3. Тесты: поле не сетится в @Before
class CsvParserTest {
private lateinit var parser: CsvParser
@Before
fun setup() {
// забыли parser = CsvParser(...)
}
@Test
fun `should parse three rows`() {
val rows = parser.parse("sample.csv") // ?
assertEquals(3, rows.size)
}
}
JUnit вызывает метод теста, видит незаполненный parser
— ошибка.
Что делать:
Ставим
@BeforeEach
/@Before
и инициализирую там.-
Если объект дорогой —
lazy
:private val parser by lazy { CsvParser(File("sample.csv")) }
Либо
Delegates.notNull<T>()
— полезно тем, что компилятор заставит присвоить хотя бы один раз.
Ошибка 4. lateinit у ViewBinding во Фрагменте → утечка памяти
class ProfileFragment : Fragment() {
private lateinit var binding: FragmentProfileBinding // кажется, удобно…
override fun onCreateView(...) = FragmentProfileBinding.inflate(inflater)
.also { binding = it }
// …
override fun onDestroyView() {
// binding = null ← *нельзя*, lateinit!
}
}
Фрагмент держит ссылку на уже уничтоженное View‑дерево; ликает всё, от Bitmap‑ов до Context‑а.
Как можно сделать:
private var _binding: FragmentProfileBinding? = null
private val binding get() = requireNotNull(_binding)
override fun onCreateView(...) = FragmentProfileBinding.inflate(inflater)
.also { _binding = it }
override fun onDestroyView() { _binding = null }
Или ставим делегат viewBinding()
.
Ошибка 5. Гонки корутин: чтение раньше записи
lateinit var session: SessionManager
suspend fun initSession() = withContext(Dispatchers.IO) {
session = SessionManager(api.login()) // поток 1
}
suspend fun doRequest() = withContext(Dispatchers.Default) {
session.fetchStuff() // поток 2: может
} // опередить и упасть
Один поток ещё пишет, другой уже читает — null safety бессильна.
Лекарства:
Mutex
илиAtomicRef
— жёсткая синхронизация.Или объявить как
val session by lazy(LazyThreadSafetyMode.PUBLICATION) { blockingLogin() }
— лениво, но потокобезопасно.В Android часто достаточно запустить
initSession()
вApplication.onCreate()
синхронно, чтобы гарантировать порядок.
Как жить без ?, но и без lateinit
by lazy { … }
Простой и безопасный. Работает для val
, потокобезопасен по дефолту.
val logger by lazy { LoggerFactory.getLogger(javaClass) }
Минус — нельзя «разинициализировать» (некритично в большинстве сервисов).
Null-Object Pattern
Часто сервис имеет «пустую» реализацию.
interface Analytics { fun hit(event: String) }
object NoOpAnalytics : Analytics {
override fun hit(event: String) { /* no-op */ }
}
// В проде:
val analytics: Analytics = if (BuildConfig.DEBUG) NoOpAnalytics else RealAnalytics()
Плюс — ни ?
, ни lateinit
, минус — надо писать заглушку, но зато тесты упрощаются.
Делегаты
-
Delegates.notNull<T>()
— заставляет присвоить до первого использования, но без полуинициализированных состояний.var token by Delegates.notNull<String>()
-
Свой делегат‑очиститель для фрагмента:
class AutoCleared<T : Any>(val fragment: Fragment) : ReadWriteProperty<Fragment, T> { private var value: T? = null override fun getValue(thisRef: Fragment, property: KProperty<*>): T = value ?: error("Value destroyed") override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { this.value = value fragment.viewLifecycleOwnerLiveData.observe(fragment) { owner -> owner.lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { this@AutoCleared.value = null } }) } } } class ProfileFragment : Fragment() { private var binding by AutoCleared<FragmentProfileBinding>(this) // … }
После
onDestroyView()
делегат сам обнуляет ссылку.
Итоги
lateinit
в Kotlin — это не костыль, но и не волшебная палочка. Он решает конкретную задачу: отложенную инициализацию без ?
, но требует чёткого понимания жизненного цикла, многопоточности и поведения DI‑фреймворков. Безопасная альтернатива почти всегда есть — lazy
, делегаты, Null Object, constructor injection.
Используете lateinit
в проде? Натыкались на ошибки или нашли устойчивые паттерны? Делитесь опытом в комментариях.
Если вы работаете с Android и хотите углубиться в современные подходы разработки, обратите внимание на серию открытых уроков курса Android Developer. Professional.
21 июля в 20:00 — поговорим о подходах к созданию анимаций, которые делают интерфейс живым и отзывчивым без ущерба для производительности.
Участвовать5 августа в 20:00 — соберём с нуля небольшое приложение и разберём, как выстраивать его UI и логику с учётом актуальных требований.
Участвовать12 августа в 20:00 — рассмотрим, как внедрить собственные элементы в реактивную архитектуру приложения.
УчаствоватьТакже доступно вступительное тестирование, которое поможет вам оценить актуальный уровень владения Android‑инструментами и определить, насколько программа курса вам подходит.