我有一个包含产品列表片段和其他许多片段的活动,并尝试使用架构组件导航控制器。
问题是:它会替换(起始目标)产品列表片段,但我不希望在用户点击返回按钮时重新加载列表。
如何将片段事务设置为添加而不是替换?
我有一个包含产品列表片段和其他许多片段的活动,并尝试使用架构组件导航控制器。
问题是:它会替换(起始目标)产品列表片段,但我不希望在用户点击返回按钮时重新加载列表。
如何将片段事务设置为添加而不是替换?
我遇到了同样的问题,在等待 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)
}
}
}
您需要重写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)
}
....
}}
YourNavHostFragment
。我遇到了同样的问题,但是我更新了我的代码来使用LiveData和ViewModel。
当您按下返回键时,ViewModel不会再次被创建,因此您的数据得以保留。
确保在ViewModel的init方法中进行API调用,这样它只会在创建ViewModel时发生一次。
只需复制FragmentNavigator
的代码(300行),并将replace()
替换为add()
。对我来说,这是目前最好的解决方案。
@Navigator.Name("fragment")
public class CustomFragmentNavigator extends
Navigator<...> {
...
public NavDestination navigate(...) {
...
ft.add(mContainerId, frag);
...
}
...
}
您可以将这些类用作自定义的NavHostFragment
和Navigator
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" />
@Rainmaker 在我看来是正确的,我也做了同样的事情。 我们还可以在 onSaveInstanceState 中保存 Recycler View 的位置/状态, 以便在返回列表片段时返回到相同的 Recycler View 位置。
经过一番搜索,发现不可能实现,但是可以通过使用viewmodel和livedata或者rxjava来解决这个问题。这样,在fragment状态转换后,我的产品列表就不会每次重新加载了。
androidx.navigation.fragment.FragmentNavigator#navigate
方法,就会看到它内部使用了ft.replace(mContainerId, frag);
。我认为这里唯一的选择是作为目标启动一个新的活动。 - Kyrylo Zapylaiev