使用导航架构组件添加(而不是替换)片段。

83

我有一个包含产品列表片段和其他许多片段的活动,并尝试使用架构组件导航控制器。

问题是:它会替换(起始目标)产品列表片段,但我不希望在用户点击返回按钮时重新加载列表。

如何将片段事务设置为添加而不是替换?


1
你需要提供更多关于“transaction”替换“Start Destination”的信息。当你导航到其他活动时,它是否被替换,或者当你返回到“Start Destination”时,列表是否重新加载? - Bam
很遗憾,目前(至少在 2.0.0 版本中)似乎无法实现。如果您查看 androidx.navigation.fragment.FragmentNavigator#navigate 方法,就会看到它内部使用了 ft.replace(mContainerId, frag);。我认为这里唯一的选择是作为目标启动一个新的活动。 - Kyrylo Zapylaiev
4
这太可怕了!今天我遇到一个问题,当从不同的“Fragment”返回时,“WebView”总是重新加载!而且我看不到任何防止它发生的办法。 - xinaiz
2
@UtkuKUTLU 页面未找到。 - Master Zzzing
https://issuetracker.google.com/issues/127932815 - rafaelasguerra
显示剩余2条评论
8个回答

16

如果你想要添加一个类似对话框的片段而不是替换Android导航组件,那么可以使用此方法,但需要使用版本2.1.0以上的导航组件。

解决方案

您还可以查看"对话框目标"。


非常好的解决方案!! - Jatin
问题在于,由于将其作为<dialog>,它不再遵循动画。 - hitch45
@hitch45 不正确,你可以使用代码显式地设置动画。 - Bravo

7

我遇到了同样的问题,在等待 add 和其他用于片段事务的选项时,我实现了此解决方法以在返回时保留状态。

我只是添加了一个检查,如果存在绑定(binding),那么就恢复先前的状态,对于网络调用也是一样,如果数据已经存在于视图模型中,则不进行网络重新获取。经过测试,它可以正常工作。

编辑: 对于RecyclerView,我相信它会自动返回到你离开片段之前列表的相同状态,但在 onSavedInstanceState 中存储位置也是可行的。

  private lateinit var binding: FragmentSearchResultsBinding

  override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        viewModel =
            ViewModelProviders.of(this, mViewModelFactory).get(SearchResultsViewModel::class.java)
        return if (::binding.isInitialized) {
            binding.root
        } else {
            binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)

            with(binding) {
               //some stuff
                root
            }
        }
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //reload only if search results are empty
        if (viewModel.searchResults.isEmpty()) {
           args.searchKey.let {
                binding.toolbarHome.title = it
                viewModel.onSearchResultRequest(it)
            }
        }
    }

5

您需要重写NavHostFragment的createFragmentNavigator方法,并返回YourFragmentNavigator

YourFragmentNavigator必须重写FragmentNavigator的navigate方法。

将FragmentNavigator的navigate方法复制并粘贴到您的YourFragmentNavigator中。

在navigate方法中,将ft.replace(mContainerId, frag);行更改为

if (fragmentManager.fragments.size <= 0) {
    ft.replace(containerId, frag)
} else {
    ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
    ft.add(containerId, frag)
}

解决方案将如下所示:
class YourNavHostFragment : NavHostFragment() {
override fun createFragmentNavigator(): Navigator<...> {
    return YourFragmentNavigator(...)
}}

....

class YourFragmentNavigator(...) : FragmentNavigator(...) {

override fun navigate(...){
    ....
    if (fragmentManager.fragments.size <= 0) {
        ft.replace(containerId, frag)
    } else {
        ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
        ft.add(containerId, frag)
     }
     ....
}}

在你的XML中使用YourNavHostFragment

1
我看了一下,但是你的子类将无法访问FragmentNavigator私有字段mBackStack(至少对于Navigation Components 2.3.0)。要做到这一点,您需要将此字段添加到您的子类中,并创建覆盖onSaveState、onRestoreState和popBackStack的方法来引用mBackStack。并且要跟上Navigation Components的新版本,以确保您的覆盖仍然有效。 - Eric
不要忘记添加 ft.addToBackStack(entry.id),这样 popBackStack 就可以稍后弹出顶部的 fragment。 - John

3

我遇到了同样的问题,但是我更新了我的代码来使用LiveData和ViewModel。

当您按下返回键时,ViewModel不会再次被创建,因此您的数据得以保留。

确保在ViewModel的init方法中进行API调用,这样它只会在创建ViewModel时发生一次。


1
请注意,我建议更新整个架构为MVVM。我提供的代码是一个基于MVVM架构的应用程序。https://github.com/cult-mentality/thank_you_tree_app - CULT_MENTALITY
我选择了这个解决方案。非常感谢! - iroiroys

2

只需复制FragmentNavigator的代码(300行),并将replace()替换为add()。对我来说,这是目前最好的解决方案。

@Navigator.Name("fragment")
public class CustomFragmentNavigator extends 
Navigator<...> {
    ...

    public NavDestination navigate(...) {
        ...
        ft.add(mContainerId, frag);
        ...
    }

    ...
}

1
你能展示一下这个类的用法吗? - OhhhThatVarun

1

您可以将这些类用作自定义的NavHostFragmentNavigator

NavHostFragment

class YourNavHostFragment : NavHostFragment() {

    override fun onCreateNavHostController(navHostController: NavHostController) {
        /**
         * Done this on purpose.
         */
        if (false) {
            super.onCreateNavHostController(navHostController)
        }
        val containerId = if (id != 0 && id != View.NO_ID) id else R.id.nav_host_fragment_container
        navController.navigatorProvider += YourFragmentNavigator(requireContext(), parentFragmentManager, containerId)
        navController.navigatorProvider += DialogFragmentNavigator(requireContext(), childFragmentManager)
    }
}

导航器

@Navigator.Name("fragment")
class YourFragmentNavigator(private val context: Context, private val fragmentManager: FragmentManager, private val containerId: Int) : Navigator<YourFragmentNavigator.Destination>() {

    private val savedIds = mutableSetOf<String>()

    /**
     * {@inheritDoc}
     *
     * This method must call
     * [FragmentTransaction.setPrimaryNavigationFragment]
     * if the pop succeeded so that the newly visible Fragment can be retrieved with
     * [FragmentManager.getPrimaryNavigationFragment].
     *
     * Note that the default implementation pops the Fragment
     * asynchronously, so the newly visible Fragment from the back stack
     * is not instantly available after this call completes.
     */
    override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
        if (fragmentManager.isStateSaved) {
            Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already saved its state")
            return
        }
        if (savedState) {
            val beforePopList = state.backStack.value
            val initialEntry = beforePopList.first()
            // Get the set of entries that are going to be popped
            val poppedList = beforePopList.subList(
                beforePopList.indexOf(popUpTo),
                beforePopList.size
            )
            // Now go through the list in reversed order (i.e., started from the most added)
            // and save the back stack state of each.
            for (entry in poppedList.reversed()) {
                if (entry == initialEntry) {
                    Log.i(TAG, "FragmentManager cannot save the state of the initial destination $entry")
                } else {
                    fragmentManager.saveBackStack(entry.id)
                    savedIds += entry.id
                }
            }
        } else {
            fragmentManager.popBackStack(popUpTo.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
        }
        state.pop(popUpTo, savedState)
    }

    override fun createDestination(): Destination {
        return Destination(this)
    }

    /**
     * Instantiates the Fragment via the FragmentManager's
     * [androidx.fragment.app.FragmentFactory].
     *
     * Note that this method is **not** responsible for calling
     * [Fragment.setArguments] on the returned Fragment instance.
     *
     * @param context Context providing the correct [ClassLoader]
     * @param fragmentManager FragmentManager the Fragment will be added to
     * @param className The Fragment to instantiate
     * @param args The Fragment's arguments, if any
     * @return A new fragment instance.
     */
    @Suppress("DeprecatedCallableAddReplaceWith")
    @Deprecated(
        """Set a custom {@link androidx.fragment.app.FragmentFactory} via
      {@link FragmentManager#setFragmentFactory(FragmentFactory)} to control
      instantiation of Fragments."""
    )
    fun instantiateFragment(context: Context, fragmentManager: FragmentManager, className: String, args: Bundle?): Fragment {
        return fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
    }

