Backstack management : Restarter 只能在 owner 的初始化阶段创建

Backstack management : Restarter must be created only during owner's initialization stage

我在 MainActivity 中使用底部导航栏来处理一些片段。这是用于在它们之间切换的代码:

private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
    if (item.isChecked &&
        supportFragmentManager.findFragmentById(R.id.act_main_fragment_container) != null
    )
        return@OnNavigationItemSelectedListener false
    val fragment =
        when (item.itemId) {
            R.id.navigation_home      -> fragments[0]
            R.id.navigation_bookings  -> fragments[1]
            R.id.navigation_messages  -> fragments[2]
            R.id.navigation_dashboard -> fragments[3]
            R.id.navigation_profile   -> fragments[4]
            else                      -> fragments[0]
        }
    this replaceWithNoBackStack fragment
    return@OnNavigationItemSelectedListener true
}

方法 replaceWithNoBackstack 只是一个简写:

supportFragmentManager
    ?.beginTransaction()
    ?.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
    ?.replace(containerId, fragment)
    ?.commit()

问题是当我在它们之间切换得更快时,我的应用程序崩溃并出现以下异常:

java.lang.IllegalStateException: Restarter must be created only during owner's initialization stage at androidx.savedstate.SavedStateRegistryController.performRestore(SavedStateRegistryController.java:59) at androidx.fragment.app.Fragment.performCreate(Fragment.java:2580) at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:837) at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1237) at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1302) at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439) at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2075) at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1865) at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1820) at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1726) at androidx.fragment.app.FragmentManagerImpl.run(FragmentManagerImpl.java:150) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6709) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769) I've been searching a lot and couldn't find an answer.

如果我执行 API 调用,将应用程序置于后台,等待响应,当我返回应用程序时,应用程序崩溃,因为我正在尝试立即显示对话框片段(我认为发生这种情况的原因是从后台返回时重新创建片段的事务在显示对话框片段时仍在进行中)。我通过为对话框设置 500 毫秒的延迟以一种 hacky 的方式解决了这个问题,因为我想不出其他解决方案。

请询问您是否需要有关此的更多详细信息。 提前致谢!

可能的临时解决方案

编辑 我通过将 app compat depedency 降级到 androidx.appcompat:appcompat:1.0.2 解决了这个问题,但这只是一个临时解决方案,因为我将来必须更新它。我希望有人能解决这个问题。

编辑 2 我通过从片段事务中删除 setTransition() 解决了这个问题。至少我知道为什么 android 应用程序通常没有很好的过渡

编辑 3 也许避免此问题并使事情顺利进行的最佳解决方案就是使用 ViewPager 来处理底部栏导航

如果您正在使用 'androidx.core:core-ktx:1.0.2', 尝试更改为 1.0.1

如果您正在使用生命周期(或 rxFragment)和 androidx_appcompat:alpha05,请尝试更改版本。
例如)appcompat:1.1.0-beta01 或 1.0.2

我认为在目标片段被重用时保存状态时显示为错误(onPause-onResume)。

我将 androidx.appcompat:appcompat:1.0.2 的实现更改为 api,它对我有用

因为版本1.0.0没有检查状态,所以不会抛异常, 但是版本1.1.0修改了源码,所以抛出异常

这是Fragment version-1.1.0源代码,它将调用方法performRestore

    void performCreate(Bundle savedInstanceState) {
        if (mChildFragmentManager != null) {
            mChildFragmentManager.noteStateNotSaved();
        }
        mState = CREATED;
        mCalled = false;
        mSavedStateRegistryController.performRestore(savedInstanceState);
        onCreate(savedInstanceState);
        mIsCreated = true;
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onCreate()");
        }
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    }

/**
the exception
**/
public void performRestore(@Nullable Bundle savedState) {
        Lifecycle lifecycle = mOwner.getLifecycle();
        if (lifecycle.getCurrentState() != Lifecycle.State.INITIALIZED) {
            throw new IllegalStateException("Restarter must be created only during "
                    + "owner's initialization stage");
        }
        lifecycle.addObserver(new Recreator(mOwner));
        mRegistry.performRestore(lifecycle, savedState);
    }

