简体   繁体   中英

The specified child already has a parent. Fragment on back press

I know this question seems to be asked and answered, but it is not true. Because I haven't met a solution yet.

I have Activity with a lot of fragments. And I store all transitions history (can return to each fragment in LIFO order with the back press - because each adds to back stack)

I want to reach the next feature: When I press back - view of bottom fragment must not be re-creating.

I do next

1) Use android navigation components and transitions like

fun navigate(@IdRes resId: Int, bundle: Bundle? = null, navOptions: NavOptions? = null, sharedElements: List<View>? = null) {
        navController.navigate(resId, bundle, navOptions, sharedElements?.let {
            if (it.isEmpty()) null else
                FragmentNavigatorExtras(
                        *it.map { view -> Pair(view, view.transitionNameCompat.safe) }
                                .filter { pair -> pair.second.isNotEmpty() }
                                .toTypedArray()
                )
        })
    }

where idRes is destination id (not transition id)

2) hold the content view in fragment and detach it from the parent in onCreateView view method. Because getView() returns null even fragment appear from back stack.

private var contentView: View? = null

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
        prepareView(contentView) ?: createView(inflater, container)

private fun prepareView(view: View?): View? {
    val parent = view?.parent
    val viewGroup = parent as? ViewGroup
    viewGroup?.removeView(view)
    return view
}

protected open fun createView(inflater: LayoutInflater, container: ViewGroup?): View? {
    val layout = layout()
    if (layout == -1)
        throw IllegalArgumentException("You need to override \"layout()\" fun or override \"createView\" fun")
    return inflater.inflate(layout(), container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    if (contentView == null) {
        onFirstInitialization(view, savedInstanceState)
    } else {
        onNextInitialization(view, savedInstanceState)
    }
    onEachInitialization(view, savedInstanceState)
    contentView = view
}

I've got exception ONLY IF I transit to next fragment and press "back" rapidly! In normal mode all good and correct.

Exception:

 java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
        at android.view.ViewGroup.addViewInner(ViewGroup.java:5050)
        at android.view.ViewGroup.addView(ViewGroup.java:4881)
        at android.view.ViewGroup.addView(ViewGroup.java:4821)
        at android.view.ViewGroup.addView(ViewGroup.java:4794)
        at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:890)
        at androidx.fragment.app.FragmentManagerImpl.addAddedFragments(FragmentManagerImpl.java:2092)
        at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1866)
        at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1822)
        at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1723)
        at androidx.fragment.app.FragmentManagerImpl.dispatchStateChange(FragmentManagerImpl.java:2624)
        at androidx.fragment.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManagerImpl.java:2580)
        at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:2571)
        at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:907)
        at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1235)
        at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1301)
        at androidx.fragment.app.FragmentManagerImpl.dispatchStateChange(FragmentManagerImpl.java:2620)
        at androidx.fragment.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManagerImpl.java:2580)
        at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:246)
        at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:541)
        at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:201)
        at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1470)
        at android.app.Activity.performStart(Activity.java:7176)
        at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3086)
        at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
        at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1926)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6923)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

I can solve this by place prepareView to onDestroyView lifecycle method and everything work correctly, but it produces memory leak: I find it out - when the view has not a parent after onDestroyView then onDestroy never called and fragments hold in memory even after the back press.

You can say "Hey dude, don't hurry, use your app slowly and the exception will never be thrown, as you say. But app in production and I met crashes with this bug" :( How I can handle this case?

UPDATE: I researched a little and found out something. Exception thrown only when view not detached from window. And view detach from window only after 300-400 ms after new Fragment opened

Yes! It's true. You can't create again its object because it has a parent. The main problem may you haven't noticed is whenever you called your fragment again then onCreateView() is called.

So, in-sort I suggest you move your code from onCreateView() to onCreate() method of the fragment.

I also struggled with this type of problem. So, inflate your view in onCreate() method and return main view in onCreateView() method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM