Почему этот Fragment течёт всей иерархией View при каждой навигации?
FeedFragment собирает flow из repository и отправляет каждый результат во View. LeakCanary сообщает об удержанном FeedFragment вместе со всей иерархией View после каждого ухода с экрана, и число удержанных экземпляров растёт с каждым визитом.
Ограничения: flow должен продолжать собираться, пока экран виден; не переносите работу в Activity; ничто не должно удерживать уничтоженную View.
class FeedFragment : Fragment(R.layout.feed) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private lateinit var binding: FeedBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FeedBinding.bind(view)
scope.launch {
repository.feedUpdates().collect { items ->
binding.list.submitList(items)
}
}
}
}
Найдите и исправьте ошибку.
Созданный вручную scope никто не отменяет, поэтому собирающая корутина переживает onDestroyView и продолжает держать binding — на каждый визит удерживается мёртвая иерархия View. Собирайте из viewLifecycleOwner.lifecycleScope внутри repeatOnLifecycle(STARTED), который отменяется при смерти View, либо отменяйте scope сами в onDestroyView.
- ✗Создавать
CoroutineScopeруками воFragmentи никогда его не отменять - ✗Собирать привязанный к View flow в
lifecycleScope, а не вviewLifecycleOwner.lifecycleScope - ✗Винить диспетчер или поле binding вместо неотменённого scope
- →Почему
viewLifecycleOwner— правильный владелец для привязанного к View сбора воFragment? - →Что делает
repeatOnLifecycle(STARTED), чего не делает голыйlaunchв том же scope?
Баг
scope создан вручную и живёт столько же, сколько объект Fragment. Его никто не отменяет, поэтому запущенная в нём корутина продолжает собирать flow и после onDestroyView — а её lambda захватывает binding, то есть всю уничтоженную иерархию View. Каждый визит на экран добавляет ещё одну живую корутину и ещё одну мёртвую иерархию.
class FeedFragment : Fragment(R.layout.feed) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) // ❌ никем не отменяется
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FeedBinding.bind(view)
scope.launch {
repository.feedUpdates().collect { items ->
binding.list.submitList(items) // ❌ удерживает View после onDestroyView
}
}
}
}
Исправление
Используйте scope, отменяемый вместе с View: viewLifecycleOwner.lifecycleScope. repeatOnLifecycle(STARTED) дополнительно останавливает сбор, когда экран уходит в фон, и возобновляет его при возврате.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FeedBinding.bind(view)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
repository.feedUpdates().collect { items ->
binding.list.submitList(items) // ✅ сбор умирает вместе с View
}
}
}
}
Если собственный scope действительно нужен, отменяйте его сами: override fun onDestroyView() { scope.cancel(); super.onDestroyView() }. Ключ один — время жизни scope не должно превышать время жизни того, что он удерживает.