使用导航体系结构组件添加(而不是替换)片段

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

问题是: 它替换了(开始目标)产品列表片段,并且我不希望当用户点击后退按钮时重新加载列表。

如何使片段事务作为添加而不是替换?

21717 次浏览

在搜索了一点之后,这是不可能的,但是问题本身可以用 viewmodel 和 livedata 或 rxjava 来解决。 因此,在事务之后保持片段状态,并且我的产品列表不会每次重新加载

我遇到了同样的问题,在等待 add和其他片段事务选项时,我实现了这项工作,以便在反击时保持状态。

我只是添加了一个检查,如果绑定是存在的,然后我只是恢复以前的状态,与网络调用一样,我添加了一个检查,如果数据是存在于视图模型中,然后不做网络重新获取。经过测试,它工作正常。

编辑: 对于回收视图,我相信它会自动返回到相同的状态列表之前,你从片段导航,但存储在 onSavedInstanceSate的位置也是可能的

  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)
}
}
}

@ 唤雨师在我看来是对的,我也做了同样的事。 我们还可以在 onSaveInstanceState 中保存回收器视图的位置/状态 以便在导航回列表片段时返回到相同的回收器视图位置。

Android 导航组件只是替换,但你想添加片段代替替换类似的对话框,你可以使用这一点,但需要最小。“ 版本2.1.0 ”用于导航组件。

解决方案

你可以看到“ 对话框目的地

必须重写 NavHostFragment 的 createFragmentNavigator方法并返回 YourFragmentNavigator

YourFragmentNavigator必须重写 FragmentNavigator 的 navigate方法。

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

在导航方法中,用

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

我也面临着同样的问题,但是在我的案例中,我更新了我的代码来使用 livedata 和 viewmodel。 当您按回视图模型时,不会再次创建视图模型,因此您的数据将被保留。

确保在 viewmodel 的 init 方法中执行 api 调用,以便在创建 viewmodel 时只发生一次

只需复制 FragmentNavigator的代码(300行)并用 add()代替 replace()。这是目前对我来说最好的解决办法。

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


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


...
}

您可以将这些类用作自定义 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"
}
}

用法

在您的活动/片段中,片段容器视图应该如下所示。

<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" />