Я пытаюсь реализовать Навигация с компонентами архитектуры Jetpack в своем существующем приложении.
У меня есть приложение с одной активностью, где основной фрагмент (ListFragment) представляет собой список элементов. В настоящее время, когда пользователь нажимает на элемент списка, fragmentTransaction.add(R.id.main, detailFragment) добавляет в стек второй фрагмент. Таким образом, при нажатии кнопки «Назад» DetailFragment отсоединяется и снова отображается ListFragment.
Архитектура навигации делает это автоматически. Вместо добавления нового фрагмента это заменены, поэтому представление фрагмента уничтожается, вызывается onDestroyView(), а onCreateView() вызывается при нажатии кнопки «Назад» для воссоздания представления.
Я понимаю, что это хороший шаблон, используемый с LiveData и ViewModel, чтобы не использовать больше памяти, чем необходимо, но в моем случае это раздражает, потому что список имеет сложную компоновку, а его раздувание требует времени и ресурсов ЦП, а также потому, что мне понадобится чтобы сохранить позицию прокрутки списка и снова прокрутить до той же позиции, которую пользователь оставил фрагмент. Это возможно, но кажется, что должен существовать лучший способ.
Я попытался «сохранить» представление в частном поле фрагмента и повторно использовать его на onCreateView(), если оно уже есть, но это кажется анти-шаблоном.
private View view = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (view == null) {
view = inflater.inflate(R.layout.fragment_list, container, false);
//...
}
return view;
}
Есть ли другой, более элегантный способ избежать повторного раздувания макета?
Нет, кажется, нет другого решения, кроме как воссоздать представление.
Просто чтобы другие пользователи знали, кажется очень простым сохранить и восстановить положение прокрутки списков (recyclerView), например, посмотрите на этот другой вопрос: stackoverflow.com/questions/47110168/…
Мне удалось исправить эту проблему по-другому в моем коде. Вместо того, чтобы каждый раз создавать новую модель представления в onCreateView(), я использовал ViewModelProviders.of(). Это сохранит мою позицию прокрутки без необходимости проходить через saveInstanceStates
Да, это правильный способ работы с ViewModel, но он не решает проблему повторного заполнения представления. Я думаю, что шаблон состоит в том, чтобы повторно раздуть его, отдавая приоритет использованию памяти, а не производительности.
Вы также можете не использовать навигацию для этих двух представлений, но сделать это для других.
ты решил эту проблему? Действительно ли работает решение Ian Lake?
да, это работает, но учтите, что, как сказано, потребляется больше памяти.
@pauminku, можем ли мы поделиться рабочим кодом этой проблемы.
посмотри мой ответ на этот вопрос может тебе поможет Сохранить состояние в компоненте навигации
Ян Лейк из google ответил мне, что мы можем сохранить представление в переменной и вместо раздувать новый макет, просто вернись экземпляр предварительно сохраненный вид на onCreateView()
Источник: https://twitter.com/ianhlake/status/1103522856535638016
утечка может показать это как утечку, но это ложный положительный результат..
Итак, пример кода, который я поставил в вопросе, правильный? Рады узнать. Не могли бы вы дать ссылку на разговор с Яном Лейком? в случае, если это на общественном форуме.
Было бы здорово, если бы у нас был образец. @erluxman
после раздувания макета сохраните его в переменной перед возвратом.. это так просто
Есть ли решение DataBinding для этого? Я пробовал этот подход, но, похоже, он не работает.
Есть ли способ добиться обратного перехода общего элемента без сохранения представления в переменной? обратный переход не работает, потому что представление воссоздается, и сохранение представления в переменной вызывает исключения OOM
Этот ответ является искажением цитируемого твита. Ян прямо упоминает, что это «постоянная трата памяти и ресурсов», что означает, что вам следует избегать этого, если это возможно.
Если вы здесь и хотите избежать, по крайней мере, вызова API (который находится в onViewCreated или onActivityCreated), который вызывается из-за повторного создания фрагмента, переместите такие вызовы в соответствующие ViewModels и вызовите их внутри блока в этом.
@YellowJ проверьте мой ответ, это моя помощь stackoverflow.com/a/67924398/8660721
Я пробовал так, и это работает для меня.
ViewModel с помощью navGraphViewModels (в режиме реального времени в области навигации)ViewModel// fragment.kt
private val vm by navGraphViewModels<VM>(R.id.nav_graph) { vmFactory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Restore state
vm.state?.let {
(recycler.layoutManager as GridLayoutManager).onRestoreInstanceState(it)
}
}
override fun onPause() {
super.onPause()
// Store state
vm.state = (recycler.layoutManager as GridLayoutManager).onSaveInstanceState()
}
// vm.kt
var state:Parcelable? = null
Вы спасли мой день!
Лучшее решение от команды Android: github.com/android/architecture-components-samples/tree/master/…
Это состояние будет потеряно из-за нехватки памяти, вам, вероятно, следует поместить его в SavedStateHandle.
Вы можете иметь постоянное представление для своего фрагмента с помощью реализации ниже
Базовый фрагмент
open class BaseFragment : Fragment(){
var hasInitializedRootView = false
private var rootView: View? = null
fun getPersistentView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?, layout: Int): View? {
if (rootView == null) {
// Inflate the layout for this fragment
rootView = inflater?.inflate(layout,container,false)
} else {
// Do not inflate the layout again.
// The returned View of onCreateView will be added into the fragment.
// However it is not allowed to be added twice even if the parent is same.
// So we must remove rootView from the existing parent view group
// (it will be added back).
(rootView?.getParent() as? ViewGroup)?.removeView(rootView)
}
return rootView
}
}
Главный фрагмент
class MainFragment : BaseFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return getPersistentView(inflater, container, savedInstanceState, R.layout.content_main)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!hasInitializedRootView) {
hasInitializedRootView = true
setListeners()
loadViews()
}
}
}
Это сэкономило мне много времени. Большое спасибо за публикацию этого
Лучший ответ, который я нашел по теме! Спасибо, чемпион
Livedata не работает (не наблюдает) с этим решением.
Далеко лучший и самый простой, но livedata не работает. Я не могу перестать думать, что нет ПРАВИЛЬНОГО подхода к этой проблеме. Согласно трекеру проблем на github навигационного компонента, они все еще говорят, что перерисовка была их целью. плотина
Я получаю эту ошибку: У указанного дочернего элемента уже есть родитель. Сначала вы должны вызвать removeView() для родителя дочернего элемента. Также иногда приложение зависает
Лучший ответ на эту тему: я исправил проблему наблюдения за оперативными данными в моем случае, повторно наблюдая за моей базовой LiveData в getPresistentView() в части else следующим образом (rootView?.parent as? ViewGroup)?.removeView(rootView) viewModel.stateBase.observe(viewLifecycleOwner, Observer { baseRender() }) }, где baseRender() — это абстрактная функция, которую я переопределяю в любом дочернем фрагменте.
можно ли это изменить для использования с привязкой данных?
Это работает, но есть побочный эффект! предположим, что вы находитесь в frag1, затем переходите к frag2 с помощью навигационного компонента, и вы возвращаетесь к frag1 и наблюдаете за результатом из frag2. наблюдатель ничего не увидит, а слушатели вообще не работают, потому что hasInitializedRootView имеет значение true.
Хотя я думаю, что NavigationAdvancedSample — лучшее решение, я также решил эту проблему, используя код @shahab-rauf. Потому что у меня недостаточно времени, чтобы применить это в моем проекте.
abstract class AppFragment: Fragment() {
private var persistingView: View? = null
private fun persistingView(view: View): View {
val root = persistingView
if (root == null) {
persistingView = view
return view
} else {
(root.parent as? ViewGroup)?.removeView(root)
return root
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val p = if (persistingView == null) onCreatePersistentView(inflater, container, savedInstanceState) else persistingView // prevent inflating
if (p != null) {
return persistingView(p)
}
return super.onCreateView(inflater, container, savedInstanceState)
}
protected open fun onCreatePersistentView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (persistingView != null) {
onPersistentViewCreated(view, savedInstanceState)
}
}
protected open fun onPersistentViewCreated(view: View, savedInstanceState: Bundle?) {
logv("onPersistentViewCreated")
}
}
class DetailFragment : AppFragment() {
override fun onCreatePersistentView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// I used data-binding
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_program_detail, container, false)
binding.model = viewModel
binding.lifecycleOwner = this
return binding.root
}
override fun onPersistentViewCreated(view: View, savedInstanceState: Bundle?) {
super.onPersistentViewCreated(view, savedInstanceState)
// RecyclerView bind with adapter
binding.curriculumRecycler.adapter = adapter
binding.curriculumRecycler.apply {
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
}
viewModel.curriculums.observe(viewLifecycleOwner, Observer {
adapter.applyItems(it ?: emptyList())
})
viewModel.refresh()
}
}
если вы следуете расширенному образцу из Google, они используют расширение. Вот его модифицированная версия. В моем случае мне пришлось показывать и скрывать фрагмент, когда они прикреплялись и отсоединялись:
/**
* Manages the various graphs needed for a [BottomNavigationView].
*
* This sample is a workaround until the Navigation Component supports multiple back stacks.
*/
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
if (this.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0, fragmentTag)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
false
} else {
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
if (selectedItemTag != newlySelectedItemTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(
firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
if (!selectedFragment.isAdded) {
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.add(selectedFragment, newlySelectedItemTag)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
hide(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setReorderingAllowed(true)
.commit()
} else {
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.show(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
hide(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setReorderingAllowed(true)
.commit()
}
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupItemReselected(graphIdToTagMap, fragmentManager)
// Handle deep link
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
private fun BottomNavigationView.setupItemReselected(
graphIdToTagMap: SparseArray<String>,
fragmentManager: FragmentManager
) {
setOnNavigationItemReselectedListener { item ->
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
val navController = selectedFragment.navController
// Pop the back stack to the start destination of the current navController graph
navController.popBackStack(
navController.graph.startDestination, false
)
}
}
private fun BottomNavigationView.setupDeepLinks(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent)
&& selectedItemId != navHostFragment.navController.graph.id
) {
this.selectedItemId = navHostFragment.navController.graph.id
}
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.hide(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean,
fragmentTag: String
) {
if (navHostFragment.isAdded) return
fragmentManager.beginTransaction()
.add(navHostFragment, fragmentTag)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
Было бы здорово, если бы ваш ответ также содержал руководство по использованию.
Это тот же ответ, что и предложенный @Shahab Rauf, только дополнительной вещью является включение привязки данных и реализация onCreateView только в BaseFragment вместо дочерних фрагментов. А также инициализация navController в onViewCreated() BaseFragment.
Базовый фрагмент
abstract class BaseFragment<T : ViewDataBinding, VM : BaseViewModel<UiState>> : Fragment() {
protected lateinit var binding: T
var hasInitializedRootView = false
private var rootView: View? = null
protected abstract val mViewModel: ViewModel
protected lateinit var navController: NavController
fun getPersistentView(
inflater: LayoutInflater?,
container: ViewGroup?,
savedInstanceState: Bundle?,
layout: Int
): View? {
if (rootView == null) {
binding = DataBindingUtil.inflate(inflater!!, getFragmentView(), container, false)
//setting the viewmodel
binding.setVariable(BR.mViewModel, mViewModel)
// Inflate the layout for this fragment
rootView = binding.root
} else {
// Do not inflate the layout again.
// The returned View of onCreateView will be added into the fragment.
// However it is not allowed to be added twice even if the parent is same.
// So we must remove rootView from the existing parent view group
// (it will be added back).
(rootView?.getParent() as? ViewGroup)?.removeView(rootView)
}
return rootView
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? = getPersistentView(inflater, container, savedInstanceState, getFragmentView())
//this method is used to get the fragment layout file
abstract fun getFragmentView(): Int
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
}
}
HomeFragment (любой фрагмент, расширяющий BaseFragment)
class HomeFragment : BaseFragment<HomeFragmentBinding, HomeViewModel>(),
RecycleViewClickListener {
override val mViewModel by viewModel<HomeViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!hasInitializedRootView) {hasInitializedRootView = true
setListeners()
loadViews()
--------
}
Привет, проблема исправлена в последней версии 2.4.0-alpha01, теперь есть официальная поддержка навигации по нескольким обратным стекам.
Проверьте ссылку: https://developer.android.com/jetpack/androidx/releases/navigation#version_240_2
Это отличная функция, но я думаю, что она никоим образом не помогает решить проблему: избегайте повторного создания представления.
Это поможет ускорить создание фрагмента, а когда вы используете привязку данных и viewModel, данные все равно будут сохранены в представлении в случае обратного нажатия.
просто сделайте это:
lateinit var binding: FragmentConnectBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
if (this::binding.isInitialized) {
binding
} else {
binding = FragmentConnectBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.model = connectModel
binding.lifecycleOwner = viewLifecycleOwner
viewModel.buildAllProfiles()
// do what ever you need to do in first creation
}
setupObservers()
return binding.root
}
Для разработчиков Java, как описано и объединено из приведенных выше ответов,
Базовый фрагмент.java
public abstract class BaseFragment<T extends ViewDataBinding, V extends BaseViewModel> extends Fragment {
private View mRootView;
private T mViewDataBinding;
private V mViewModel;
public boolean hasInitializedRootView = false;
private View rootView = null;
public View getPersistentView(LayoutInflater layoutInflater, ViewGroup container, Bundle saveInstanceState, int layout) {
if (rootView == null) {
mViewDataBinding = DataBindingUtil.inflate(layoutInflater, layout, container, false);
mViewDataBinding.setVariable(getBindingVariable(),mViewModel);
rootView = mViewDataBinding.getRoot();
}else {
// Do not inflate the layout again.
// The returned View of onCreateView will be added into the fragment.
// However it is not allowed to be added twice even if the parent is same.
// So we must remove rootView from the existing parent view group
// (it will be added back).
ViewGroup viewGroup = (ViewGroup) rootView.getParent();
if (viewGroup != null){
viewGroup.removeView(rootView);
}
}
return rootView;
}
}
Реализовать в своем фрагменте как,
@AndroidEntryPoint
public class YourFragment extends BaseFragment<YourFragmentBinding, YourViewModel> {
@Override
public View onCreateView(@NonNull @NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return getPersistentView(inflater, container, savedInstanceState, getLayoutId());
}
@Override
public void onViewCreated(@NonNull @NotNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!hasInitializedRootView){
hasInitializedRootView = true;
// do your work here
}
}
}
Вы нашли ответ на это? Я сейчас застрял в той же ситуации