使用FragmentScenario测试Fragment生命周期时1.3.1版仍出现onCreateView重复调用问题
问题场景
- 测试开发阶段使用
androidx.fragment:fragment-testing编写仪器测试,核心校验逻辑为:模拟应用被Home键切到后台再切回的场景(通过FragmentScenario.moveToState()将Fragment状态从RESUMED切到CREATED,再切回RESUMED),验证Fragment在停止、恢复状态切换时的底层逻辑正确性。 - 此前使用
androidx.fragment:fragment-testing:1.2.5时所有测试用例均可正常通过,升级依赖版本到1.3.1后,上述生命周期相关测试全部失败。
异常表现
- 生命周期切换过程中,本不应该重复触发的
Fragment.onCreateView()回调被多次调用,直接导致Fragment的View被重置为布局文件定义的初始状态。 - 手动操作复现场景(正常切后台、切回前台)时不会出现View重置的问题,仅仪器测试环境能复现该异常。
- 具体用例失败情况:
- 点击TextView修改自定义文本后,经过一轮停-恢复生命周期切换,文本被重置为布局默认值
- 点击隐藏Button后,经过一轮停-恢复生命周期切换,Button重新恢复可见状态
- 仅EditText的文本校验可以正常通过,这是因为EditText的输入状态本身具备系统自动保存机制。
复现代码
被测Fragment代码
class LifecycleBugFragment : Fragment() { lateinit var textView: TextView lateinit var editText: EditText lateinit var button: Button override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_lifecycle_bug, container, false) textView = view.findViewById<TextView>(R.id.textView) textView.setOnClickListener { textView.text = "I was clicked" } editText = view.findViewById<EditText>(R.id.editText) button = view.findViewById<Button>(R.id.button) button.setOnClickListener { button.visibility = View.GONE } return view } }
Fragment布局代码
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".fragmenttesting.LifecycleBugFragment"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="default text" /> <EditText android:id="@+id/editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="text" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="click to hide me" /> </LinearLayout>
测试类代码
const val TYPED_TEXT = "some example text" const val DEFAULT_TEXT = "default text" const val CLICKED_TEXT = "I was clicked" class LifecycleBugFragmentTest { lateinit var fragmentScenario: FragmentScenario<LifecycleBugFragment> @Before fun setUp() { fragmentScenario = FragmentScenario.launchInContainer(LifecycleBugFragment::class.java) } @Test fun whenTextViewclickedAndFragmentLifecycleStoppedAndResumed_ThenTextViewTextIsStillChanged() { onView(withId(R.id.textView)).check(matches(withText(DEFAULT_TEXT))) onView(withId(R.id.textView)).perform(click()) onView(withId(R.id.textView)).check(matches(withText(CLICKED_TEXT))) stopAndResumeFragment() onView(withId(R.id.textView)).check(matches(withText(CLICKED_TEXT))) } // 此用例可通过,其余用例均失败 @Test fun whenEditTextIsEditedAndFragmentLifecycleStoppedAndResumed_ThenEditTextTextIsStillChanged() { onView(withId(R.id.editText)).perform(typeText(TYPED_TEXT)) stopAndResumeFragment() onView(withId(R.id.editText)).check(matches(withText(TYPED_TEXT))) } @Test fun whenButtonIsClickedAndFragmentLifecycleStoppedAndResumed_ThenButtonISStillNotVisible() { onView(withId(R.id.button)).perform(click()) onView(withId(R.id.button)).check(matches(not(isDisplayed()))) stopAndResumeFragment() onView(withId(R.id.button)).check(matches(not(isDisplayed()))) } private fun stopAndResumeFragment() { fragmentScenario.moveToState(Lifecycle.State.CREATED) fragmentScenario.moveToState(Lifecycle.State.RESUMED) } }
初始依赖配置
dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.31" implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation "androidx.test:runner:1.3.0" androidTestImplementation "androidx.test:core:1.3.0" androidTestImplementation "androidx.test.ext:junit:1.1.2" androidTestImplementation "androidx.test:rules:1.3.0" implementation "androidx.navigation:navigation-fragment-ktx:2.3.4" implementation "androidx.navigation:navigation-ui-ktx:2.3.4" androidTestImplementation "androidx.navigation:navigation-testing:2.3.4" debugImplementation "androidx.fragment:fragment-testing:1.3.1" implementation "androidx.navigation:navigation-compose:1.0.0-alpha09" // 其他无关依赖已省略 }
已排查方向
- 对照已知历史缺陷:
onCreateView()重复调用的问题早在Fragment 1.3.0-alpha08版本就已完成修复,理论上1.3.1版本不存在该已知问题。 - 初步怀疑依赖解析异常:项目没有直接声明
androidx.fragment:fragment主依赖,该库均以传递依赖的形式引入,担心实际解析到的版本低于带修复的阈值,未包含缺陷修复代码。 - 已添加如下Gradle依赖约束,强制全项目统一解析
androidx.fragment:fragment:1.3.1、androidx.fragment:fragment-ktx:1.3.1版本,问题依旧存在,排除显式版本解析错误的可能。
constraints { implementation('androidx.fragment:fragment:1.3.1') { because 'avoid bug' } implementation('androidx.fragment:fragment-ktx:1.3.1') { because 'avoid bug' } }
问题根因
异常为Gradle传递依赖的版本错配导致:
- 项目引入的
androidx.navigation:navigation-compose:1.0.0-alpha09为早期alpha版本,其内部传递依赖的androidx.fragment:fragment版本为1.3.0-alpha07——该版本刚好早于缺陷修复的阈值(1.3.0-alpha08),包含那个已修复的onCreateView重复调用的问题。 - 你添加的Gradle依赖约束仅作用于
implementation配置范围,没有覆盖到仪器测试运行对应的debugImplementation、androidTestImplementation等配置。最终测试运行时,实际加载的是被Navigation Compose拉低的alpha07版本Fragment类,因此命中已修复的历史缺陷,和我们预期的1.3.1版本行为不一致。
可落地的解决方向
- 扩展依赖约束的覆盖范围,将Fragment 1.3.1的版本约束应用到所有构建配置,确保主代码、测试代码、传递依赖拉取的Fragment版本全部统一对齐。
- 升级
androidx.navigation:navigation-compose到稳定正式版本,正式版内部依赖的Fragment版本会自动对齐到修复后的阈值,从根源避免旧快照版本的传递依赖拉低整体依赖版本。 - 验证方式:执行
./gradlew :app:dependencies --configuration debugAndroidTest,检查输出中androidx.fragment:fragment的实际解析版本,确认是否存在被低版本依赖拉低的情况。
内容的提问来源于stack exchange,提问作者Piotr Śmietana
相关产品推荐
相关产品推荐