    /**
     * {@inheritDoc}
     *
     * This method should always call
     * [FragmentTransaction.setPrimaryNavigationFragment]
     * so that the Fragment associated with the new destination can be retrieved with
     * [FragmentManager.getPrimaryNavigationFragment].
     *
     * Note that the default implementation commits the new Fragment
     * asynchronously, so the new Fragment is not instantly available
     * after this call completes.
     */
    override fun navigate(entries: List<NavBackStackEntry>, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
        if (fragmentManager.isStateSaved) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already saved its state")
            return
        }
        for (entry in entries) {
            navigate(entry, navOptions, navigatorExtras)
        }
    }

    private fun navigate(entry: NavBackStackEntry, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
        val backStack = state.backStack.value
        val initialNavigation = backStack.isEmpty()
        val restoreState = (navOptions != null && !initialNavigation && navOptions.shouldRestoreState() && savedIds.remove(entry.id))
        if (restoreState) {
            // Restore back stack does all the work to restore the entry
            fragmentManager.restoreBackStack(entry.id)
            state.push(entry)
            return
        }
        val destination = entry.destination as Destination
        val args = entry.arguments
        var className = destination.className
        if (className[0] == '.') {
            className = context.packageName + className
        }
        val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
        frag.arguments = args
        val ft = fragmentManager.beginTransaction()
        var enterAnim = navOptions?.enterAnim ?: -1
        var exitAnim = navOptions?.exitAnim ?: -1
        var popEnterAnim = navOptions?.popEnterAnim ?: -1
        var popExitAnim = navOptions?.popExitAnim ?: -1
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = if (enterAnim != -1) enterAnim else 0
            exitAnim = if (exitAnim != -1) exitAnim else 0
            popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
            popExitAnim = if (popExitAnim != -1) popExitAnim else 0
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
        }
        if (fragmentManager.fragments.size <= 0) {
            ft.replace(containerId, frag)
        } else {
            ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
            ft.add(containerId, frag)
        }
        @IdRes val destId = destination.id
        // TODO Build first class singleTop behavior for fragments
        val isSingleTopReplacement = (navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && backStack.last().destination.id == destId)
        val isAdded = when {
            initialNavigation -> {
                true
            }
            isSingleTopReplacement -> {
                // Single Top means we only want one instance on the back stack
                if (backStack.size > 1) {
                    // If the Fragment to be replaced is on the FragmentManager's
                    // back stack, a simple replace() isn't enough so we
                    // remove it from the back stack and put our replacement
                    // on the back stack in its place
                    fragmentManager.popBackStack(entry.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
                    ft.addToBackStack(entry.id)
                }
                false
            }
            else -> {
                ft.addToBackStack(entry.id)
                true
            }
        }
        if (navigatorExtras is Extras) {
            for ((key, value) in navigatorExtras.sharedElements) {
                ft.addSharedElement(key, value)
            }
        }
        ft.setReorderingAllowed(true)
        ft.commit()
        // The commit succeeded, update our view of the world
        if (isAdded) {
            state.push(entry)
        }
    }

    override fun onSaveState(): Bundle? {
        if (savedIds.isEmpty()) {
            return null
        }
        return bundleOf(KEY_SAVED_IDS to ArrayList(savedIds))
    }

    override fun onRestoreState(savedState: Bundle) {
        val savedIds = savedState.getStringArrayList(KEY_SAVED_IDS)
        if (savedIds != null) {
            this.savedIds.clear()
            this.savedIds += savedIds
        }
    }

    /**
     * NavDestination specific to [FragmentNavigator]
     *
     * Construct a new fragment destination. This destination is not valid until you set the
     * Fragment via [setClassName].
     *
     * @param fragmentNavigator The [FragmentNavigator] which this destination will be associated
     * with. Generally retrieved via a [NavController]'s [NavigatorProvider.getNavigator] method.
     */
    @NavDestination.ClassType(Fragment::class)
    open class Destination
    constructor(fragmentNavigator: Navigator<out Destination>) : NavDestination(fragmentNavigator) {

        /**
         * Construct a new fragment destination. This destination is not valid until you set the
         * Fragment via [setClassName].
         *
         * @param navigatorProvider The [NavController] which this destination
         * will be associated with.
         */
        //public constructor(navigatorProvider: NavigatorProvider) : this(navigatorProvider.getNavigator(FragmentNavigator::class.java))

        @CallSuper
        public override fun onInflate(context: Context, attrs: AttributeSet) {
            super.onInflate(context, attrs)
            context.resources.obtainAttributes(attrs, R.styleable.FragmentNavigator).use { array ->
                val className = array.getString(R.styleable.FragmentNavigator_android_name)
                if (className != null) setClassName(className)
            }
        }

        /**
         * Set the Fragment class name associated with this destination
         * @param className The class name of the Fragment to show when you navigate to this
         * destination
         * @return this [Destination]
         */
        fun setClassName(className: String): Destination {
            _className = className
            return this
        }

        private var _className: String? = null

        /**
         * The Fragment's class name associated with this destination
         *
         * @throws IllegalStateException when no Fragment class was set.
         */
        val className: String
            get() {
                checkNotNull(_className) { "Fragment class was not set" }
                return _className as String
            }

        override fun toString(): String {
            val sb = StringBuilder()
            sb.append(super.toString())
            sb.append(" class=")
            if (_className == null) {
                sb.append("null")
            } else {
                sb.append(_className)
            }
            return sb.toString()
        }

        override fun equals(other: Any?): Boolean {
            if (other == null || other !is Destination) return false
            return super.equals(other) && _className == other._className
        }

        override fun hashCode(): Int {
            var result = super.hashCode()
            result = 31 * result + _className.hashCode()
            return result
        }
    }

    /**
     * Extras that can be passed to FragmentNavigator to enable Fragment specific behavior
     */
    class Extras internal constructor(sharedElements: Map<View, String>) :
        Navigator.Extras {
        private val _sharedElements = LinkedHashMap<View, String>()

        /**
         * The map of shared elements associated with these Extras. The returned map
         * is an [unmodifiable][Map] copy of the underlying map and should be treated as immutable.
         */
        val sharedElements: Map<View, String>
            get() = _sharedElements.toMap()

        /**
         * Builder for constructing new [Extras] instances. The resulting instances are
         * immutable.
         */
        class Builder {
            private val _sharedElements = LinkedHashMap<View, String>()

            /**
             * Adds multiple shared elements for mapping Views in the current Fragment to
             * transitionNames in the Fragment being navigated to.
             *
             * @param sharedElements Shared element pairs to add
             * @return this [Builder]
             */
            fun addSharedElements(sharedElements: Map<View, String>): Builder {
                for ((view, name) in sharedElements) {
                    addSharedElement(view, name)
                }
                return this
            }

            /**
             * Maps the given View in the current Fragment to the given transition name in the
             * Fragment being navigated to.
             *
             * @param sharedElement A View in the current Fragment to match with a View in the
             * Fragment being navigated to.
             * @param name The transitionName of the View in the Fragment being navigated to that
             * should be matched to the shared element.
             * @return this [Builder]
             * @see FragmentTransaction.addSharedElement
             */
            fun addSharedElement(sharedElement: View, name: String): Builder {
                _sharedElements[sharedElement] = name
                return this
            }

            /**
             * Constructs the final [Extras] instance.
             *
             * @return An immutable [Extras] instance.
             */
            fun build(): Extras {
                return Extras(_sharedElements)
            }
        }

        init {
            _sharedElements.putAll(sharedElements)
        }
    }

    private companion object {
        private const val TAG = "YourFragmentNavigator"
        private const val KEY_SAVED_IDS = "androidx-nav-fragment:navigator:savedIds"
    }
}

