Backstack management : Restarter 只能在所有者初始化阶段创建

Posted

技术标签:

【中文标题】Backstack management : Restarter 只能在所有者初始化阶段创建【英文标题】:Backstack management : Restarter must be created only during owner's initialization stage 【发布时间】:2019-10-25 14:43:08 【问题描述】:

我在 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 只能在所有者初始化阶段创建 在 androidx.savedstate.SavedStateRegistryController.performRestore(SavedStateRegistryController.java:59) 在 androidx.fragment.app.Fragment.performCreate(Fragment.java:2580) 在 androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:837) 在 androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1237) 在 androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1302) 在 androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439) 在 androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2075) 在 androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1865) 在 androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1820) 在 androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1726) 在 androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150) 在 android.os.Handler.handleCallback(Handler.java:789) 在 android.os.Handler.dispatchMessage(Handler.java:98) 在 android.os.Looper.loop(Looper.java:164) 在 android.app.ActivityThread.main(ActivityThread.java:6709) 在 java.lang.reflect.Method.invoke(本机方法) 在 com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) 在 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769) 找了好久也没找到答案。

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

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

可能的温度解决方案

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

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

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

【问题讨论】:

这是什么fragments[] 数组?我怀疑问题在于您是在“重复使用”片段,而不是每次都创建新片段。 我需要重复使用它们,这就是我保留它们的原因 您是否找到了一种无需降级到 1.0.2 即可解决此问题的方法? 很遗憾,没有。我们将不得不等到新的版本 这可能是因为您试图使用replace 来添加当前为isRemoving 的片段。 【参考方案1】:

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

这是Fragment版本-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 更改代码,将无效。

【讨论】:

那么最好的处理方法是什么?我意识到当你开始一个事务而另一个事务已经发生时会发生这种情况,所以当用户打开一个片段然后立即回击时它会崩溃。 @PampaZiya 我已经编辑了答案,添加了解决方案 但是如果你想要动画移除和添加呢?【参考方案2】:

我遇到了同样的问题。

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) 行来解决这个问题,就是这样。 动画有效,没有崩溃。

【讨论】:

我遇到了同样的问题,当我尝试使用动画时应用程序崩溃了 ft?.AddToBackStack(null) 有什么替代方法吗?? 这是使用setCustomAnimations()时让它工作的唯一方法!在上一个动画未完成之前触发了新的 FragmentTransaction 时,我的应用程序崩溃了。花了几个小时,谢谢。仍然很奇怪这如何以及为什么可以解决问题。 确实这也是我唯一的解决方案。但这会为添加的片段添加一个历史条目。我真的必须覆盖 onBackPress 并手动完成活动吗?【参考方案3】:

如果您使用的是 '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.1.0-beta01【参考方案4】:

我将实现更改为 api for androidx.appcompat:appcompat:1.0.2 并且它对我有用

【讨论】:

【参考方案5】:

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

为避免这种情况,我禁用导航栏,直到转换完成。所以我创建了一种方法来启用/禁用 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 加载的片段创建了一个抽象片段类。在这个类中,我重写了 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) 就足够了。

【讨论】:

【参考方案6】:

我通过在添加片段方法中添加“同步”解决了这个问题

之前

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());
    

【讨论】:

【参考方案7】:

这个错误似乎可以使用 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

【讨论】:

我尝试更新两者,但没有成功。如果您在动画到新片段(中间事务)时回击,它只会对我崩溃。我没有使用pop,我只是在启动一个新的替换事务,这会导致同样的错误 是的,这是正确的,在 rc03 中的某些情况下仍然会发生这种情况:( 是的,即使使用 1.2.0-alpha01 也没有解决这个问题。我想知道解决这个问题的最佳方法是什么? @PampaZiya 如果你有办法重现这个问题,你可以在issues tracker 中发布一个示例项目吗?我们中的一些人只在 Google Play 跟踪器中看到它,并且它需要一个不可重现的完美时机。 @Dmitry 您可以通过在片段之间设置动画并在片段动画进入时点击后退按钮来重现。【参考方案8】:

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

编辑:另一种方法是将片段添加到 backstack。

【讨论】:

我认为添加到后台可以解决问题。无法删除动画,因为我需要动画作为要求。谢谢。【参考方案9】:

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

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

【讨论】:

【参考方案10】:

除了Drown Coder's 解决方案之外没有任何效果,但它仍然不完美,因为它会将事务添加到后台堆栈。因此,如果您按下底部导航中的所有按钮,则后堆栈中的每个片段至少有 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();

【讨论】:

【参考方案11】:

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

CASE-1

    在活动的框架布局中为片段充气 启动 API 请求(应用在前台时不要消耗 api 响应) 让您的应用保持在后台 使用 API 请求(假设您想在 api 响应中添加另一个片段) 在同一帧布局上使用 .replace() 方法为另一个片段充气 您将能够创建崩溃

案例-2

    在活动的框架布局中为片段充气 启动 API 请求 在前台使用 api(假设您想在 api 响应中添加另一个片段,使用片段管理器的 .replace() 方法) 将您的应用置于后台 重新创建您的应用程序(您可以使用“不保留活动”、更改权限、更改系统语言来执行此操作) 返回您的应用程序 您的活动将开始重新创建 Activity 将自动重新创建其已经膨胀的片段,假设它属于 (point-1) 确保在第 8 点之后的重新创建案例中再次请求 API 使用 API 响应并使用 .replace() 方法填充另一个片段 您将能够创建 Crash(在本例中,已经有一个过渡正在运行 point-8,并且您正在添加另一个片段在 point-10)

【讨论】:

以上是关于Backstack management : Restarter 只能在所有者初始化阶段创建的主要内容,如果未能解决你的问题,请参考以下文章

在 Fragment 中管理 BackStack 的最佳方法是啥 [重复]

具有多个 backstack 的片段

Android:片段 backStack

如何在 BackStack 上反转片段动画?

片段行为:FragmentTransaction::replace() 和反向 backStack 操作

以编程方式返回到 backstack 中的上一个片段