Хранение состояний переписчика в фрагменте с библиотекой подкачки и компонентом архитектуры навигации
Я использую 2 компонента реактивного ранца: библиотека подкачки и навигация.
В моем случае у меня есть 2 фрагмента: ListMoviesFragment и MovieDetailFragment
когда я прокручиваю определенное расстояние и щелкаю по элементу фильма в обзоре переработчика, MovieDetailFragment присоединяется, а ListMoviesFragment находится в backstack. Затем я нажимаю кнопку "Назад", фрагмент ListMovies возвращается из backstack.
Точка - это позиция прокрутки, а элементы ListMoviesFrament сбрасываются точно так же, как при первом подключении к его активности. Итак, как сохранить состояние переработчика, чтобы предотвратить это?
Другими словами, как сохранить состояния целого фрагмента, например скрыть / показать фрагмент с FragmentTransaction традиционным способом, но современным способом (навигация)
Мои образцы кодов:
макет фрагмента:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="net.karaokestar.app.SplashFragment">
<TextView
android:id="@+id/singer_label"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Ca sĩ"
android:textColor="@android:color/white"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/btn_game_more"
android:layout_centerVertical="true"
android:background="@drawable/shape_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="@dimen/header_margin_bottom_list"
android:textStyle="bold"
android:padding="@dimen/header_padding_size"
android:textAllCaps="true"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_singers"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Фрагмент кода котлина:
package net.karaokestar.app
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_splash.*
import net.karaokestar.app.home.HomeSingersAdapter
import net.karaokestar.app.home.HomeSingersRepository
class SplashFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_splash, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val singersAdapter = HomeSingersAdapter()
singersAdapter.setOnItemClickListener{
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
}
list_singers.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
list_singers.setHasFixedSize(true)
list_singers.adapter = singersAdapter
getSingersPagination().observe(viewLifecycleOwner, Observer {
singersAdapter.submitList(it)
})
}
fun getSingersPagination() : LiveData<PagedList<Singer>> {
val repository = HomeSingersRepository()
val pagedListConfig = PagedList.Config.Builder().setEnablePlaceholders(true)
.setPageSize(Configurations.SINGERS_PAGE_SIZE).setPrefetchDistance(Configurations.SINGERS_PAGE_SIZE).build()
return LivePagedListBuilder(repository, pagedListConfig).build()
}
}
11 ответов
Поскольку вы используете NavController, вы не можете сохранять вид фрагмента списка при навигации.
Вместо этого вы можете сохранить данные RecyclerView и использовать эти данные при воссоздании представления после обратной навигации.
Проблема в том, что ваш адаптер и singersPagination создаются заново каждый раз, когда создается представление фрагмента. Вместо,
Переехать
singersAdapter
в поле:private val singersAdapter = HomeSingersAdapter()
Переместить эту часть в
onAttach
getSingersPagination().observe(viewLifecycleOwner, Observer { singersAdapter.submitList(it) })
Вызов
retainInstance(true)
вonAttach
, Таким образом, даже изменения конфигурации не будут сбрасывать состояние.
На фрагменте onSaveinstanceState сохраните информацию о макете просмотра переработчика:
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_LAYOUT, myRecyclerView.getLayoutManager().onSaveInstanceState());
}
и на onActivityCreated восстановите положение прокрутки:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
myRecyclerView.getLayoutManager().onRestoreInstanceState(
savedInstanceState.getParcelable(KEY_LAYOUT));
}
}
Попробуйте выполнить следующие действия:
- Инициализируйте свой адаптер в
onCreate
вместо того . Сохраните инициализацию один раз и прикрепите ееonCreateView
илиonViewCreated
. - Не возвращайте новый экземпляр вашего pagedList из
getSingersPagination()
метод каждый раз, вместо этого сохраните его в сопутствующем объекте илиViewModel
(предпочтительно) и использовать повторно.
Посмотрите следующий код, чтобы получить общее представление о том, что делать:
class SingersViewModel: ViewModel() {
private var paginatedLiveData: MutableLiveData<YourType>? = null;
fun getSingersPagination(): LiveData<YourType> {
if(paginatedLiveData != null)
return paginatedLiveData;
else {
//create a new instance, store it in paginatedLiveData, then return
}
}
}
Причины вашей проблемы:
- Вы каждый раз подключаете новый адаптер, поэтому он поднимается вверх.
- Вы каждый раз создаете новый постраничный список, поэтому он перескакивает наверх, думая, что все данные новые.
Пример фрагмента кода, который вы разместили, не соответствует описанию проблемы, я думаю, это всего лишь иллюстрация того, что вы делаете в своем приложении.
В примере кода фактическая навигация (транзакция фрагмента) скрыта за этой строкой:
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
Ключ в том, как прикреплен фрагмент данных.
Основываясь на вашем описании, ваш фрагмент данных, вероятно, прикреплен с FragmentTransaction.replace(int containerViewId, Fragment fragment)
, На самом деле, это сначала удалить текущий фрагмент списка, а затем добавить фрагмент детали в контейнер. В этом случае состояние фрагмента списка не сохраняется. Когда вы нажимаете кнопку назад, onViewCreated
фрагмента списка будет запущен снова.
Чтобы сохранить состояние вашего фрагмента списка, вы должны использовать FragmentTransaction.add(int containerViewId, Fragment fragment)
вместо replace
, Таким образом, фрагмент списка остается там, где он есть, и он "покрывается" фрагментом детали. Когда вы нажимаете кнопку назад, onViewCreated
не будет вызван, так как вид фрагмента не будет уничтожен.
Каждый раз, когда фрагмент возвращается снова, он получает новый PagedList, это приводит к тому, что адаптер представляет новые данные, поэтому вы увидите, что представление ресайклера перемещается наверх.
Переместив создание PagedList в viewModel и отметив, чтобы вернуть существующий PagedList вместо создания нового, вы решите проблему. Конечно, в зависимости от вашего бизнеса приложений может потребоваться создание нового PagedList, но вы можете полностью его контролировать. (например: при нажатии для обновления, когда пользователь вводит данные для поиска...)
Это так просто, используйте только этот метод cachedIn()
fun getAllData() {
viewModelScope.launch {
_response.value = repository.getPagingData().cachedIn(viewModelScope)
}
}
У меня была такая же проблема с моим представлением корзины с использованием библиотеки подкачки. Мой список списка всегда будет перезагружаться и прокручиваться вверх, когда нажимается кнопка навигации вверх из моего фрагмента сведений. Взяв за основу точку зрения @Janos Breuer, я переместил инициализацию моей модели представления и первоначальный вызов списка (репозитория) в метод фрагмента onCreate(), который вызывается только один раз в жизненном цикле фрагмента.
onCreate() Система вызывает этот метод при создании фрагмента. Вы должны инициализировать основные компоненты фрагмента, которые вы хотите сохранить, когда фрагмент приостановлен или остановлен, а затем возобновлен.
Что нужно помнить при решении следующих проблем:
Чтобы операционная система могла автоматически обрабатывать восстановление состояния представления, необходимо указать идентификатор. Я считаю, что это не так (потому что вы должны идентифицировать RecyclerView, чтобы связать данные).
Вы должны использовать правильный FragmentManager. У меня была такая же проблема с вложенным фрагментом, и все, что мне нужно было сделать, это использовать ChildFragmentManager.
SaveInstanceState() запускается действием. Пока активность жива, она не вызывается.
Вы можете использовать ViewModels для сохранения состояния и восстановления его в onViewCreated()
Наконец, контроллер навигации создает новый экземпляр фрагмента каждый раз, когда мы переходим к месту назначения. Очевидно, что это плохо работает с сохранением постоянного состояния. Пока не будет официального исправления, вы можете проверить следующий обходной путь, который поддерживает присоединение / отсоединение, а также различные backstack для каждой вкладки. Даже если вы не используете BottomNavigationView, вы можете использовать его для реализации механизма присоединения / отсоединения.
Простые шаги...
Дайте
id
для всех представлений, которые вам нужно поддерживать в обратном стеке фрагментов. НравитьсяScrollview, root layout, Recyclerview
...Свойства, которые должны содержать значения, инициализируются в фрагменте . Как адаптер, считать... кроме ссылок на просмотры .
Если вы устанавливаете наблюдателей, используйте lifecycleowner как
this@yourFragment
вместо viewLifecycleOwner.
Вот и все. Фрагмент вызывается только один раз, поэтому установка свойств на
onCreate()
останется в памяти до фрагмента
onDestroy()
вызывается (не onDestroyView).
Весь код ссылки на представление должен быть там после onCreateView() и до onDestroyView().
Кстати, если возможно, вы можете сохранить свойства в ViewModel, которые будут там даже при изменении конфигурации фрагмента, где все переменные экземпляра будут уничтожены.
надеюсь, что эта ссылка поможет:PagingDataAdapter.refresh() не работает после навигации по фрагменту
Что ж ... Вы можете проверить, инициализировано ли представление или нет (для котлина)
Инициализировать переменную просмотра
private lateinit var _view: View
В onCreateView проверьте, инициализировано ли представление или нет
if (!this::_view.isInitialized) {
return inflater.inflate(R.layout.xyz, container, false)
}
return _view
И в onViewCreated просто проверьте это
if (!this::_view.isInitialized) {
_view = view
// ui related methods
}
Это решение для onreplace () .... но если данные не меняются при обратном нажатии, вы можете использовать метод фрагмента add ().
Здесь будет вызван oncreateview, но ваши данные не будут перезагружены.
Извините за опоздание. У меня была аналогичная проблема, и я нашел решение (возможно, не лучшее). В моем случае я адаптировал изменения ориентации. Вам нужно сохранить и получить состояние LayoutManager И последний ключ адаптера.
В действии / фрагменте, показывающем ваш RecyclerView, объявите 2 переменные:
private Parcelable layoutState;
private int lastKey;
В onSaveInstanceState:
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if(adapter != null) {
lastKey = (Integer)adapter.getCurrentList().getLastKey();
outState.putInt("lastKey",lastKey);
outState.putParcelable("layoutState",dataBinding.customRecyclerView
.getLayoutManager().onSaveInstanceState());
}
}
В onCreate:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//.... setContentView etc...
if(savedInstanceState != null) {
layoutState = savedInstanceState.getParcelable("layoutState");
lastKey = savedInstanceState.getInt("lastKey",0);
}
dataBinding.customRecyclerView
.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(lastKey != 0) {
dataBinding.customRecyclerView.scrollToPosition(lastKey);
Log.d(LOG_TAG, "list restored; last list position is: " +
((Integer)adapter.getCurrentList().getLastKey()));
lastKey = 0;
if(layoutState != null) {
dataBinding.customRecyclerView.getLayoutManager()
.onRestoreInstanceState(layoutState);
layoutState = null;
}
}
}
});
}
Вот и все. Теперь ваш RecyclerView должен правильно восстановиться.