[英]My fragments keep recreating whenever I reclick or navigate to the next fragment
我已经在我的 android 应用程序上实现了新的架构组件,但不幸的是,处理这些片段的状态对我来说是一场噩梦。 每当我按下片段的图标时,每次导航时都会重新创建片段。 我该如何处理或者保存这些片段状态?
这是我处理五个片段的主要活动:
public class MainActivityCenterofInformation extends AppCompatActivity {
BottomNavigationView bottomNavigationView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_maincict);
setUpNavigation ();
}
public void setUpNavigation(){
bottomNavigationView = findViewById (R.id.bottom_nav_cict);
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager ()
.findFragmentById (R.id.nav_host_fragment_cict);
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
}
//adding animations to the fragment
}
我无法阅读 Kotlin,所以请指导我使用 Java,谢谢。
TL; DR:跳到已经告诉我的步骤!!! 部分
这是片段的正常行为。 假设每次删除或替换它们时都会重新创建它们,并且您应该使用onSaveInstanceState
恢复它们的状态。
这是一篇很好的文章,描述了如何做到这一点:保存片段状态
除此之外,您可以使用View Model ,它是以下推荐的 android 架构的一部分。 它们是保留和恢复 UI 数据的好方法。
您可以按照此逐步代码实验室学习如何实现此架构
编辑:解决方案
注意:该解决方案假设一个人不想使用
ViewModels
,而只是想使用导航 UI 隐藏或显示片段
它涵盖以下几点
背景 :
Android Navigation 组件有一个NavController
类,您可以使用它导航到不同的目的地。 NavController
使用一个真正进行导航的Navigator
。 Navigator
是一个抽象类,任何人都可以扩展/继承它以根据目的地的类型提供自定义导航。 当使用片段作为目的地时, NavHostFragment
使用一个FragmentNavigator
,每当我们使用FragmentTransaction.replace()
导航时,它的默认实现会替换片段,这会完全破坏前一个片段并添加一个新片段。 所以我们必须创建自己的导航器,而不是使用FragmentTransaction.replace()
我们将使用FragmentTransaction.hide()
和FragmentTransaction.show()
的组合来避免碎片被破坏。
Navigation UI 的默认行为:
默认情况下,每当您导航到除主片段以外的任何其他片段时,它们都不会被添加到后台堆栈中,因此假设您按以下顺序选择片段
A -> B -> C -> D -> E
你的后栈只有
[A, E]
如您所见,片段 B、C、D 未添加到 backstack,因此按下后按将始终让您进入片段 A,这是主片段
我们现在想要的行为:
我们想要一个简单而有效的行为。 我们不想将所有片段都添加到 backstack,但是如果片段已经在 backstack 中,我们希望将所有片段弹出到选定的片段。
假设我按以下顺序选择片段
A -> B -> C -> D -> E
后台也应该是
[A, B, C, D, E]
按下后,只有最后一个片段应该被弹出,并且 backstack 应该是这样的
[A, B, C, D]
但是如果我们导航到让我们说片段 B,因为 B 已经在堆栈中,那么 B 上面的所有片段都应该被弹出,我们的 backstack 应该看起来像这样
[A, B]
我希望这种行为是有意义的。 使用全局操作很容易实现此行为,如下所示,并且比默认行为更好。
好帅! 怎么办 ? :
现在我们有两个选择
FragmentNavigator
FragmentNavigator
好吧,我个人只想扩展FragmentNavigator
并覆盖navigate()
方法,但由于它的所有成员变量都是私有的,我无法实现正确的导航。
所以我决定复制粘贴整个FragmentNavigator
类,然后将整个代码中的名称从“FragmentNavigator”更改为我想要调用的任何名称。
只是告诉我步骤已经! :
第 1 步:创建自定义导航器
这是我的自定义导航器,名为StickyCustomNavigator
。 除了navigate()
方法外,所有代码都与FragmentNavigator
相同。 如您所见,它使用hide()
、 show()
和add()
方法而不是replace()
。 逻辑很简单。 隐藏前一个片段并显示目标片段。 如果这是我们第一次访问特定的目标片段,则添加片段而不是显示它。
@Navigator.Name("sticky_fragment")
public class StickyFragmentNavigator extends Navigator<StickyFragmentNavigator.Destination> {
private static final String TAG = "StickyFragmentNavigator";
private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds";
private final Context mContext;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final FragmentManager mFragmentManager;
private final int mContainerId;
@SuppressWarnings("WeakerAccess") /* synthetic access */
ArrayDeque<Integer> mBackStack = new ArrayDeque<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsPendingBackStackOperation = false;
private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
new FragmentManager.OnBackStackChangedListener() {
@SuppressLint("RestrictedApi")
@Override
public void onBackStackChanged() {
// If we have pending operations made by us then consume this change, otherwise
// detect a pop in the back stack to dispatch callback.
if (mIsPendingBackStackOperation) {
mIsPendingBackStackOperation = !isBackStackEqual();
return;
}
// The initial Fragment won't be on the back stack, so the
// real count of destinations is the back stack entry count + 1
int newCount = mFragmentManager.getBackStackEntryCount() + 1;
if (newCount < mBackStack.size()) {
// Handle cases where the user hit the system back button
while (mBackStack.size() > newCount) {
mBackStack.removeLast();
}
dispatchOnNavigatorBackPress();
}
}
};
public StickyFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager,
int containerId) {
mContext = context;
mFragmentManager = manager;
mContainerId = containerId;
}
@Override
protected void onBackPressAdded() {
mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
}
@Override
protected void onBackPressRemoved() {
mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
}
@Override
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
+ " saved its state");
return false;
}
if (mFragmentManager.getBackStackEntryCount() > 0) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mIsPendingBackStackOperation = true;
} // else, we're on the first Fragment, so there's nothing to pop from FragmentManager
mBackStack.removeLast();
return true;
}
@NonNull
@Override
public StickyFragmentNavigator.Destination createDestination() {
return new StickyFragmentNavigator.Destination(this);
}
@NonNull
public Fragment instantiateFragment(@NonNull Context context,
@SuppressWarnings("unused") @NonNull FragmentManager fragmentManager,
@NonNull String className, @Nullable Bundle args) {
return Fragment.instantiate(context, className, args);
}
@Nullable
@Override
public NavDestination navigate(@NonNull StickyFragmentNavigator.Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
String tag = Integer.toString(destination.getId());
Fragment primaryNavigationFragment = mFragmentManager.getPrimaryNavigationFragment();
if(primaryNavigationFragment != null)
ft.hide(primaryNavigationFragment);
Fragment destinationFragment = mFragmentManager.findFragmentByTag(tag);
if(destinationFragment == null) {
destinationFragment = instantiateFragment(mContext, mFragmentManager, className, args);
destinationFragment.setArguments(args);
ft.add(mContainerId, destinationFragment , tag);
}
else
ft.show(destinationFragment);
ft.setPrimaryNavigationFragment(destinationFragment);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.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
mFragmentManager.popBackStackImmediate(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()), 0);
mIsPendingBackStackOperation = false;
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
mIsPendingBackStackOperation = true;
isAdded = true;
}
if (navigatorExtras instanceof FragmentNavigator.Extras) {
FragmentNavigator.Extras extras = (FragmentNavigator.Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
@Override
@Nullable
public Bundle onSaveState() {
Bundle b = new Bundle();
int[] backStack = new int[mBackStack.size()];
int index = 0;
for (Integer id : mBackStack) {
backStack[index++] = id;
}
b.putIntArray(KEY_BACK_STACK_IDS, backStack);
return b;
}
@Override
public void onRestoreState(@Nullable Bundle savedState) {
if (savedState != null) {
int[] backStack = savedState.getIntArray(KEY_BACK_STACK_IDS);
if (backStack != null) {
mBackStack.clear();
for (int destId : backStack) {
mBackStack.add(destId);
}
}
}
}
@NonNull
private String generateBackStackName(int backStackIndex, int destId) {
return backStackIndex + "-" + destId;
}
private int getDestId(@Nullable String backStackName) {
String[] split = backStackName != null ? backStackName.split("-") : new String[0];
if (split.length != 2) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
try {
// Just make sure the backStackIndex is correctly formatted
Integer.parseInt(split[0]);
return Integer.parseInt(split[1]);
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean isBackStackEqual() {
int fragmentBackStackCount = mFragmentManager.getBackStackEntryCount();
// Initial fragment won't be on the FragmentManager's back stack so +1 its count.
if (mBackStack.size() != fragmentBackStackCount + 1) {
return false;
}
// From top to bottom verify destination ids match in both back stacks/
Iterator<Integer> backStackIterator = mBackStack.descendingIterator();
int fragmentBackStackIndex = fragmentBackStackCount - 1;
while (backStackIterator.hasNext() && fragmentBackStackIndex >= 0) {
int destId = backStackIterator.next();
try {
int fragmentDestId = getDestId(mFragmentManager
.getBackStackEntryAt(fragmentBackStackIndex--)
.getName());
if (destId != fragmentDestId) {
return false;
}
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
return true;
}
@NavDestination.ClassType(Fragment.class)
public static class Destination extends NavDestination {
private String mClassName;
public Destination(@NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(StickyFragmentNavigator.class));
}
public Destination(@NonNull Navigator<? extends StickyFragmentNavigator.Destination> fragmentNavigator) {
super(fragmentNavigator);
}
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.FragmentNavigator);
String className = a.getString(R.styleable.FragmentNavigator_android_name);
if (className != null) {
setClassName(className);
}
a.recycle();
}
@NonNull
public final StickyFragmentNavigator.Destination setClassName(@NonNull String className) {
mClassName = className;
return this;
}
@NonNull
public final String getClassName() {
if (mClassName == null) {
throw new IllegalStateException("Fragment class was not set");
}
return mClassName;
}
}
public static final class Extras implements Navigator.Extras {
private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();
Extras(Map<View, String> sharedElements) {
mSharedElements.putAll(sharedElements);
}
@NonNull
public Map<View, String> getSharedElements() {
return Collections.unmodifiableMap(mSharedElements);
}
public static final class Builder {
private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();
@NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElements(@NonNull Map<View, String> sharedElements) {
for (Map.Entry<View, String> sharedElement : sharedElements.entrySet()) {
View view = sharedElement.getKey();
String name = sharedElement.getValue();
if (view != null && name != null) {
addSharedElement(view, name);
}
}
return this;
}
@NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElement(@NonNull View sharedElement, @NonNull String name) {
mSharedElements.put(sharedElement, name);
return this;
}
@NonNull
public StickyFragmentNavigator.Extras build() {
return new StickyFragmentNavigator.Extras(mSharedElements);
}
}
}
}
第 2 步:使用自定义标签
现在打开您的navigation.xml
文件并使用您之前在@Navigator.Name()
中提供的任何名称重命名与底部导航相关的片段标签。
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<sticky_fragment
android:id="@+id/navigation_home"
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
</navigation>
第 3 步:添加全局操作
全局操作是一种从应用程序中的任何位置导航到目的地的方式。 您可以使用可视化编辑器或直接使用 xml 来添加全局操作。 使用以下设置对每个片段设置全局操作
这是您添加全局操作后您的navigation.xml
的样子
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<sticky_fragment
android:id="@+id/navigation_home"
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<sticky_fragment
android:id="@+id/navigation_images"
android:name="com.example.bottomnavigationlogic.ui.images.ImagesFragment"
android:label="@string/title_images"
tools:layout="@layout/fragment_images" />
<sticky_fragment
android:id="@+id/navigation_videos"
android:name="com.example.bottomnavigationlogic.ui.videos.VideosFragment"
android:label="@string/title_videos"
tools:layout="@layout/fragment_videos" />
<sticky_fragment
android:id="@+id/navigation_songs"
android:name="com.example.bottomnavigationlogic.ui.songs.SongsFragment"
android:label="@string/title_songs"
tools:layout="@layout/fragment_songs" />
<sticky_fragment
android:id="@+id/navigation_notifications"
android:name="com.example.bottomnavigationlogic.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
<action
android:id="@+id/action_global_navigation_home"
app:destination="@id/navigation_home"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_home" />
<action
android:id="@+id/action_global_navigation_notifications"
app:destination="@id/navigation_notifications"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_notifications" />
<action
android:id="@+id/action_global_navigation_songs"
app:destination="@id/navigation_songs"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_songs" />
<action
android:id="@+id/action_global_navigation_videos"
app:destination="@id/navigation_videos"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_videos" />
</navigation>
第 4 步:使用全局操作
当你写
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
然后在setupWithNavController()
NavigationUI 内部使用bottomNavigationView.setOnNavigationItemSelectedListener()
根据单击的菜单项的 id 导航到正确的片段。 它的默认行为就像我之前提到的那样。 我们将向其中添加我们自己的实现,并使用全局操作来实现我们想要的背压行为。
这是您在MainActivity
中简单地执行此操作的方法
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
最后一步 5:将您的自定义导航器添加到 NavController
在 MainActivity 中添加导航器,如下所示。 确保您传递的是childFragmentManager
的NavHostFragment
。
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
同样使用setGraph()
方法在此处将导航图添加到NavController
,如下所示。
这就是我的MainActivity
在第 4步和第 5 步之后的样子
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_images, R.id.navigation_videos,R.id.navigation_songs,R.id.navigation_notifications)
.build();
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
navController.setGraph(R.navigation.mobile_navigation);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView,navController);
navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
}
}
希望这可以帮助。
我不完全确定这是否是您正在寻找的答案,但如果您担心管理状态,那么管理状态的现代方式是使用称为视图模型的东西。 视图模型是 MVVM 架构的一个组件。 它们的目的是保存数据并将数据暴露给您的片段/活动以显示。 使用导航架构,如果您在此视图模型中适当地存储与每个片段相关的数据,您的状态将保留在视图模型中。
话虽如此,我个人建议研究 MVVM 架构以及专门查看模型。 否则,保留状态的蛮力是通过在整个片段中使用savedInstance
状态并手动保存和恢复重要数据。
我认为您可能需要在单击底部导航视图项目两次时防止重新创建片段。 bottomNavigationView.setOnNavigationItemReselectedListener { /*Nothing to ignore reselection*/}
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.