| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.fragment.app |
| |
| import android.content.Context |
| import android.os.Bundle |
| import android.util.AttributeSet |
| import android.view.LayoutInflater |
| import android.view.Menu |
| import android.view.View |
| import android.view.ViewGroup |
| import android.widget.TextView |
| import androidx.core.view.ViewCompat |
| import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks |
| import androidx.fragment.app.test.EmptyFragmentTestActivity |
| import androidx.fragment.app.test.FragmentTestActivity |
| import androidx.fragment.app.test.TestViewModel |
| import androidx.fragment.test.R |
| import androidx.lifecycle.Lifecycle |
| import androidx.lifecycle.LifecycleEventObserver |
| import androidx.lifecycle.ViewModelProvider |
| import androidx.lifecycle.ViewModelStore |
| import androidx.test.annotation.UiThreadTest |
| import androidx.test.core.app.ActivityScenario |
| import androidx.test.ext.junit.runners.AndroidJUnit4 |
| import androidx.test.filters.MediumTest |
| import androidx.testutils.withActivity |
| import com.google.common.truth.Truth.assertThat |
| import com.google.common.truth.Truth.assertWithMessage |
| import org.junit.Assert.fail |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.mockito.Mockito.mock |
| import org.mockito.Mockito.times |
| import org.mockito.Mockito.verify |
| |
| @RunWith(AndroidJUnit4::class) |
| @MediumTest |
| class FragmentLifecycleTest { |
| |
| @Suppress("DEPRECATION") |
| @get:Rule |
| val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java) |
| |
| @Test |
| fun basicLifecycle() { |
| val fm = activityRule.activity.supportFragmentManager |
| val strictFragment = StrictFragment() |
| |
| // Add fragment; StrictFragment will throw if it detects any violation |
| // in standard lifecycle method ordering or expected preconditions. |
| fm.beginTransaction().add(strictFragment, "EmptyHeadless").commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment is not added").that(strictFragment.isAdded).isTrue() |
| assertWithMessage("fragment is detached").that(strictFragment.isDetached).isFalse() |
| assertWithMessage("fragment is not resumed").that(strictFragment.isResumed).isTrue() |
| val lifecycle = strictFragment.lifecycle |
| assertThat(lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) |
| |
| // Test removal as well; StrictFragment will throw here too. |
| fm.beginTransaction().remove(strictFragment).commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment is added").that(strictFragment.isAdded).isFalse() |
| assertWithMessage("fragment is resumed").that(strictFragment.isResumed).isFalse() |
| assertThat(lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) |
| // Once removed, a new Lifecycle should be created just in case |
| // the developer reuses the same Fragment |
| assertThat(strictFragment.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) |
| |
| // This one is perhaps counterintuitive; "detached" means specifically detached |
| // but still managed by a FragmentManager. The .remove call above |
| // should not enter this state. |
| assertWithMessage("fragment is detached").that(strictFragment.isDetached).isFalse() |
| } |
| |
| @Test |
| fun detachment() { |
| val fm = activityRule.activity.supportFragmentManager |
| val f1 = StrictFragment() |
| val f2 = StrictFragment() |
| |
| fm.beginTransaction().add(f1, "1").add(f2, "2").commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue() |
| |
| // Test detaching fragments using StrictFragment to throw on errors. |
| fm.beginTransaction().detach(f1).detach(f2).commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not detached").that(f1.isDetached).isTrue() |
| assertWithMessage("fragment 2 is not detached").that(f2.isDetached).isTrue() |
| assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse() |
| assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse() |
| |
| // Only reattach f1; leave v2 detached. |
| fm.beginTransaction().attach(f1).commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| assertWithMessage("fragment 1 is detached").that(f1.isDetached).isFalse() |
| assertWithMessage("fragment 2 is not detached").that(f2.isDetached).isTrue() |
| |
| // Remove both from the FragmentManager. |
| fm.beginTransaction().remove(f1).remove(f2).commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse() |
| assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse() |
| assertWithMessage("fragment 1 is detached").that(f1.isDetached).isFalse() |
| assertWithMessage("fragment 2 is detached").that(f2.isDetached).isFalse() |
| } |
| |
| @Test |
| fun basicBackStack() { |
| val fm = activityRule.activity.supportFragmentManager |
| val f1 = StrictFragment() |
| val f2 = StrictFragment() |
| |
| // Add a fragment normally to set up |
| fm.beginTransaction().add(f1, "1").commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| |
| // Remove the first one and add a second. We're not using replace() here since |
| // these fragments are headless and as of this test writing, replace() only works |
| // for fragments with views and a container view id. |
| // Add it to the back stack so we can pop it afterwards. |
| fm.beginTransaction().remove(f1).add(f2, "2").addToBackStack("stack1").commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse() |
| assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue() |
| |
| // Test popping the stack |
| fm.popBackStack() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse() |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| } |
| |
| @Test |
| fun attachBackStack() { |
| val fm = activityRule.activity.supportFragmentManager |
| val f1 = StrictFragment() |
| val f2 = StrictFragment() |
| |
| // Add a fragment normally to set up |
| fm.beginTransaction().add(f1, "1").commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| |
| fm.beginTransaction().detach(f1).add(f2, "2").addToBackStack("stack1").commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not detached").that(f1.isDetached).isTrue() |
| assertWithMessage("fragment 2 is detached").that(f2.isDetached).isFalse() |
| assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse() |
| assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue() |
| } |
| |
| @Test |
| fun viewLifecycle() { |
| // Test basic lifecycle when the fragment creates a view |
| |
| val fm = activityRule.activity.supportFragmentManager |
| val f1 = StrictViewFragment() |
| |
| fm.beginTransaction().add(android.R.id.content, f1).commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| val view = f1.requireView() |
| assertWithMessage("fragment 1 returned null from getView").that(view).isNotNull() |
| assertWithMessage("fragment 1's view is not attached to a window") |
| .that(ViewCompat.isAttachedToWindow(view)).isTrue() |
| |
| fm.beginTransaction().remove(f1).commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse() |
| assertWithMessage("fragment 1 returned non-null from getView after removal") |
| .that(f1.view).isNull() |
| assertWithMessage("fragment 1's previous view is still attached to a window") |
| .that(ViewCompat.isAttachedToWindow(view)).isFalse() |
| } |
| |
| @Test |
| fun viewReplace() { |
| // Replace one view with another, then reverse it with the back stack |
| |
| val fm = activityRule.activity.supportFragmentManager |
| val f1 = StrictViewFragment() |
| val f2 = StrictViewFragment() |
| |
| fm.beginTransaction().add(android.R.id.content, f1).commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| |
| val origView1 = f1.requireView() |
| assertWithMessage("fragment 1 returned null view").that(origView1).isNotNull() |
| assertWithMessage("fragment 1's view not attached") |
| .that(ViewCompat.isAttachedToWindow(origView1)).isTrue() |
| |
| fm.beginTransaction().replace(android.R.id.content, f2).addToBackStack("stack1").commit() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse() |
| assertWithMessage("fragment 2 is added").that(f2.isAdded).isTrue() |
| assertWithMessage("fragment 1 returned non-null view").that(f1.view).isNull() |
| assertWithMessage("fragment 1's old view still attached") |
| .that(ViewCompat.isAttachedToWindow(origView1)).isFalse() |
| val origView2 = f2.requireView() |
| assertWithMessage("fragment 2 returned null view").that(origView2).isNotNull() |
| assertWithMessage("fragment 2's view not attached") |
| .that(ViewCompat.isAttachedToWindow(origView2)).isTrue() |
| |
| fm.popBackStack() |
| executePendingTransactions(fm) |
| |
| assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue() |
| assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse() |
| assertWithMessage("fragment 2 returned non-null view").that(f2.view).isNull() |
| assertWithMessage("fragment 2's view still attached") |
| .that(ViewCompat.isAttachedToWindow(origView2)).isFalse() |
| val newView1 = f1.requireView() |
| assertWithMessage("fragment 1 had same view from last attachment") |
| .that(newView1).isNotSameInstanceAs(origView1) |
| assertWithMessage("fragment 1's view not attached") |
| .that(ViewCompat.isAttachedToWindow(newView1)).isTrue() |
| } |
| |
| @Test |
| @UiThreadTest |
| fun setMaxLifecycle() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| fc.attachHost(null) |
| fc.dispatchCreate() |
| |
| val fm = fc.supportFragmentManager |
| |
| val fragment = StrictViewFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .setMaxLifecycle(fragment, Lifecycle.State.STARTED) |
| .commitNow() |
| |
| fc.dispatchActivityCreated() |
| fc.dispatchStart() |
| fc.dispatchResume() |
| |
| assertThat(fragment.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) |
| } |
| |
| @Test |
| @UiThreadTest |
| fun setMaxLifecycleForceState() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| |
| val fm = fc.supportFragmentManager |
| |
| val fragment = StrictViewFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .commitNow() |
| |
| assertThat(fragment.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) |
| val view = fragment.requireView() |
| assertThat(view.parent).isNotNull() |
| |
| fm.beginTransaction() |
| .setMaxLifecycle(fragment, Lifecycle.State.CREATED) |
| .commitNow() |
| |
| assertThat(fragment.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) |
| assertThat(fragment.view).isNull() |
| assertThat(view.parent).isNull() |
| } |
| |
| @Test |
| @UiThreadTest |
| fun setMaxLifecyclePop() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| |
| val fm = fc.supportFragmentManager |
| |
| val fragment = StrictViewFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .setMaxLifecycle(fragment, Lifecycle.State.CREATED) |
| .commitNow() |
| |
| assertThat(fragment.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) |
| |
| assertThat(fragment.calledOnResume).isFalse() |
| |
| fm.beginTransaction() |
| .setMaxLifecycle(fragment, Lifecycle.State.STARTED) |
| .addToBackStack(null) |
| .commit() |
| executePendingTransactions(fm) |
| |
| assertThat(fragment.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) |
| |
| fm.popBackStackImmediate() |
| |
| assertThat(fragment.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) |
| } |
| |
| @Test |
| @UiThreadTest |
| fun setMaxLifecycleOnDifferentFragments() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictViewFragment() |
| val fragment2 = StrictViewFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment1) |
| .add(android.R.id.content, fragment2) |
| .setMaxLifecycle(fragment1, Lifecycle.State.STARTED) |
| .setMaxLifecycle(fragment2, Lifecycle.State.CREATED) |
| .commitNow() |
| |
| assertThat(fragment1.calledOnResume).isFalse() |
| assertThat(fragment2.calledOnResume).isFalse() |
| |
| assertThat(fragment1.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) |
| assertThat(fragment2.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun removeFragmentAfterOnSaveInstanceStateCycle() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictViewFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment1) |
| .commitNow() |
| |
| val fragment2 = StrictViewFragment() |
| fm.beginTransaction() |
| .replace(android.R.id.content, fragment2) |
| .addToBackStack(null) |
| .commit() |
| fm.executePendingTransactions() |
| |
| // Now go through a cycle of onSaveInstanceState |
| fc.dispatchPause() |
| fc.dispatchStop() |
| fc.saveAllState() |
| fc.noteStateNotSaved() |
| fc.dispatchStart() |
| fc.dispatchResume() |
| |
| // Now remove fragment2, which will clear out all of its state |
| fm.popBackStackImmediate() |
| |
| // And go through a full Fragment restart |
| val fc2 = fc.restart(activityRule, viewModelStore, false) |
| val fm2 = fc2.supportFragmentManager |
| |
| // All saved state should have been re-associated with the remaining |
| // fragments, with no state saved for fragments that have been popped |
| assertThat(fm2.fragmentStore.allSavedState).isEmpty() |
| } |
| |
| @Test |
| @UiThreadTest |
| fun addChildFragmentInAttach() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| fc.attachHost(null) |
| |
| val fm = fc.supportFragmentManager |
| |
| fm.beginTransaction() |
| .add(android.R.id.content, AddChildInOnAttachFragment()) |
| .commitNow() |
| |
| fc.dispatchCreate() |
| } |
| |
| @Test |
| @UiThreadTest |
| fun focusedInflatedView() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| |
| fc.attachHost(null) |
| |
| val fm = fc.supportFragmentManager |
| // imitate inflating a fragment in FragmentContainerView |
| fm.beginTransaction() |
| .setReorderingAllowed(true) |
| .add(android.R.id.content, StrictViewFragment(R.layout.with_edit_text), "fragment1") |
| .commitNowAllowingStateLoss() |
| |
| fc.dispatchCreate() |
| fc.dispatchActivityCreated() |
| fc.dispatchStart() |
| fc.dispatchResume() |
| |
| val fragment = fc.supportFragmentManager.findFragmentByTag("fragment1") |
| assertThat(fragment).isNotNull() |
| |
| val editText = |
| fragment!!.requireView().findViewById<View>(androidx.fragment.test.R.id.editText) |
| assertThat(editText.isFocused).isTrue() |
| } |
| |
| /** |
| * This test confirms that as long as a parent fragment has called super.onCreate, |
| * any child fragments added, committed and with transactions executed will be brought |
| * to at least the CREATED state by the time the parent fragment receives onCreateView. |
| * This means the child fragment will have received onAttach/onCreate. |
| */ |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun childFragmentManagerAttach() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| fc.attachHost(null) |
| fc.dispatchCreate() |
| |
| val mockLc = mock(FragmentManager.FragmentLifecycleCallbacks::class.java) |
| val mockRecursiveLc = mock(FragmentManager.FragmentLifecycleCallbacks::class.java) |
| |
| val fm = fc.supportFragmentManager |
| fm.registerFragmentLifecycleCallbacks(mockLc, false) |
| fm.registerFragmentLifecycleCallbacks(mockRecursiveLc, true) |
| |
| val fragment = ChildFragmentManagerFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .commitNow() |
| |
| verify<FragmentManager.FragmentLifecycleCallbacks>(mockLc, times(1)) |
| .onFragmentCreated(fm, fragment, null) |
| |
| fc.dispatchActivityCreated() |
| |
| val childFragment = fragment.childFragment!! |
| |
| verify<FragmentLifecycleCallbacks>(mockLc, times(1)) |
| .onFragmentActivityCreated(fm, fragment, null) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentActivityCreated(fm, fragment, null) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentActivityCreated(fm, childFragment, null) |
| |
| fc.dispatchStart() |
| |
| verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentStarted(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentStarted(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentStarted(fm, childFragment) |
| |
| fc.dispatchResume() |
| |
| verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentResumed(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentResumed(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentResumed(fm, childFragment) |
| |
| // Confirm that the parent fragment received onAttachFragment |
| assertWithMessage("parent fragment did not receive onAttachFragment") |
| .that(fragment.calledOnAttachFragment).isTrue() |
| |
| fc.dispatchStop() |
| |
| verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentStopped(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentStopped(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentStopped(fm, childFragment) |
| |
| viewModelStore.clear() |
| fc.dispatchDestroy() |
| |
| verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentDestroyed(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentDestroyed(fm, fragment) |
| verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1)) |
| .onFragmentDestroyed(fm, childFragment) |
| } |
| |
| /** |
| * This test checks that FragmentLifecycleCallbacks are invoked when expected. |
| */ |
| @Test |
| @UiThreadTest |
| fun fragmentLifecycleCallbacks() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| fc.attachHost(null) |
| fc.dispatchCreate() |
| |
| val fm = fc.supportFragmentManager |
| |
| val fragment = ChildFragmentManagerFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .commitNow() |
| |
| fc.dispatchActivityCreated() |
| |
| fc.dispatchStart() |
| fc.dispatchResume() |
| |
| // Confirm that the parent fragment received onAttachFragment |
| assertWithMessage("parent fragment did not receive onAttachFragment") |
| .that(fragment.calledOnAttachFragment).isTrue() |
| |
| fc.shutdown(viewModelStore) |
| } |
| |
| /** |
| * This tests that fragments call onDestroy when the activity finishes. |
| */ |
| @Test |
| @UiThreadTest |
| fun fragmentDestroyedOnFinish() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragmentA = StrictViewFragment(R.layout.fragment_a) |
| val fragmentB = StrictViewFragment(R.layout.fragment_b) |
| fm.beginTransaction() |
| .add(android.R.id.content, fragmentA) |
| .commit() |
| fm.executePendingTransactions() |
| fm.beginTransaction() |
| .replace(android.R.id.content, fragmentB) |
| .addToBackStack(null) |
| .commit() |
| fm.executePendingTransactions() |
| fc.shutdown(viewModelStore) |
| assertThat(fragmentB.calledOnDestroy).isTrue() |
| assertThat(fragmentA.calledOnDestroy).isTrue() |
| } |
| |
| /** |
| * Test that onDestroyView gets called for childFragment with Animation even when there is a |
| * config change. |
| */ |
| @Test |
| @UiThreadTest |
| fun childFragmentViewDestroyedWithParent() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment = ParentFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .commitNow() |
| |
| val childFragment = StrictViewFragment() |
| |
| fragment.childFragmentManager.beginTransaction() |
| .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) |
| .add(R.id.fragmentContainer, childFragment, "child") |
| .commitNow() |
| |
| val viewLifecycleOwner = childFragment.viewLifecycleOwner |
| |
| fc.restart(activityRule, viewModelStore, false) |
| |
| assertWithMessage("ChildFragment viewLifecycle was not destroyed") |
| .that(viewLifecycleOwner.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED) |
| } |
| |
| /** |
| * Test to ensure childFragment gets initState() called when parent is destroyed |
| */ |
| @Test |
| @UiThreadTest |
| fun childFragmentInitWhenFragmentManagerDestroyed() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment = ParentFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .commitNow() |
| |
| val childFragment = StrictViewFragment() |
| |
| fragment.childFragmentManager.beginTransaction() |
| .add(R.id.fragmentContainer, childFragment, "child") |
| .commitNow() |
| |
| val lifecycle = childFragment.lifecycle |
| |
| fc.restart(activityRule, viewModelStore, false) |
| |
| assertWithMessage("ChildFragment lifecycle instance is same") |
| .that(lifecycle).isNotSameInstanceAs(childFragment.lifecycle) |
| } |
| |
| @Test |
| @UiThreadTest |
| fun childFragmentManagerDestroyedWhenFragmentRemoved() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| val fragment = StrictFragment() |
| fm.beginTransaction() |
| .add(fragment, "tag") |
| .commitNow() |
| |
| fm.beginTransaction() |
| .remove(fragment) |
| .commitNow() |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| fm.beginTransaction() |
| .add(fragment, "tag") |
| .commitNow() |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun childFragmentManagerDestroyedWhenRetainedFragmentRemoved() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| // Not using StrictFragment here, retained fragment wont go to ATTACHED |
| val fragment = Fragment() |
| fragment.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment, "tag") |
| .commitNow() |
| |
| fm.beginTransaction() |
| .remove(fragment) |
| .commitNow() |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| fm.beginTransaction() |
| .add(fragment, "tag") |
| .commitNow() |
| } |
| |
| /** |
| * Test to ensure childFragment gets initState() called when parent is removed |
| */ |
| @Test |
| @UiThreadTest |
| fun childFragmentInitWhenParentRemoved() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment = StrictViewFragment(R.layout.simple_container) |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .commitNow() |
| |
| val childFragment = StrictViewFragment() |
| |
| fragment.childFragmentManager.beginTransaction() |
| .add(R.id.fragmentContainer, childFragment, "child") |
| .commitNow() |
| |
| val lifecycle = childFragment.lifecycle |
| |
| fm.beginTransaction() |
| .remove(fragment) |
| .commitNow() |
| |
| assertWithMessage("ChildFragment lifecycle instance is same") |
| .that(lifecycle).isNotSameInstanceAs(childFragment.lifecycle) |
| } |
| |
| @Test |
| @UiThreadTest |
| fun testSetArgumentsLifecycle() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val f = StrictFragment() |
| f.arguments = Bundle() |
| |
| fm.beginTransaction().add(f, "1").commitNow() |
| |
| f.arguments = Bundle() |
| |
| fc.dispatchPause() |
| @Suppress("DEPRECATION") |
| fc.saveAllState() |
| |
| try { |
| f.arguments = Bundle() |
| } catch (e: IllegalStateException) { |
| assertThat(e) |
| .hasMessageThat().contains("Fragment already added and state has been saved") |
| } |
| |
| fc.dispatchStop() |
| |
| try { |
| f.arguments = Bundle() |
| } catch (e: IllegalStateException) { |
| assertThat(e) |
| .hasMessageThat().contains("Fragment already added and state has been saved") |
| } |
| |
| viewModelStore.clear() |
| fc.dispatchDestroy() |
| |
| // Fully destroyed, so fragments have been removed. |
| f.arguments = Bundle() |
| } |
| |
| /** |
| * Ensure that FragmentManager rejects commit() and commitNow() prior to restoring |
| * saved instance state |
| */ |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun addRetainedBeforeRestoreSaveState() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commitNow() |
| |
| fc.shutdown(viewModelStore, false) |
| |
| fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| |
| // Now before we restoreSaveState, add a retained Fragment |
| val fragment2 = StrictFragment() |
| fragment2.retainInstance = true |
| try { |
| fc.supportFragmentManager.beginTransaction() |
| .add(fragment2, "2") |
| .commitNow() |
| fail("commitNow() should fail prior to onCreate") |
| } catch (expected: IllegalStateException) { |
| } |
| try { |
| fc.supportFragmentManager.beginTransaction() |
| .add(fragment2, "2") |
| .commit() |
| fail("commit() should fail prior to onCreate") |
| } catch (expected: IllegalStateException) { |
| } |
| } |
| |
| /** |
| * Ensure that FragmentManager |
| */ |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun addRetainedAfterSaveState() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commitNow() |
| |
| // Now save the state of the FragmentManager |
| fc.dispatchPause() |
| val savedState = fc.saveAllState() |
| |
| val fragment2 = StrictFragment() |
| fragment2.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment2, "2") |
| .commitNowAllowingStateLoss() |
| |
| fc.dispatchStop() |
| fc.dispatchDestroy() |
| |
| fc = activityRule.startupFragmentController(viewModelStore, savedState) |
| fm = fc.supportFragmentManager |
| |
| assertThat(fm.findFragmentByTag("1")) |
| .isSameInstanceAs(fragment1) |
| assertThat(fm.findFragmentByTag("2")) |
| .isNull() |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun popBackStackImmediateAfterSaveState() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commitNow() |
| |
| // Now save the state of the FragmentManager |
| fc.dispatchPause() |
| fc.saveAllState() |
| |
| val fragment2 = StrictFragment() |
| fragment2.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment2, "2") |
| .commitNowAllowingStateLoss() |
| |
| try { |
| fm.popBackStackImmediate() |
| fail("PopBackStackImmediate after saveState should throw IllegalStateException") |
| } catch (e: IllegalStateException) { |
| assertWithMessage("popBackStackImmediate should throw an IllegalStateException") |
| .that(e) |
| .hasMessageThat() |
| .contains("Can not perform this action after onSaveInstanceState") |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun popBackStackAfterSaveState() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commitNow() |
| |
| // Now save the state of the FragmentManager |
| fc.dispatchPause() |
| fc.saveAllState() |
| |
| val fragment2 = StrictFragment() |
| fragment2.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment2, "2") |
| .commitNowAllowingStateLoss() |
| |
| try { |
| fm.popBackStack() |
| fail("PopBackStack after saveState should throw IllegalStateException") |
| } catch (e: IllegalStateException) { |
| assertWithMessage("popBackStack should throw an IllegalStateException") |
| .that(e) |
| .hasMessageThat() |
| .contains("Can not perform this action after onSaveInstanceState") |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun popBackStackAfterManagerDestroyed() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commitNow() |
| |
| // Now destroy the Fragment Manager |
| fc.dispatchPause() |
| fc.dispatchStop() |
| fc.dispatchDestroy() |
| |
| try { |
| fm.popBackStack() |
| fail("PopBackStack after FragmentManager destroyed should throw IllegalStateException") |
| } catch (e: IllegalStateException) { |
| assertWithMessage("popBackStack should throw an IllegalStateException") |
| .that(e) |
| .hasMessageThat() |
| .contains("FragmentManager has been destroyed") |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun commitWhenFragmentManagerNeverAttached() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| |
| try { |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commit() |
| fail( |
| "Commit when FragmentManager never attached should throw " + |
| "IllegalStateException" |
| ) |
| } catch (e: IllegalStateException) { |
| assertWithMessage( |
| "Commit when FragmentManager never attached should throw an " + |
| "IllegalStateException" |
| ) |
| .that(e) |
| .hasMessageThat() |
| .contains("FragmentManager has not been attached to a host.") |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun popBackStackAndFragmentHostDestroyed() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commitNow() |
| |
| // Now save the state of the FragmentManager |
| fc.dispatchPause() |
| |
| val fragment2 = StrictFragment() |
| fragment2.retainInstance = true |
| fm.beginTransaction() |
| .add(fragment2, "2") |
| .commitNowAllowingStateLoss() |
| |
| fc.dispatchStop() |
| fc.dispatchDestroy() |
| |
| try { |
| fm.popBackStack() |
| fail("PopBackStack after host destroyed should throw IllegalStateException") |
| } catch (e: IllegalStateException) { |
| assertWithMessage("popBackStack should throw an IllegalStateException") |
| .that(e) |
| .hasMessageThat() |
| .contains("FragmentManager has been destroyed") |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun commitNowWhenFragmentHostNeverAttached() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| val fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fragment1.retainInstance = true |
| try { |
| fm.beginTransaction() |
| .add(fragment1, "1") |
| .commitNow() |
| fail("CommitNow when host never attached should throw an IllegalStateException") |
| } catch (e: IllegalStateException) { |
| assertWithMessage("CommitNow should throw an IllegalStateException") |
| .that(e) |
| .hasMessageThat() |
| .contains("FragmentManager has not been attached to a host.") |
| } |
| } |
| |
| /** |
| * When a fragment is saved in non-config, it should be restored to the same index. |
| */ |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun restoreNonConfig() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val backStackRetainedFragment = StrictFragment() |
| backStackRetainedFragment.retainInstance = true |
| val fragment1 = StrictFragment() |
| fm.beginTransaction() |
| .add(backStackRetainedFragment, "backStack") |
| .add(fragment1, "1") |
| .setPrimaryNavigationFragment(fragment1) |
| .addToBackStack(null) |
| .commit() |
| fm.executePendingTransactions() |
| val fragment2 = StrictFragment() |
| fragment2.retainInstance = true |
| fragment2.setTargetFragment(fragment1, 0) |
| val fragment3 = StrictFragment() |
| fm.beginTransaction() |
| .remove(backStackRetainedFragment) |
| .remove(fragment1) |
| .add(fragment2, "2") |
| .add(fragment3, "3") |
| .addToBackStack(null) |
| .commit() |
| fm.executePendingTransactions() |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| var foundFragment2 = false |
| for (fragment in fc.supportFragmentManager.fragments) { |
| if (fragment === fragment2) { |
| foundFragment2 = true |
| assertThat(fragment.getTargetFragment()).isNotNull() |
| assertThat(fragment.getTargetFragment()!!.tag).isEqualTo("1") |
| } else { |
| assertThat(fragment.tag).isNotEqualTo("2") |
| } |
| } |
| assertThat(foundFragment2).isTrue() |
| fc.supportFragmentManager.popBackStackImmediate() |
| val foundBackStackRetainedFragment = fc.supportFragmentManager |
| .findFragmentByTag("backStack") |
| assertWithMessage("Retained Fragment on the back stack was not retained") |
| .that(foundBackStackRetainedFragment).isEqualTo(backStackRetainedFragment) |
| } |
| |
| /** |
| * Check that retained fragments in the backstack correctly restored after two "configChanges" |
| */ |
| @Suppress("DEPRECATION") |
| @Test |
| @UiThreadTest |
| fun retainedFragmentInBackstack() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| val fragment1 = StrictFragment() |
| fm.beginTransaction().add(fragment1, "1").addToBackStack(null).commit() |
| fm.executePendingTransactions() |
| |
| val child = StrictFragment() |
| child.retainInstance = true |
| fragment1.childFragmentManager.beginTransaction().add(child, "child").commit() |
| fragment1.childFragmentManager.executePendingTransactions() |
| |
| val fragment2 = StrictFragment() |
| fm.beginTransaction().remove(fragment1).add(fragment2, "2").addToBackStack(null).commit() |
| fm.executePendingTransactions() |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| fm.popBackStackImmediate() |
| val retainedChild = fm.findFragmentByTag("1")!! |
| .childFragmentManager.findFragmentByTag("child") |
| assertThat(retainedChild).isEqualTo(child) |
| } |
| |
| /** |
| * When the FragmentManager state changes, the pending transactions should execute. |
| */ |
| @Test |
| @UiThreadTest |
| fun runTransactionsOnChange() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| val fragment1 = RemoveHelloInOnResume() |
| val fragment2 = StrictFragment() |
| fm.beginTransaction().add(fragment1, "1").setReorderingAllowed(false).commit() |
| fm.beginTransaction().add(fragment2, "Hello").setReorderingAllowed(false).commit() |
| fm.executePendingTransactions() |
| |
| assertThat(fm.fragments.size).isEqualTo(2) |
| assertThat(fm.fragments.contains(fragment1)).isTrue() |
| assertThat(fm.fragments.contains(fragment2)).isTrue() |
| |
| fc = fc.restart(activityRule, viewModelStore) |
| fm = fc.supportFragmentManager |
| |
| assertThat(fm.fragments.size).isEqualTo(1) |
| for (fragment in fm.fragments) { |
| assertThat(fragment is RemoveHelloInOnResume).isTrue() |
| } |
| } |
| |
| @Test |
| @UiThreadTest |
| @Suppress("DEPRECATION") |
| fun optionsMenu() { |
| val viewModelStore = ViewModelStore() |
| val fc = activityRule.startupFragmentController(viewModelStore) |
| val fm = fc.supportFragmentManager |
| |
| val fragment = InvalidateOptionFragment() |
| fm.beginTransaction().add(android.R.id.content, fragment).commit() |
| fm.executePendingTransactions() |
| |
| val menu = mock(Menu::class.java) |
| fc.dispatchPrepareOptionsMenu(menu) |
| assertThat(fragment.onPrepareOptionsMenuCalled).isTrue() |
| fragment.onPrepareOptionsMenuCalled = false |
| fc.shutdown(viewModelStore) |
| fc.dispatchPrepareOptionsMenu(menu) |
| assertThat(fragment.onPrepareOptionsMenuCalled).isFalse() |
| } |
| |
| /** |
| * When a retained instance fragment is saved while in the back stack, it should go |
| * through onCreate() when it is popped back. |
| */ |
| @Test |
| @UiThreadTest |
| fun retainInstanceWithOnCreate() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| val fragment1 = OnCreateFragment() |
| |
| fm.beginTransaction().add(fragment1, "1").commit() |
| fm.beginTransaction().remove(fragment1).addToBackStack(null).commit() |
| |
| fc = fc.restart(activityRule, viewModelStore) |
| |
| // Save again, but keep the state |
| fc = fc.restart(activityRule, viewModelStore, false) |
| |
| fm = fc.supportFragmentManager |
| |
| fm.popBackStackImmediate() |
| val fragment2 = fm.findFragmentByTag("1") as OnCreateFragment |
| assertThat(fragment2.onCreateCalled).isTrue() |
| fm.popBackStackImmediate() |
| } |
| |
| /** |
| * A retained instance fragment should go through onCreate() once, even through save and |
| * restore. |
| */ |
| @Test |
| @UiThreadTest |
| fun retainInstanceOneOnCreate() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| val fragment = OnCreateFragment() |
| |
| fm.beginTransaction().add(fragment, "fragment").commit() |
| fm.executePendingTransactions() |
| |
| fm.beginTransaction().remove(fragment).addToBackStack(null).commit() |
| |
| assertThat(fragment.onCreateCalled).isTrue() |
| fragment.onCreateCalled = false |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| fm.popBackStackImmediate() |
| assertThat(fragment.onCreateCalled).isFalse() |
| } |
| |
| /** |
| * A retained instance fragment added via XML should go through onCreate() once, but should get |
| * onInflate calls for each inflation. |
| */ |
| @Test |
| @UiThreadTest |
| fun retainInstanceLayoutOnInflate() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| var parentFragment = RetainedInflatedParentFragment() |
| |
| fm.beginTransaction().add(android.R.id.content, parentFragment).commit() |
| fm.executePendingTransactions() |
| |
| val childFragment = parentFragment.childFragmentManager |
| .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment |
| |
| fm.beginTransaction().remove(parentFragment).addToBackStack(null).commit() |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| fm.popBackStackImmediate() |
| |
| parentFragment = fm.findFragmentById(android.R.id.content) as RetainedInflatedParentFragment |
| val childFragment2 = parentFragment.childFragmentManager |
| .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment |
| |
| assertWithMessage("Child Fragment should be retained") |
| .that(childFragment2).isEqualTo(childFragment) |
| assertWithMessage("Child Fragment should have onInflate called twice") |
| .that(childFragment2.mOnInflateCount).isEqualTo(2) |
| } |
| |
| @Test |
| @UiThreadTest |
| fun retainInstanceLayoutOnInflateWithFragmentContainerView() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| var parentFragment = RetainedInflatedParentFragmentContainerView() |
| |
| fm.beginTransaction().add(android.R.id.content, parentFragment).commit() |
| fm.executePendingTransactions() |
| |
| val childFragment = parentFragment.childFragmentManager |
| .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment |
| |
| fm.beginTransaction().remove(parentFragment).addToBackStack(null).commit() |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| fm.popBackStackImmediate() |
| |
| parentFragment = fm.findFragmentById(android.R.id.content) as |
| RetainedInflatedParentFragmentContainerView |
| val childFragment2 = parentFragment.childFragmentManager |
| .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment |
| |
| assertWithMessage("Child Fragment should be retained") |
| .that(childFragment2).isEqualTo(childFragment) |
| assertWithMessage("Child Fragment should have onInflate called once") |
| .that(childFragment2.mOnInflateCount).isEqualTo(1) |
| } |
| |
| /** |
| * When a Fragment is added solely via a <fragment> tag, we need to specifically |
| * test what happens when a configuration change, etc. happens that removes the |
| * <fragment> tag from the layout. |
| * |
| * What should happen is that the Fragment remains in the FragmentManager, but it |
| * should not move beyond CREATED until it is re-added to the layout. |
| * |
| * SwappingInflatedParentFragment switches between two layouts: one with a |
| * <fragment> tag and one without. This allows us to test the transitions |
| * between the two just by restarting the FragmentController (effectively |
| * going through a virtual configuration change between two different layouts). |
| */ |
| @Test |
| @UiThreadTest |
| fun inflatedFragmentNotInLayout() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| var parentFragment = SwappingInflatedParentFragment() |
| fm.beginTransaction().add(android.R.id.content, parentFragment).commit() |
| fm.executePendingTransactions() |
| |
| var childFragment = parentFragment.childFragmentManager |
| .findFragmentById(R.id.inflated_fragment) |
| // The child fragment was added via a <fragment> tag, so it |
| // should receive lifecycle events by default |
| assertThat(childFragment).isNotNull() |
| assertThat(childFragment!!.lifecycle.currentState) |
| .isEqualTo(Lifecycle.State.RESUMED) |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| parentFragment = fm.findFragmentById(android.R.id.content) as |
| SwappingInflatedParentFragment |
| childFragment = parentFragment.childFragmentManager |
| .findFragmentById(R.id.inflated_fragment) |
| // Ensure the Fragment is still in the FragmentManager, but hasn't moved |
| // beyond CREATED since it isn't in the layout this time |
| assertThat(childFragment).isNotNull() |
| assertThat(childFragment!!.lifecycle.currentState) |
| .isEqualTo(Lifecycle.State.CREATED) |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| parentFragment = fm.findFragmentById(android.R.id.content) as |
| SwappingInflatedParentFragment |
| childFragment = parentFragment.childFragmentManager |
| .findFragmentById(R.id.inflated_fragment) |
| // Now that the <fragment> tag is back, the fragment should receive |
| // lifecycle events again |
| assertThat(childFragment).isNotNull() |
| assertThat(childFragment!!.lifecycle.currentState) |
| .isEqualTo(Lifecycle.State.RESUMED) |
| } |
| |
| /** |
| * Confirm that when a Fragment added via the <fragment> tag is removed from the layout |
| * that we still clear any non config state when the FragmentManager is destroyed. |
| */ |
| @Test |
| @UiThreadTest |
| fun inflatedFragmentNotInLayoutDestroysViewModel() { |
| val viewModelStore = ViewModelStore() |
| var fc = activityRule.startupFragmentController(viewModelStore) |
| var fm = fc.supportFragmentManager |
| |
| var parentFragment = SwappingInflatedParentFragment() |
| fm.beginTransaction().add(android.R.id.content, parentFragment).commit() |
| fm.executePendingTransactions() |
| |
| var childFragment = parentFragment.childFragmentManager |
| .findFragmentById(R.id.inflated_fragment) |
| // The child fragment was added via a <fragment> tag, so it |
| // should receive lifecycle events by default |
| assertThat(childFragment).isNotNull() |
| assertThat(childFragment!!.lifecycle.currentState) |
| .isEqualTo(Lifecycle.State.RESUMED) |
| |
| // Add a ViewModel to the child fragment so that it has some retained state |
| val createdViewModel = ViewModelProvider(childFragment)[TestViewModel::class.java] |
| |
| fc = fc.restart(activityRule, viewModelStore, false) |
| fm = fc.supportFragmentManager |
| |
| parentFragment = fm.findFragmentById(android.R.id.content) as |
| SwappingInflatedParentFragment |
| childFragment = parentFragment.childFragmentManager |
| .findFragmentById(R.id.inflated_fragment) |
| // Ensure the Fragment is still in the FragmentManager, but hasn't moved |
| // beyond CREATED since it isn't in the layout this time |
| assertThat(childFragment).isNotNull() |
| assertThat(childFragment!!.lifecycle.currentState) |
| .isEqualTo(Lifecycle.State.CREATED) |
| assertThat(createdViewModel.cleared) |
| .isFalse() |
| |
| fc.shutdown(viewModelStore, true) |
| |
| assertThat(createdViewModel.cleared) |
| .isTrue() |
| } |
| |
| @Test |
| fun inflatedFragmentTagAfterResume() { |
| with(ActivityScenario.launch(FragmentTestActivity::class.java)) { |
| val fragment = withActivity { |
| setContentView(R.layout.activity_inflated_fragment) |
| val fm = supportFragmentManager |
| fm.findFragmentById(R.id.inflated_fragment) as StrictViewFragment |
| } |
| |
| assertThat(fragment).isNotNull() |
| assertThat(fragment.isResumed).isTrue() |
| } |
| } |
| |
| @Test |
| fun inflatedFragmentContainerViewAfterResume() { |
| with(ActivityScenario.launch(FragmentTestActivity::class.java)) { |
| var fragment = withActivity { |
| setContentView(R.layout.inflated_fragment_container_view) |
| val fm = supportFragmentManager |
| fm.findFragmentById(R.id.fragment_container_view) as InflatedFragment |
| } |
| |
| assertThat(fragment).isNotNull() |
| assertThat(fragment.isResumed).isTrue() |
| |
| recreate() |
| |
| fragment = withActivity { |
| setContentView(R.layout.inflated_fragment_container_view) |
| val fm = supportFragmentManager |
| fm.findFragmentById(R.id.fragment_container_view) as InflatedFragment |
| } |
| |
| assertThat(fragment).isNotNull() |
| assertThat(fragment.requireView().parent).isNotNull() |
| assertThat(fragment.isResumed).isTrue() |
| } |
| } |
| |
| @Test |
| fun inflatedFragmentContainerViewWithMultipleFragmentsAfterResume() { |
| with(ActivityScenario.launch(FragmentTestActivity::class.java)) { |
| val addedFragment1 = StrictViewFragment() |
| val addedFragment2 = StrictViewFragment() |
| var fragment = withActivity { |
| setContentView(R.layout.inflated_fragment_container_view) |
| val fm = supportFragmentManager |
| fm.beginTransaction() |
| .add(R.id.fragment_container_view, addedFragment1, "addedFragment1") |
| .add(R.id.fragment_container_view, addedFragment2, "addedFragment2") |
| .commitNow() |
| fm.findFragmentByTag("fragment1") as InflatedFragment |
| } |
| |
| assertThat(fragment).isNotNull() |
| assertThat(fragment.isResumed).isTrue() |
| assertThat(addedFragment1.isResumed).isTrue() |
| assertThat(addedFragment2.isResumed).isTrue() |
| |
| recreate() |
| |
| val fm = withActivity { |
| setContentView(R.layout.inflated_fragment_container_view) |
| supportFragmentManager |
| } |
| |
| fragment = fm.findFragmentByTag("fragment1") as InflatedFragment |
| val restoredAddedFragment1 = |
| fm.findFragmentByTag("addedFragment1") as StrictViewFragment |
| val restoredAddedFragment2 = |
| fm.findFragmentByTag("addedFragment2") as StrictViewFragment |
| |
| assertThat(fragment).isNotNull() |
| |
| assertThat(fragment.requireView().parent).isNotNull() |
| assertThat(restoredAddedFragment1.requireView().parent).isNotNull() |
| assertThat(restoredAddedFragment2.requireView().parent).isNotNull() |
| |
| assertThat(fragment.isResumed).isTrue() |
| assertThat(restoredAddedFragment1.isResumed).isTrue() |
| assertThat(restoredAddedFragment2.isResumed).isTrue() |
| } |
| } |
| |
| @Test |
| @UiThreadTest |
| fun testReplaceChildFragmentInViewCreated() { |
| val viewModelStore = ViewModelStore() |
| val fc = FragmentController.createController( |
| ControllerHostCallbacks(activityRule.activity, viewModelStore) |
| ) |
| fc.attachHost(null) |
| fc.dispatchCreate() |
| |
| val fm = fc.supportFragmentManager |
| |
| val fragment = AddChildInOnCreateParentFragment() |
| fm.beginTransaction() |
| .add(android.R.id.content, fragment) |
| .commitNow() |
| |
| fc.dispatchActivityCreated() |
| fc.shutdown(viewModelStore, true) |
| } |
| |
| class AddChildInOnCreateParentFragment : StrictViewFragment(R.layout.simple_container) { |
| lateinit var replaceInViewCreateFragment: ReplaceInViewCreatedParentFragment |
| |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| replaceInViewCreateFragment = ReplaceInViewCreatedParentFragment() |
| childFragmentManager.beginTransaction() |
| .replace(R.id.fragmentContainer, replaceInViewCreateFragment) |
| .commit() |
| } |
| } |
| |
| class ReplaceInViewCreatedParentFragment : StrictViewFragment(R.layout.fragment_a) { |
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| super.onViewCreated(view, savedInstanceState) |
| viewLifecycleOwner.lifecycle.addObserver( |
| LifecycleEventObserver { _, _ -> } |
| ) |
| parentFragmentManager.beginTransaction() |
| .replace(R.id.fragmentContainer, StrictViewFragment()) |
| .commit() |
| } |
| } |
| |
| private fun executePendingTransactions(fm: FragmentManager) { |
| activityRule.runOnUiThread { fm.executePendingTransactions() } |
| } |
| |
| class ParentFragment : StrictViewFragment(R.layout.simple_container) |
| |
| /** |
| * This tests a deliberately odd use of a child fragment, added in onCreateView instead |
| * of elsewhere. It simulates creating a UI child fragment added to the view hierarchy |
| * created by this fragment. |
| */ |
| class ChildFragmentManagerFragment : StrictFragment() { |
| private lateinit var savedChildFragmentManager: FragmentManager |
| private lateinit var childFragmentManagerChildFragment: ChildFragmentManagerChildFragment |
| |
| val childFragment: Fragment? |
| get() = childFragmentManagerChildFragment |
| |
| override fun onAttach(context: Context) { |
| super.onAttach(context) |
| savedChildFragmentManager = childFragmentManager |
| } |
| |
| override fun onCreateView( |
| inflater: LayoutInflater, |
| container: ViewGroup?, |
| savedInstanceState: Bundle? |
| ) = TextView(inflater.context).also { |
| assertWithMessage("child FragmentManagers not the same instance") |
| .that(childFragmentManager).isSameInstanceAs(savedChildFragmentManager) |
| var child = savedChildFragmentManager |
| .findFragmentByTag("tag") as ChildFragmentManagerChildFragment? |
| if (child == null) { |
| child = ChildFragmentManagerChildFragment("foo") |
| savedChildFragmentManager.beginTransaction().add(child, "tag").commitNow() |
| assertWithMessage("argument strings don't match") |
| .that(child.string).isEqualTo("foo") |
| } |
| childFragmentManagerChildFragment = child |
| } |
| } |
| |
| class ChildFragmentManagerChildFragment : StrictFragment { |
| lateinit var string: String |
| private set |
| |
| constructor() |
| |
| constructor(arg: String) { |
| val b = Bundle() |
| b.putString("string", arg) |
| arguments = b |
| } |
| |
| override fun onAttach(context: Context) { |
| super.onAttach(context) |
| string = requireArguments().getString("string", "NO VALUE") |
| } |
| } |
| |
| class AddChildInOnAttachFragment : StrictFragment() { |
| override fun onAttach(context: Context) { |
| super.onAttach(context) |
| |
| childFragmentManager.beginTransaction() |
| .add(Fragment(), "child") |
| .commitNow() |
| } |
| } |
| |
| class RemoveHelloInOnResume : Fragment() { |
| override fun onResume() { |
| super.onResume() |
| val fragment = parentFragmentManager.findFragmentByTag("Hello") |
| if (fragment != null) { |
| parentFragmentManager.beginTransaction().remove(fragment).commit() |
| } |
| } |
| } |
| |
| class InvalidateOptionFragment : Fragment() { |
| var onPrepareOptionsMenuCalled: Boolean = false |
| |
| init { |
| setHasOptionsMenu(true) |
| } |
| |
| override fun onPrepareOptionsMenu(menu: Menu) { |
| onPrepareOptionsMenuCalled = true |
| assertThat(context).isNotNull() |
| super.onPrepareOptionsMenu(menu) |
| } |
| } |
| |
| @Suppress("DEPRECATION") |
| class OnCreateFragment : Fragment() { |
| var onCreateCalled: Boolean = false |
| |
| init { |
| retainInstance = true |
| } |
| |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| onCreateCalled = true |
| } |
| } |
| |
| class RetainedInflatedParentFragment : |
| Fragment(R.layout.nested_retained_inflated_fragment_parent) |
| |
| class RetainedInflatedParentFragmentContainerView : |
| Fragment(R.layout.nested_retained_inflated_fragment_container_parent) |
| |
| class RetainedInflatedChildFragment : Fragment(R.layout.nested_inflated_fragment_child) { |
| internal var mOnInflateCount = 0 |
| |
| @Suppress("DEPRECATION") |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| retainInstance = true |
| } |
| |
| override fun onInflate(context: Context, attrs: AttributeSet, savedInstanceState: Bundle?) { |
| super.onInflate(context, attrs, savedInstanceState) |
| mOnInflateCount++ |
| } |
| } |
| |
| /** |
| * A fragment which swaps between two layouts every time it is created: one with |
| * a <fragment> tag and one empty layout. |
| */ |
| class SwappingInflatedParentFragment : Fragment() { |
| private var count = 0 |
| |
| override fun onCreate(savedInstanceState: Bundle?) { |
| super.onCreate(savedInstanceState) |
| count = savedInstanceState?.getInt("COUNT") ?: 0 |
| } |
| |
| override fun onCreateView( |
| inflater: LayoutInflater, |
| container: ViewGroup?, |
| savedInstanceState: Bundle? |
| ): View? { |
| return inflater.inflate( |
| if (count % 2 == 0) { |
| R.layout.activity_inflated_fragment |
| } else { |
| R.layout.activity_content |
| }, |
| container, false |
| ) |
| } |
| |
| override fun onSaveInstanceState(outState: Bundle) { |
| super.onSaveInstanceState(outState) |
| outState.putInt("COUNT", count + 1) |
| } |
| } |
| } |