使用方法

在您的活动/片段中,您的FragmentContainerView应该如下所示。

<androidx.fragment.app.FragmentContainerView
            android:id="@+id/navHost"
            android:name="in.your.android.core.platform.navigation.YourNavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_graph" />

在YourNavHostFragment中使用NavHostFragment的navController时,会出现无限循环与堆栈的问题。 - AlexS
在YourNavHostFragment中使用NavHostFragment的navController时,会出现无限循环与堆栈的问题。 - undefined
@AlexS 你的堆栈跟踪? - OhhhThatVarun

0

@Rainmaker 在我看来是正确的,我也做了同样的事情。 我们还可以在 onSaveInstanceState 中保存 Recycler View 的位置/状态, 以便在返回列表片段时返回到相同的 Recycler View 位置。


-1

经过一番搜索,发现不可能实现,但是可以通过使用viewmodel和livedata或者rxjava来解决这个问题。这样,在fragment状态转换后,我的产品列表就不会每次重新加载了。


4
提供一些代码示例或链接来支持你的回答会更好。 - Taseer
4
就我所知,ViewModel 不会有所帮助。Fragment 被销毁后,实际上会创建一个全新的 ViewModel。我自己也遇到了相同的问题。我的当前解决方法是在 Activity 的范围内拥有一个 state 对象作为 ViewModel。 - Matt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接