这是1.0.0版本的源码,没有调用performRestore,所以不会抛异常

void performCreate(Bundle savedInstanceState) {
    if (mChildFragmentManager != null) {
        mChildFragmentManager.noteStateNotSaved();
    }
    mState = CREATED;
    mCalled = false;
    onCreate(savedInstanceState);
    mIsCreated = true;
    if (!mCalled) {
        throw new SuperNotCalledException("Fragment " + this
                + " did not call through to super.onCreate()");
    }
    mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}

两种不同的解决方案可以解决这个问题:
第一个方案是拆分交易。
因为我们总是使用 replace 或将 removeadd 合并到一个 Transaction 中。 我们可以像这样将交易拆分为两个交易:

FragmentTransaction ft = manager.beginTransaction();
        Fragment prev = manager.findFragmentByTag(tag);
        if (prev != null) {
        //commit immediately
            ft.remove(prev).commitAllowingStateLoss();
        }
        FragmentTransaction addTransaction = manager.beginTransaction();
        addTransaction.addToBackStack(null);
        addTransaction.add(layoutId, fragment,
                tag).commitAllowingStateLoss();

因为这两个事务将是两个不同的消息,将由 Handler 处理。
第二种解决方案是提前检查状态。 大家可以关注源码,提前查看状态

FragmentTransaction ft = manager.beginTransaction();
        Fragment prev = manager.findFragmentByTag(tag);
        if (prev != null) {
        if (prev.getLifecycle().getCurrentState() != Lifecycle.State.INITIALIZED) {
            return;
        }
            ft.remove(prev);
        }

我推荐第一种方式,因为第二种方式是跟着源码走,如果有源码 code 更改密码,将无效。

这个错误似乎可以通过使用 androidx.appcompat:appcomat:1.1.0-rc01 和 androidx.fragment:fragment:1.1.0-rc03

来解决

https://developer.android.com/jetpack/androidx/releases/fragment#1.1.0-rc03

我遇到了同样的问题。

val fragment = Account.activityAfterLogin
        val ft = activity?.getSupportFragmentManager()?.beginTransaction()
        //error
        ft?.setCustomAnimations(android.R.anim.slide_in_left,android.R.anim.slide_out_right)0
        ft?.replace(R.id.framelayout_account,fragment)
        ft?.commit()

更改库版本没有帮助。 我通过在 ft?.setCustomAnimations () 方法之后添加 ft?.AddToBackStack(null) 行来解决这个问题,仅此而已。 动画有效,没有崩溃。

我在使用 setCustomAnimations 时遇到了这个问题。 通过删除 setCustomAnimations 解决了我的问题。 即使在使用 setCustomAnimation 显示之前创建片段的新实例时,我也没有问题。

编辑:另一种方法是将片段添加到后台堆栈。

如果有帮助的话,我在使用 BottomNavigationView 和 setCustomAnimations 时遇到了同样的问题,主要是通过在 Fragment 之间快速切换,您可能会在前一个尚未完成时启动 FragmentTransaction 然后崩溃。

为避免这种情况,我禁用了导航栏,直到转换完成。因此,我创建了一个 enable/disable BottomNavigationView 项目的方法(禁用 BottomNavigationView 本身不会禁用菜单,或者我没有找到方法),然后在转换完成后重新启用它们。

为了禁用项目,我在开始 FragmentTransition 之前调用了以下方法:

public void toggleNavigationBarItems(boolean enabled) {
   Menu navMenu = navigationView.getMenu();
   for (int i = 0; i < navMenu.size(); ++i) {
       navMenu.getItem(i).setEnabled(enabled);
   }
}

为了重新启用它们,我为从 BottomNavigationView 加载的片段创建了一个抽象片段 class。在此 class 中,我重写了 onCreateAnimator(如果您使用 View Animation,您应该重写 onCreateAnimation)并重新启用它们 onAnimationEnd。

