Фрагменты уничтожены / воссозданы с помощью компонентов Jetpack для Android-навигации
Я пытаюсь реализовать навигацию с помощью компонентов архитектуры 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;
}
Есть ли другой, более элегантный способ избежать раздувания макета?
12 ответов
Ян Лейк из Google ответил мне, что мы можем сохранить представление в переменной и вместо того, чтобы надувать новый макет, просто вернуть экземпляр предварительно сохраненного представления в onCreateView()
Leakcanery может показать это как утечку, но это ложное срабатывание..
Вы можете иметь постоянное представление для своего фрагмента с помощью нижеприведенной реализации
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
}
}
MainFragment
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()
}
}
}
Я пытался так, и это работает для меня.
- В этом
ViewModel
поnavGraphViewModels
(Live в области навигации) - Сохраните любое состояние для восстановления в
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
Это поможет ускорить создание фрагмента, и когда вы используете привязку данных и данные модели представления все равно будут сохраняться в представлении в случае обратного нажатия.
просто сделай это:
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
}
если вы следуете расширенному образцу из 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"
Думал, что 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()
}
}
Это тот же ответ, что и предложенный @Shahab Rauf, только дополнительная вещь - это включение привязки данных и реализация onCreateView только в BaseFragment вместо дочерних фрагментов. А также инициализировать navController в onViewCreated () BaseFragment.
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
Если вы просто хотите проверить, был ли воссоздан ваш фрагмент , мы можем просто переопределить onCreate(), который вызывался только один раз за всю жизнь для фрагмента.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Your onetime operation or function call here.
}
Для разработчиков 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
}
}
}
Если вы используете DataBinding, измените только следующее кодирование. это редактируемый ответ Шахаба Рауфа
BaseFragment
open class BaseFragment<T : ViewDataBinding> : Fragment() {
var hasInitializedRootView = false
private var rootView: View? = null
lateinit var binding: T
fun getPersistentView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
layout: Int
): View? {
if (rootView == null) {
binding = DataBindingUtil.inflate(inflater, layout, container, false)
rootView = binding.root
} else {
(rootView?.parent as? ViewGroup)?.removeView(rootView)
}
return rootView
}
}
MainFragment
class MainFragment: BaseFragment<FragmentProfileBinding>() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return getPersistentView(inflater, container, savedInstanceState, R.layout.fragment_profile)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!hasInitializedRootView) {
hasInitializedRootView = true
setListeners()
loadViews()
}
}
private fun setListeners(){
binding.btnShare.setOnClickListener { }
}
private fun loadViews(){}
}
Если вы еще не можете решить проблему с навигационным компонентом, просто используйте общую функцию:
supportFragmentManager.beginTransaction().add(R.id.navFragment, fragment).addToBackStack("").commit()
Он легко сохранит представление в стек, когда вы нажмете на него!