每当我重新单击或导航到下一个片段时,片段都会不断重新创建

时间:2019-12-08 06:05:32

标签: java android android-fragments

我已经在我的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。

3 个答案:

答案 0 :(得分:1)

我不确定这是否是您要寻找的答案,但是如果您担心管理状态,那么现代的状态管理方法就是使用称为视图模型的方法。视图模型是MVVM体系结构的组成部分。它们的目的是保存数据并将其公开给您的片段/活动以显示。使用导航体系结构,如果将与每个片段相关的数据适当地存储在此视图模型中,则状态将保留在视图模型中。

话虽如此,我个人建议研究MVVM架构以及专门查看模型。否则,通过在整个片段中使用savedInstance状态并手动保存和恢复重要数据,可以保持状态。

链接:  -Android View Model Component  -Android architecture guide

答案 1 :(得分:1)

  

TL; DR:跳至 只需向我展示这些步骤 部分

这是片段的正常行为。假定每次删除或替换它们时都将重新创建它们,并且假定使用onSaveInstanceState在此处恢复状态。

这是一篇不错的文章,描述了如何进行:Saving Fragment States

除此之外,您可以使用View Model,它是以下推荐的android体系结构的一部分。它们是保留和还原UI数据的好方法。

您可以通过逐步进行code lab

来学习如何实现此体系结构

Android recommended architecture

编辑:解决方案

花了一段时间,但在这里。解决方案目前不使用ViewModels

请仔细阅读,因为每个步骤都很重要。该解决方案涵盖以下两个部分

  • 在后退按键上实施正确的导航
  • 在导航过程中保持活动片段

背景:

Android导航组件提供了NavController类,您可以使用该类在不同的目的地之间导航。内部NavController使用实际执行导航的NavigatorNavigator是一个抽象类,任何人都可以扩展/继承该类来创建自己的自定义导航器,以根据目标类型提供自定义导航。当使用片段作为目的地时,NavHostFragment使用FragmentNavigator,当我们使用FragmentTransaction.replace()进行导航时,其默认实现会替换片段,这会完全破坏先前的片段并添加新的片段。因此,我们必须创建自己的导航器,而不是使用FragmentTransaction.replace()FragmentTransaction.hide()的组合,以避免碎片被破坏。

导航UI的默认行为:

默认情况下,无论何时导航到home / starting片段以外的任何其他片段,它们都不会添加到Backstack中,因此可以说,如果您按以下顺序选择片段

FragmentTransaction.show()

您的后背包只有

A -> B -> C -> D -> E

您会看到未将B,C,D片段添加到后堆栈中,因此按回车键将始终使您进入片段A,即原始片段

我们现在想要的行为:

我们想要一个简单而有效的行为。我们不希望将所有片段都添加到Backstack中,但是如果该片段已经在Backstack中,我们希望将所有片段弹出到选定的片段中。

假设我按以下顺序选择片段

[A, E]

后退堆栈也应该是

A -> B -> C -> D -> E

在按回时仅弹出最后一个片段,而后堆栈应像这样

[A, B, C, D, E]

但是如果我们导航到片段B,因为B已经在堆栈中,则应该弹出B上方的所有片段,并且我们的后堆栈应该像这样

[A, B, C, D]

我希望这种行为是有道理的。这种行为很容易使用全局操作来实现,如下所示,并且比默认行为要好。

好!怎么办 ? :

现在我们有两个选择

  1. 扩展 [A, B]
  2. 复制/粘贴FragmentNavigator

我个人只是想扩展FragmentNavigator并覆盖FragmentNavigator方法,但是由于它的所有成员变量都是私有的,所以我无法实现正确的导航。

所以我决定复制粘贴整个navigate()类,然后将整个代码中的名称从“ FragmentNavigator” 更改为我想要的名称。

已经向我展示了这些步骤!!! :

  1. 创建自定义导航器
  2. 使用自定义标签
  3. 添加全局操作
  4. 使用全局操作
  5. 将自定义导航器添加到NavController

步骤1:创建自定义导航器

这是我的自定义导航器,名为FragmentNavigator。除StickyCustomNavigator方法外,所有代码均与FragmentNavigator相同。如您所见,它使用navigate()hide()show()方法而不是add()。逻辑很简单。隐藏上一个片段并显示目标片段。如果这是我们第一次访问特定的目标片段,请添加片段而不是显示它。

replace()

步骤2:使用自定义标记

现在打开您的@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); } } } } 文件,并使用您先前在navigation.xml中输入的名称重命名与您的底部导航相关的片段标签。

@Navigator.Name()

步骤3:添加全局操作

全局操作是一种从应用程序中任何位置导航到目标位置的方法。您可以使用可视编辑器或直接使用xml添加全局操作。使用以下设置在每个片段上设置全局操作

  • 目的地:自我
  • popUpTo:自我
  • singleTop:true / checked

enter image description here enter image description here

这是您添加全局操作后<?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> 的外观

navigation.xml

步骤4:使用全局操作

当你写

<?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>

然后在 NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ()); 内部,NavigationUI使用setupWithNavController()来导航到适当的片段,具体取决于所单击菜单项的ID。正如我前面提到的,它是默认行为。我们将在其中添加自己的实现,并使用全局操作来实现所需的反压行为。

这里是您如何简单地在bottomNavigationView.setOnNavigationItemSelectedListener()

中完成此操作
MainActivity

最终步骤5:将自定义导航器添加到NavController

在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; } }); 中的childFragmentManager

NavHostFragment

也可以使用navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment)); 方法将导航图添加到此处的NavController,如下所示。

这是步骤4 步骤5

之后的setGraph()的样子
MainActivity

希望这会有所帮助。

答案 2 :(得分:-1)

我认为您可能需要防止在两次单击底部导航视图项目时重新创建片段。 bottomNavigationView.setOnNavigationItemReselectedListener { /*Nothing to ignore reselection*/} 之后的NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());