@Nullable
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
    if(enter){ // check the note below
        Animator animator = AnimatorInflater.loadAnimator(getContext(), nextAnim);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                myActivity.toggleNavigationBarItems(true)
            }
        });
        return animator;
    }
    return super.onCreateAnimator(transit, enter, nextAnim);
}

注意:由于我的进入和退出动画具有相同的持续时间,因此我不需要同步它们,因为进入动画在退出动画之后开始。这就是为什么 if (enter) 就足够了。

我能够通过对所有底部导航片段交易使用 commitNow() 而不是 commit() 来解决这个问题(希望如此)。 我更喜欢这种方法,因为它允许您仍然在片段之间使用自定义转换。

注意:仅当您不希望将底部导航事务添加到后台堆栈(无论如何您都不应该这样做)时,这才是解决方案。

除了 解决方案外没有任何效果,但它仍然不完美,因为它向后台堆栈添加了事务。因此,如果您按下底部导航中的所有按钮,则在后台堆栈中至少有每个片段中的 1 个。我略微改进了这个解决方案,所以你不要使用 .replace() 会导致应用程序崩溃的动作动画。

代码如下:

if (getChildFragmentManager().getBackStackEntryCount() > 0) {
    getChildFragmentManager().popBackStack();
}

FragmentTransaction addTransaction = getChildFragmentManager().beginTransaction();
addTransaction.setCustomAnimations(R.animator.fragment_fade_in, R.animator.fragment_fade_out);
addTransaction.addToBackStack(null);
addTransaction.add(R.id.frame, fragment, fragment.getClass().getName()).commitAllowingStateLoss();

我通过添加 'synchronized' 和添加片段方法解决了这个问题

之前

public void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
    try {
        if (!fragment.isAdded()) {
            if (param != null) {
                fragment.setArguments(param);
            }

            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
                    .add(contentFrameId, fragment)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

            if (addToStack)
                fragmentTransaction.addToBackStack(fragment.getClass().toString());

            fragmentTransaction.commit();
        }
    } catch (IllegalStateException e) {
        handleError(e.getMessage());
    } catch (Exception e) {
        handleError(e.getMessage());
    }
}

之后:

public synchronized void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
    try {
        if (!fragment.isAdded()) {
            if (param != null) {
                fragment.setArguments(param);
            }

            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
                    .add(contentFrameId, fragment)
                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);

            if (addToStack)
                fragmentTransaction.addToBackStack(fragment.getClass().toString());

            fragmentTransaction.commit();
        }
    } catch (IllegalStateException e) {
        handleError(e.getMessage());
    } catch (Exception e) {
        handleError(e.getMessage());
    }
}

我找到了另一种创建案例的方法。

案例 1

  1. 在 activity
  2. 处膨胀帧布局中的片段
  3. 启动一个 API 请求(应用在前台时不消耗 api 响应)
  4. 让您的应用程序在后台运行
  5. 使用 API 请求(假设您想在 api 响应中添加另一个片段)
  6. 在同一帧布局上使用 .replace() 方法膨胀另一个片段
  7. 您将能够创建崩溃

CASE-2

  1. 在 activity
  2. 处膨胀帧布局中的片段
  3. 开始 API 请求
  4. 在前台使用 api(假设您想在 api 响应中添加另一个片段,使用片段管理器的 .replace() 方法)
  5. 将您的应用置于后台
  6. 重新创建您的应用程序(您可以使用“不保留活动”、更改权限、更改系统语言来执行此操作)
  7. 回到你的应用程序
  8. 您的 activity 将开始重新创建
  9. Activity 假设它是 (point-1)
  10. 将自动重新创建其已经膨胀的片段
  11. 确保 API 在点 8
  12. 之后在重新创建的情况下再次请求
  13. 使用 API 响应并使用 .replace() 方法膨胀另一个片段
  14. 您将能够创建崩溃(在本例中,转换已经是 运行 点 8,而您要在点 10 添加另一个片段)