Android: как перерисовать фрагмент внутри ViewPager2? detach() attach() не работают
Столкнулся с небольшой сложностью. На всякий случай опишу ситуацию в целом, чтобы был понятен контекст.
Ситуация: есть активность, включающая в себя ViewPager, который перелистывает фрагменты. Каждый фрагмент содержит RecyclerView, реализует его адаптер и функцию кликов по элементам (CardView). Содержимое CardView берется из базы данных и в ходе работы активности может меняться в зависимости от действий пользователя. Далее обрисую очень схематично, просто чтобы понятна была суть.
Существует 2 сценария:
1 сценарий: пользователь кликает по CardView -> кликнутый CardView и вместе с ним еще несколько CardView на экране тут же меняют цвет
2 сценарий: пользователь кликает по CardView на текущем фрагменте -> несколько CardView на другом фрагменте меняют цвет
Задача: нужно поддерживать внешний вид всех фрагментов в актуальном состоянии на протяжении работы активности. Иными словами, чтобы пользователь получал результат сразу после клика на CardView, а не после перезапуска активности.
Все дело в том, что нужно не просто перехватить нажатие на CardView и обновить данные конкретно в нем, а заставить обновиться сразу несколько CardView. При этом, могут быть использованы наборы данных из другой таблицы бд (т.е. после нажатие может требоваться перезаполнять RecyclerView элементами CardView с данными уже из другой таблицы).
Согласен, схема запутанная, но в запутанности и суть, поэтому нужно было какое-то достаточно простое рабочее решение, которое не приводило бы к путанице с данными. Чтобы логика обработки данных была первична по отношению к графическому представлению, и не случалось такого, что в бд записывается одно, а на экран выводится другое.
Как я решаю данную задачу: фиксирую действие пользователя, записываю все нужные данные в бд, после чего пересоздаю фрагмент. Такое решение гарантирует, что состояние элементов на экране точно будет соответствовать содержимому бд, и кроме того, оно простое, записывается в несколько строк и подходит для любых сценариев.
Как происходит обновление фрагмента:
public void restartFragment(Fragment fragment) {
if(fragment!=null) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.detach(fragment);
ft.attach(fragment);
ft.commit();
}
}
Схема работает, всех все устраивает, задача решена.
Но! Вернемся к самому началу. Есть ViewPager, и для его работы с фрагментами нужен адаптер. Первоначально использовался класс FragmentPagerAdapter, который уже порядочное время, как deprecated. Код требует поддержки, и встал вопрос о переходе на более актуальный вариант. Официальная документация рекомендует использовать ViewPager2 с адаптером FragmentStateAdapter, что и было сделано.
Проверяем работу. Приложение вызывает:
restartFragment(myFragment);
...и не происходит ничего. После клика на CardView, цвета не меняются => фрагмент не перерисовывается. Чтобы любые изменения вступили в силу, нужно перезапустить активность, только тогда фрагменты перерисуются с новыми данными.
Я начинаю разбираться, нагуглил разные варианты, самый рабочий вот этот:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.detach(fragment);
ft.commit();
ft = getSupportFragmentManager().beginTransaction();
ft.attach(fragment);
ft.commit();
Но даже он не перерисовывает фрагмент, который в данный момент на экране. Чтобы обновленный фрагмент перерисовался, нужно пролистнуть страницу и вернуться обратно. То есть, функция работает, изменения вступают в силу, но визуально ничего не меняется, пока фрагмент не уйдет с экрана.
Так же пробовал выставлять ft.setReorderingAllowed(false) и использовать commitNow() и commitAllowingStateLoss() вместо commit(), но ничего не работает. Понятно, что можно накрутить костылей, типа перелистывать страницу, обновлять, и возвращаться обратно автоматически и т.д., но хочется какое-то человеческое решение.
С этим и связан вопрос. Как теперь обновлять фрагмент, который находится в ViewPager2 непосредственно на экране? Буду крайне благодарен любой информации, советам, ссылкам, хоть на русском, хоть на английском.
UPD
В комментариях Yura Ivanov посоветовал не обновлять фрагмент и использовать notifyItemChanged() или notifyItemRangeChanged(). Выглядит перспективно, вот что получилось:
Структура отображения такая: ViewPager2 внутри него Fragment внутри него RecyclerView
Применение notify...Changed() к адаптеру RecyclerView внутри фрагмента не дает никакого эффекта при любых комбинациях.
Применение notify...Changed() к адаптеру ViewPager2 некоторые результаты дает. Действительно, ViewPager2 обновляется, но вот как это работает:
getSupportFragmentManager().beginTransaction().detach(fragment).commitNow();
getSupportFragmentManager().beginTransaction().attach(fragment).commitNow();
FragmentStateAdapter fSA = (FragmentStateAdapter) viewPager2.getAdapter();
fSA.notifyItemChanged(1);
Как уже писал чуть выше, если вместо attach-detach фрагмента обновлять изменения RecyclerView внутри фрагмента, то ничего не работает (хотя, по логике, как будто бы должно).
НО у этого решения есть 2 проблемы (и возможно они связаны):
Оно работает долго. В старом ViewPager фрагмент обновлялся моментально, а тут нужно ждать около секунды, и для пользователя это выглядит, как будто приложение тормозит.
При обновлении фрагмента (а точнее при notifyItemChanged()) появляется мерцание (типа fadeIn fadeOut), оно раздражает и вероятно с ним связана задержка.
Копнув глубже, я вычитал, что ViewPager2 вроде как построен по логике RecyclerView (по крайней мере, FragmentStateAdapter наследуется от RecyclerView.Adapter), и у RecyclerView при adapter.notify...Changed() тоже есть такая анимация мерцания. И там она убирается вот так:
((SimpleItemAnimator) myRecycler.getItemAnimator()).setSupportsChangeAnimations(false);
У ViewPager2 такого метода нет, и непонятно, как от этой анимации избавиться. На данном этапе решение чисто технически рабочее, но не до конца юзабельное. Осталось избавиться от мерцания с задержкой.