Поворот экрана и жизненный цикл Fragment
Два вопроса, которые задают подряд: «что происходит при повороте экрана» и «чем цикл фрагмента отличается от цикла Activity». Второй — фильтр на тех, кто реально писал фрагменты.
Configuration change — изменение конфигурации устройства (ориентация, размер окна, язык, тема, размер шрифта). По умолчанию Android уничтожает Activity и создаёт её заново, чтобы она подтянула ресурсы под новую конфигурацию.
Вопрос 1: «Что происходит при повороте экрана?»
Что проверяет интервьюер
Понимаете ли вы, зачем система вообще пересоздаёт экран (а не «почему это баг Android»), и знаете ли три разных механизма сохранения состояния — они переживают разные катастрофы.
Развёрнутый ответ
Поворот — это смена конфигурации. Разметка, строки и размеры могут отличаться для портрета и ландшафта (layout-land/, values-sw600dp/), поэтому проще всего пересоздать экран с нуля, чем пытаться «переприменить» ресурсы. Порядок такой:
onPause → onStop → onSaveInstanceState → onDestroy
(новый экземпляр Activity)
onCreate(savedInstanceState) → onStart → onRestoreInstanceState → onResume
Что при этом выживает — вопрос на засыпку. Механизмов три, и они покрывают разные сценарии:
| Механизм | Переживает поворот | Переживает смерть процесса | Ограничения |
| Поля Activity | нет | нет | — |
onSaveInstanceState (Bundle) | да | да | только Parcelable, суммарно сотни килобайт |
ViewModel | да | нет | умирает вместе с процессом |
SavedStateHandle внутри ViewModel | да | да | тот же лимит размера, что у Bundle |
ViewModel переживает поворот не магией: ViewModelStore сохраняется системой как «non-configuration instance» и передаётся новому экземпляру Activity. При этом ViewModel не переживает убийство процесса — для этого и нужен SavedStateHandle.
class SearchViewModel(
private val state: SavedStateHandle
) : ViewModel() {
// переживёт и поворот, и смерть процесса
val query: StateFlow<String> = state.getStateFlow("query", "")
// переживёт только поворот
private val _results = MutableStateFlow<List<Item>>(emptyList())
val results: StateFlow<List<Item>> = _results
fun onQueryChanged(value: String) {
state["query"] = value
}
}
Про Bundle обязательно скажите про размер: он передаётся через Binder-транзакцию, лимит которой около 1 МБ на процесс. Попытка положить туда список объектов или битмап даёт TransactionTooLargeException — уже на устройстве пользователя, а не в дебаге.
Типичные ошибки кандидатов
- Предлагают
android:configChanges="orientation|screenSize"как «решение». Это не решение, а отключение механизма: Activity перестаёт пересоздаваться, вы получаетеonConfigurationChangedи обязаны руками переприменить все ресурсы. И это всё равно не спасёт от смерти процесса — то есть баг вы не убрали, а спрятали. - Говорят «ViewModel решает проблему сохранения состояния». Только половину: перезапуск процесса он не переживает.
- Забывают, что смена темы «светлая/тёмная», размера шрифта в настройках, языка и режима split-screen — это тоже configuration change с тем же пересозданием.
Вопрос 2: «Чем жизненный цикл Fragment отличается от Activity?»
Что проверяет интервьюер
Знание ключевого факта: у фрагмента два жизненных цикла — самого фрагмента и его View. Их рассинхрон — причина половины утечек и «двойных подписок» в реальных проектах.
Развёрнутый ответ
Помимо привычных колбэков у фрагмента есть свои: onAttach → onCreate → onCreateView → onViewCreated → onStart → onResume и обратно onPause → onStop → onDestroyView → onDestroy → onDetach.
Главное отличие — onDestroyView. Когда фрагмент уходит в back stack, его View уничтожается, а сам объект фрагмента остаётся жив. Потом при возврате вызывается новый onCreateView. То есть один экземпляр фрагмента может пережить несколько View.
Отсюда два практических следствия, которые и хотят услышать:
class ProfileFragment : Fragment(R.layout.fragment_profile) {
private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentProfileBinding.bind(view)
// ВАЖНО: viewLifecycleOwner, а не this
viewModel.user.observe(viewLifecycleOwner) { user ->
binding.name.text = user.name
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // иначе фрагмент удержит мёртвую иерархию View
}
}
- Обнуляйте binding в
onDestroyView. Иначе живой фрагмент держит ссылку на уничтоженную иерархию View — это классическая утечка памяти, которую ловит LeakCanary. - Подписывайтесь на
viewLifecycleOwner, а не наthis. Если передать сам фрагмент, подписка не снимется при уничтожении View. Вернувшись из back stack, фрагмент подпишется ещё раз — и колбэк начнёт срабатывать дважды, трижды, четырежды. Симптом «после возврата назад экран мигает и запросы дублируются» почти всегда об этом.
Типичные ошибки кандидатов
- Считают, что фрагмент — «просто маленькая Activity». Тогда не объяснить, зачем нужен
viewLifecycleOwner. - Кладут инициализацию View в
onCreateфрагмента — там View ещё не существует. - Не знают, что фрагмент должен иметь публичный конструктор без аргументов: система пересоздаёт его сама, а аргументы передаются через
Bundleвarguments.
Как ответить кратко (20–30 секунд)
«Поворот — это configuration change: Activity уничтожается и создаётся заново, чтобы подтянуть ресурсы под новую конфигурацию. Состояние сохраняю тремя способами:
ViewModelпереживает поворот, но не смерть процесса;onSaveInstanceStateиSavedStateHandleпереживают оба случая, но ограничены размером Binder-транзакции.configChangesотключать не стоит — это маскирует проблему. У фрагмента же два жизненных цикла: свой и у View. Поэтому binding обнуляю вonDestroyView, а на данные подписываюсь черезviewLifecycleOwner, иначе после возврата из back stack получаю дублирующиеся подписки.»