【问题标题】:Fragments destroyed / recreated with Jetpack's Android Navigation components使用 Jetpack 的 Android 导航组件销毁/重新创建的片段
【发布时间】:2019-07-02 00:37:50
【问题描述】:

我正在尝试在我现有的应用中实现Navigation with Jetpack's architecture components

我有一个单一的活动应用程序,其中主要片段 (ListFragment) 是项目列表。目前,当用户点击列表项时,fragmentTransaction.add(R.id.main, detailFragment) 会将第二个片段添加到堆栈中。因此,当按下返回时,DetailFragment 被分离,ListFragment 再次显示。

使用导航架构,这是自动处理的。它不是添加新的片段而是replaced,因此片段视图被销毁,onDestroyView() 被调用,onCreateView() 在按下返回以重新创建视图时被调用。

我知道这是与 LiveDataViewModel 一起使用的一个很好的模式,以避免使用不必要的内存,但在我的情况下,这很烦人,因为列表的布局很复杂,并且膨胀它会耗费时间和 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)的滚动位置似乎很容易,例如看看这个其他问题:stackoverflow.com/questions/47110168/…
  • 我设法在我的代码中以不同的方式解决了这个问题。我不是每次都在 onCreateView() 中创建一个新的视图模型,而是使用 ViewModelProviders.of()。这将保留我的滚动位置,而无需通过 savedInstanceStates
  • 是的,这是使用 ViewModel 的正确方法,但它不能解决重新膨胀视图的问题。我认为模式是重新膨胀它,优先考虑内存使用而不是性能。

标签: android android-fragments android-jetpack android-architecture-navigation android-jetpack-navigation


【解决方案1】:
来自 google 的

Ian Lake 回复我说我们可以将视图存储在变量中 并且 而不是 膨胀一个新的布局,只需在onCreateView()

上返回 预存储视图 的实例

来源:https://twitter.com/ianhlake/status/1103522856535638016

Leakcanary 可能会将其显示为泄漏,但其误报..

【讨论】:

  • 那么,我在问题中提出的代码示例是否正确?很高兴知道。你能把与 Ian Lake 的对话的链接放上去吗?如果它在公共论坛上。
  • 如果我们能有一个样本,那将是一个很大的帮助。 @erluxman
  • 这有 DataBinding 解决方案吗?我尝试了这种方法,但似乎不起作用。
  • 这个答案是对引用推文的歪曲。 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

【讨论】:

【解决方案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
      
              }
      
          }
      
      
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-27
        • 2022-06-16
        • 2020-08-13
        • 1970-01-01
        • 1970-01-01
        • 2020-01-23
        • 1970-01-01
        相关资源
        最近更新 更多