使用 Jetpack 的 Android 导航组件销毁/重新创建的片段
Posted
技术标签:
【中文标题】使用 Jetpack 的 Android 导航组件销毁/重新创建的片段【英文标题】:Fragments destroyed / recreated with Jetpack's Android Navigation components 【发布时间】:2019-07-02 00:37:50 【问题描述】:我正在尝试在我现有的应用中实现Navigation with Jetpack's architecture components。
我有一个单一的活动应用程序,其中主要片段 (ListFragment
) 是项目列表。目前,当用户点击列表项时,fragmentTransaction.add(R.id.main, detailFragment)
会将第二个片段添加到堆栈中。因此,当按下返回时,DetailFragment
被分离,ListFragment
再次显示。
使用导航架构,这是自动处理的。它不是添加新的片段而是replaced,因此片段视图被销毁,onDestroyView()
被调用,onCreateView()
在按下返回以重新创建视图时被调用。
我知道这是与 LiveData 和 ViewModel 一起使用的一个很好的模式,以避免使用不必要的内存,但在我的情况下,这很烦人,因为列表的布局很复杂,并且膨胀它会耗费时间和 CPU ,也是因为我需要保存列表的滚动位置并再次滚动到用户离开片段的同一位置。这是可能的,但似乎应该存在更好的方法。
我尝试将视图“保存”在片段的私有字段中,并在 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)的滚动位置似乎很容易,例如看看这个其他问题:***.com/questions/47110168/… 我设法在我的代码中以不同的方式解决了这个问题。我不是每次都在 onCreateView() 中创建一个新的视图模型,而是使用 ViewModelProviders.of()。这将保留我的滚动位置,而无需通过 savedInstanceStates 是的,这是使用 ViewModel 的正确方法,但它不能解决重新膨胀视图的问题。我认为模式是重新膨胀它,优先考虑内存使用而不是性能。 【参考方案1】: 来自 google 的Ian Lake 回复我说我们可以将视图存储在变量中 并且 而不是 膨胀一个新的布局,只需在onCreateView()
来源:https://twitter.com/ianhlake/status/1103522856535638016
Leakcanary 可能会将其显示为泄漏,但其误报..
【讨论】:
那么,我在问题中提出的代码示例是否正确?很高兴知道。你能把与 Ian Lake 的对话的链接放上去吗?如果它在公共论坛上。 如果我们能有一个样本,那将是一个很大的帮助。 @erluxman 这有 DataBinding 解决方案吗?我尝试了这种方法,但似乎不起作用。 github.com/square/leakcanary/issues/1656 另有说明。 这个答案是对引用推文的歪曲。 Ian 明确提到这是“不断浪费内存和资源”,这意味着您应该尽可能避免这样做。【参考方案2】:您可以通过以下实现为您的片段提供持久视图
BaseFragment
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()
Source
【讨论】:
这为我节省了很多时间。非常感谢您发布此内容 我在该主题上找到的最佳答案!谢谢冠军 Livedata 在此解决方案中不起作用(未观察到)。 目前最好也最简单,但 livedata 不起作用。我不能停止认为这个问题没有适当的方法。根据导航组件 github 上的问题跟踪器,他们仍然说重绘是他们的意图。大坝。 我收到这个错误:指定的孩子已经有一个父母。您必须首先在孩子的父母上调用 removeView()。此外,有时应用程序会冻结【参考方案3】:我试过这样,它对我有用。
由navGraphViewModels
初始化ViewModel
(在导航范围内直播)
将任何要恢复的状态存储在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 中。【参考方案4】:如果您关注 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"
【讨论】:
如果您的答案也包含使用指南,那就太好了。【参考方案5】:这将有助于加快片段的创建,并且当您使用数据绑定和 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
【讨论】:
【参考方案6】:虽然我认为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()
【讨论】:
我重新阅读了这个问题 2 次,没有发现任何关于绑定的内容。更多的绑定甚至没有声明为变量。它只是不知从何而来......以及这种绑定如何帮助防止破坏片段?【参考方案7】:这与@Shahab Rauf 建议的答案相同,只是额外的事情是包含数据绑定并仅在 BaseFragment 而不是子片段中实现 onCreateView。并且还在BaseFragment的onViewCreated()中初始化navController。
基础片段
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 的 Fragment)
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()
--------
【讨论】:
【参考方案8】:您好,该问题已在最新版本 2.4.0-alpha01 中得到修复,现在正式支持多退栈导航
查看链接: https://developer.android.com/jetpack/androidx/releases/navigation#version_240_2
【讨论】:
这是一个很棒的功能,但我认为它对相关问题没有任何帮助:避免重新创建视图。【参考方案9】:对于 Java 开发人员,如上述答案所述并结合,
BaseFragment.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
【讨论】:
以上是关于使用 Jetpack 的 Android 导航组件销毁/重新创建的片段的主要内容,如果未能解决你的问题,请参考以下文章
是否可以使用 Android 导航架构组件(Android Jetpack)有条件地设置 startDestination?
Android Jetpack导航组件——Navigation的使用
踩坑!Android Jetpack组件间库之Navigation